/* ==========================================================================
   Ward Intelligence Platform — app.css
   One hand-written stylesheet. No preprocessor, no framework, no CDN.
   Mobile first: base rules are the 320-480px design; min-width queries add.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. DIVERGING CHOROPLETH RAMP  ***  app.js reads these exact tokens  ***
   --------------------------------------------------------------------------
   Scale runs on pain_score (0 = best/improving  ->  100 = worst/worsening).
   Blue = improving, pale = flat, red = worsening. Never a rainbow.

   Bucket breakpoints (app.js MUST mirror these):
     scale-1  score <  15
     scale-2  score <  30
     scale-3  score <  45
     scale-4  score <  55   (flat / pale)
     scale-5  score <  70
     scale-6  score <  85
     scale-7  score >= 85
   -------------------------------------------------------------------------- */

:root {
  --scale-1: #2166ac;
  --scale-2: #6a9dc7;
  --scale-3: #bcd7e6;
  --scale-4: #ece5d7;
  --scale-5: #f2bda3;
  --scale-6: #d97a5f;
  --scale-7: #a02c1e;

  /* Legible text colour to sit on each swatch. */
  --on-1: #ffffff;
  --on-2: #10202c;
  --on-3: #14120f;
  --on-4: #14120f;
  --on-5: #14120f;
  --on-6: #17100d;
  --on-7: #ffffff;

  --scale-na: #d9d2c4;
  --scale-stroke: rgba(20, 18, 15, 0.35);

  /* Sequential ramp, for /demographics only.
     The diverging ramp above encodes polarity: pale is flat, and the two ends
     mean improving and worsening. Demography has no such axis — there is no
     good or bad end to "share of residents whose mother tongue is Tamil" — so
     shading it on a red/blue diverging scale would assert a judgement the data
     does not make. This is one hue, light to dark, magnitude only.
     Single hue (8 degrees of drift across the ramp) and monotonic in OKLCH
     lightness, 0.948 down to 0.326. */
  --dem-1: #e8f0ed;
  --dem-2: #c9dfd8;
  --dem-3: #a3cabf;
  --dem-4: #78b0a2;
  --dem-5: #4d9385;
  --dem-6: #2b7568;
  --dem-7: #0d3d35;

  /* Sequential ramp, /crime only. A third ramp rather than reusing the teal:
     a reader flipping between /demographics and /crime must not mistake one
     map for the other, and identical shading on two maps that mean different
     things is exactly how that happens. Amber, single hue, monotonic in
     lightness (0.947 -> 0.424). Still sequential, NOT diverging — a crime rate
     has a high end and a low end but no meaningful midpoint to diverge from. */
  --crime-1: #f6ecdc;
  --crime-2: #eed9b8;
  --crime-3: #e2c08d;
  --crime-4: #d3a262;
  --crime-5: #bd8340;
  --crime-6: #9c6328;
  --crime-7: #6d4318;

  /* Categorical series colours, for the overview charts only. Two hues,
     assigned in fixed order and never cycled. Validated against this page's
     paper surface: colour-vision separation 20.8 (protan) and 27.4 (normal),
     both above the floor, and both clear 3:1 contrast. Every series is direct-
     labelled as well, so identity never rests on colour alone. */
  --series-1: #1668a8;
  --series-2: #b85417;
}

/* --------------------------------------------------------------------------
   2. Surface, ink and type tokens
   -------------------------------------------------------------------------- */

:root {
  --paper:      #faf7f1;   /* warm off-white */
  --paper-2:    #f4efe4;   /* recessed band */
  --paper-3:    #efe8da;   /* table zebra / chips */
  --ink:        #14120f;   /* near black */
  --ink-2:      #4a453d;
  --ink-3:      #7a736a;   /* muted labels */
  --rule:       #ddd5c7;
  --rule-2:     #c3b8a4;

  --accent:     #14594e;   /* the one accent colour */
  --accent-2:   #0d3d35;
  --accent-bg:  #e3ede9;

  --serif: Georgia, "Iowan Old Style", "Palatino Linotype", Palatino,
           "Times New Roman", serif;
  --sans:  -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
           "Helvetica Neue", Arial, sans-serif;
  --mono:  ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono",
           monospace;

  --tap: 44px;               /* minimum touch target */
  --nav-h: 44px;             /* nav item height — links AND dropdown summaries
                                read this, so their underlines share a baseline */
  --measure: 68ch;
  --radius: 3px;             /* restrained; this is a document, not a card UI */
}

/* --------------------------------------------------------------------------
   3. Reset / base
   -------------------------------------------------------------------------- */

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

html {
  font-size: 100%;
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  max-width: 100%;
  overflow-x: hidden;        /* belt; the braces are min-width:0 on flex/grid kids */
  background: var(--paper);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 1rem;
  line-height: 1.55;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
}

img, svg { max-width: 100%; height: auto; }

a { color: var(--accent); text-decoration-thickness: 1px; text-underline-offset: 2px; }
a:hover { color: var(--accent-2); }
a:focus-visible,
button:focus-visible,
select:focus-visible,
summary:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--radius);
}

h1, h2, h3, h4 {
  font-family: var(--serif);
  font-weight: 700;
  line-height: 1.18;
  margin: 0 0 .4em;
  letter-spacing: -0.01em;
}

h1 { font-size: 1.6rem; }
h2 { font-size: 1.25rem; }
h3 { font-size: 1.05rem; }
h4 { font-size: .95rem; font-family: var(--sans); }

p { margin: 0 0 .85em; max-width: var(--measure); }

/* Small caps-ish section label used everywhere. */
.eyebrow,
.label {
  font-family: var(--sans);
  font-size: .7rem;
  font-weight: 600;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin: 0;
}

.muted { color: var(--ink-3); }
.small { font-size: .85rem; }
.wide { max-width: none; }

/* Numbers are the design. */
.tnum,
td.num, th.num,
.figure, .score {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

.figure {
  font-family: var(--serif);
  font-size: 2rem;
  line-height: 1;
  font-weight: 700;
  display: block;
}
.figure-lg { font-size: 2.6rem; }
.figure-sm { font-size: 1.4rem; }

.nil {
  color: var(--ink-3);
  font-variant-numeric: normal;
}

.delta-pos::before,
.delta-neg::before { content: ""; }
.delta-pos, .delta-neg { font-weight: 600; }
.delta-flat { color: var(--ink-3); }

.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  background: var(--ink);
  color: var(--paper);
  padding: .6rem 1rem;
  z-index: 1000;
}
.skip:focus { left: 0; }

.vh {
  position: absolute;
  width: 1px; height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* --------------------------------------------------------------------------
   4. Shell: masthead, nav, main, footer
   -------------------------------------------------------------------------- */

.wrap {
  width: 100%;
  max-width: 1160px;
  margin: 0 auto;
  padding: 0 1rem;
}

.masthead {
  border-bottom: 1px solid var(--rule);
  background: var(--paper);
}



.wordmark {
  font-family: var(--serif);
  font-size: 1.05rem;
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--ink);
  text-decoration: none;
  margin: 0;
}
.wordmark span { color: var(--accent); }

.masthead-note {
  font-size: .72rem;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--ink-3);
}


/* --------------------------------------------------------------------------
   5. Controls
   -------------------------------------------------------------------------- */

.controls {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: .6rem;
  padding: .85rem 0 1rem;
  border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
}

.field { display: flex; flex-direction: column; gap: .25rem; flex: 1 1 100%; min-width: 0; }
.field > label { font-size: .7rem; font-weight: 600; letter-spacing: .09em; text-transform: uppercase; color: var(--ink-3); }

select, .btn {
  min-height: var(--tap);
  font-family: inherit;
  font-size: 1rem;
  color: var(--ink);
  background: var(--paper);
  border: 1px solid var(--rule-2);
  border-radius: var(--radius);
  padding: .5rem .7rem;
  width: 100%;
}
select { appearance: none; padding-right: 2.2rem;
  background-image: linear-gradient(45deg, transparent 50%, var(--ink-2) 50%),
                    linear-gradient(135deg, var(--ink-2) 50%, transparent 50%);
  background-position: right 1.05rem center, right .8rem center;
  background-size: 6px 6px, 6px 6px;
  background-repeat: no-repeat;
}

.btn {
  width: auto;
  cursor: pointer;
  font-weight: 600;
  background: var(--ink);
  color: var(--paper);
  border-color: var(--ink);
}
.btn:hover { background: var(--accent); border-color: var(--accent); }
.btn-quiet {
  background: transparent;
  color: var(--accent);
  border-color: var(--rule-2);
}
.btn-quiet:hover { background: var(--accent-bg); color: var(--accent-2); border-color: var(--accent); }

/* Non-JS fallback submit; hidden once app.js wires change-to-submit. */
.js-on .fallback-submit { display: none; }

/* --------------------------------------------------------------------------
   6. The ramp: legend swatches and score chips
   -------------------------------------------------------------------------- */

.scale-1 { background: var(--scale-1); color: var(--on-1); }
.scale-2 { background: var(--scale-2); color: var(--on-2); }
.scale-3 { background: var(--scale-3); color: var(--on-3); }
.scale-4 { background: var(--scale-4); color: var(--on-4); }
.scale-5 { background: var(--scale-5); color: var(--on-5); }
.scale-6 { background: var(--scale-6); color: var(--on-6); }
.scale-7 { background: var(--scale-7); color: var(--on-7); }
.scale-na { background: var(--scale-na); color: var(--ink-2); }

/* Sequential swatches, /demographics legend. */
.dem-1 { background: var(--dem-1); }
.dem-2 { background: var(--dem-2); }
.dem-3 { background: var(--dem-3); }
.dem-4 { background: var(--dem-4); }
.dem-5 { background: var(--dem-5); }
.dem-6 { background: var(--dem-6); }
.dem-7 { background: var(--dem-7); }

/* Sequential swatches, /crime legend. */
.crime-1 { background: var(--crime-1); }
.crime-2 { background: var(--crime-2); }
.crime-3 { background: var(--crime-3); }
.crime-4 { background: var(--crime-4); }
.crime-5 { background: var(--crime-5); }
.crime-6 { background: var(--crime-6); }
.crime-7 { background: var(--crime-7); }

.legend {
  padding: .9rem 0 1.1rem;
  border-bottom: 1px solid var(--rule);
}
.legend-scale {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 2px;
  margin: .35rem 0 .4rem;
  max-width: 380px;
}
.legend-scale span {
  height: 16px;
  border-radius: 1px;
  display: block;
}
.legend-ends {
  display: flex;
  justify-content: space-between;
  max-width: 380px;
  font-size: .72rem;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink-3);
  font-weight: 600;
}
.legend-note { font-size: .82rem; color: var(--ink-3); margin: .5rem 0 0; }

.chip {
  display: inline-block;
  min-width: 3.1rem;
  padding: .18rem .45rem;
  border-radius: var(--radius);
  font-variant-numeric: tabular-nums;
  font-weight: 700;
  font-size: .95rem;
  text-align: center;
  line-height: 1.25;
}

.tag {
  display: inline-block;
  padding: .12rem .45rem;
  border: 1px solid var(--rule-2);
  border-radius: 999px;
  font-size: .7rem;
  font-weight: 700;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--ink-2);
  background: var(--paper-2);
  text-decoration: none;
  white-space: nowrap;
}
a.tag:hover { border-color: var(--accent); color: var(--accent); }

/* --------------------------------------------------------------------------
   7. Map
   -------------------------------------------------------------------------- */

.map-panel {
  border-bottom: 1px solid var(--rule);
  margin: 0;
}
.map-panel > summary {
  min-height: var(--tap);
  display: flex;
  align-items: center;
  gap: .5rem;
  cursor: pointer;
  font-weight: 600;
  font-size: .92rem;
  color: var(--accent);
  list-style: none;
  padding: .2rem 0;
}
.map-panel > summary::-webkit-details-marker { display: none; }
.map-panel > summary::before {
  content: "▸";
  display: inline-block;
  transition: transform .15s ease;
}
.map-panel[open] > summary::before { transform: rotate(90deg); }

/* #pollmap is the 1,445-subdivision map on /targeting. It shares every rule
   with the ward map, including the print suppression below: a map is the one
   thing on this site that does not survive being printed in greyscale. */
#map, #pollmap, #demmap, #histmap, #crimemap {
  height: 260px;
  width: 100%;
  margin: .5rem 0 1rem;
  background: var(--paper-2);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
}
.leaflet-container { background: var(--paper-2); font-family: var(--sans); outline: 0; }
.leaflet-tooltip {
  font-family: var(--sans);
  font-size: .85rem;
  background: var(--ink);
  color: var(--paper);
  border: 0;
  box-shadow: none;
  border-radius: var(--radius);
  padding: .35rem .55rem;
}
.leaflet-tooltip::before { display: none; }
.leaflet-tooltip b { font-weight: 700; }
.leaflet-tooltip .t-score { font-variant-numeric: tabular-nums; }

/* --------------------------------------------------------------------------
   8. Ranked ward list — the primary surface on mobile
   -------------------------------------------------------------------------- */

.rank-list { list-style: none; margin: 0; padding: 0; }

.rank-row {
  display: grid;
  grid-template-columns: 2.1rem 1fr auto;
  grid-template-areas:
    "rank name  score"
    "rank meta  score";
  align-items: center;
  column-gap: .7rem;
  min-height: var(--tap);
  padding: .7rem .25rem;
  border-bottom: 1px solid var(--rule);
  color: inherit;
  text-decoration: none;
}
.rank-row:hover, .rank-row:focus-visible { background: var(--paper-2); }
.rank-row[aria-current="true"] {
  background: var(--accent-bg);
  box-shadow: inset 3px 0 0 var(--accent);
}

.rank-ord {
  grid-area: rank;
  font-family: var(--serif);
  font-size: 1rem;
  font-weight: 700;
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
  text-align: right;
}
.rank-name {
  grid-area: name;
  font-family: var(--serif);
  font-size: 1.05rem;
  font-weight: 700;
  min-width: 0;
}
.rank-name .wid { color: var(--ink-3); font-weight: 400; }
.rank-meta {
  grid-area: meta;
  font-size: .82rem;
  color: var(--ink-3);
  display: flex;
  flex-wrap: wrap;
  gap: .1rem .6rem;
  min-width: 0;
}
.rank-meta .tnum { color: var(--ink-2); }
.rank-score { grid-area: score; text-align: right; }

.list-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: .3rem 1rem;
  padding: 1.4rem 0 .5rem;
  border-bottom: 2px solid var(--ink);
}
.list-head h2 { margin: 0; }

/* --------------------------------------------------------------------------
   9. Ward panel (HTMX target)
   -------------------------------------------------------------------------- */

#ward-panel { scroll-margin-top: 1rem; }

.panel-empty {
  border: 1px dashed var(--rule-2);
  border-radius: var(--radius);
  padding: 1.5rem 1.1rem;
  color: var(--ink-3);
  text-align: center;
  font-size: .92rem;
}

.ward-panel { padding-top: .5rem; }

.ward-head {
  border-bottom: 2px solid var(--ink);
  padding-bottom: .8rem;
  margin-bottom: 1.1rem;
}
.ward-head h2 { font-size: 1.55rem; margin-bottom: .15rem; }
.ward-head .incumbent { font-size: 1rem; color: var(--ink-2); margin: 0 0 .6rem; }
.ward-head .incumbent strong { color: var(--ink); }

.factbar {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: .8rem 1rem;
  margin: .9rem 0 0;
  padding: 0;
}
.factbar div { min-width: 0; }
.factbar dt { font-size: .68rem; font-weight: 600; letter-spacing: .08em; text-transform: uppercase; color: var(--ink-3); }
.factbar dd { margin: 0; font-family: var(--serif); font-size: 1.25rem; font-weight: 700; font-variant-numeric: tabular-nums; }

.panel-actions {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem;
  margin-top: 1rem;
}
.panel-actions .btn, .panel-actions a.btn { display: inline-flex; align-items: center; text-decoration: none; }

.section { margin: 1.8rem 0 0; }
.section > h3 {
  border-bottom: 1px solid var(--ink);
  padding-bottom: .35rem;
  margin-bottom: .2rem;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: .6rem;
  flex-wrap: wrap;
}
.section > h3 .count { font-family: var(--sans); font-size: .72rem; font-weight: 600; letter-spacing: .08em; text-transform: uppercase; color: var(--ink-3); }
.section-note { font-size: .86rem; color: var(--ink-3); margin: .5rem 0 .9rem; max-width: var(--measure); }

/* A metric finding (pain or win) */
.finding {
  padding: 1rem 0;
  border-bottom: 1px solid var(--rule);
}
.finding-top {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: .3rem .8rem;
  align-items: start;
}
.finding-label {
  font-family: var(--serif);
  font-size: 1.08rem;
  font-weight: 700;
  margin: 0;
  min-width: 0;
}
.finding-chip { text-align: right; }
.finding-chip .label { display: block; margin-top: .2rem; font-size: .62rem; }

.finding-nums {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: .7rem 1rem;
  margin: .8rem 0 .6rem;
}
.finding-nums div { min-width: 0; }
.finding-nums dt { font-size: .66rem; font-weight: 600; letter-spacing: .08em; text-transform: uppercase; color: var(--ink-3); }
.finding-nums dd { margin: 0; font-family: var(--serif); font-size: 1.5rem; font-weight: 700; font-variant-numeric: tabular-nums; line-height: 1.1; }
.finding-nums dd .unit { font-family: var(--sans); font-size: .72rem; font-weight: 600; color: var(--ink-3); }
.finding-nums .hero dd { font-size: 1.9rem; }

.doorline {
  margin: .6rem 0 .5rem;
  padding: .7rem .85rem;
  background: var(--paper-2);
  border-left: 3px solid var(--accent);
  border-radius: 0 var(--radius) var(--radius) 0;
  font-family: var(--serif);
  font-size: 1rem;
  line-height: 1.45;
  max-width: var(--measure);
}
.doorline .label { display: block; margin-bottom: .25rem; }

.finding-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .4rem .8rem;
  font-size: .8rem;
  color: var(--ink-3);
}
.finding-foot a { color: var(--ink-2); }

.suppressed {
  font-size: .8rem;
  color: var(--ink-3);
  font-style: italic;
}

/* --------------------------------------------------------------------------
   10. Voting record — the contrast block
   -------------------------------------------------------------------------- */

.votes { margin-top: .4rem; }

.vote-group {
  border: 1px solid var(--rule-2);
  border-radius: var(--radius);
  padding: .85rem .9rem;
  margin: 0 0 .9rem;
  scroll-margin-top: 1rem;
}
.vote-group-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: .4rem .7rem;
  padding-bottom: .5rem;
  margin-bottom: .5rem;
  border-bottom: 1px solid var(--rule);
}
.vote-group-head h4 { margin: 0; font-family: var(--serif); font-size: 1.05rem; }
.vote-group-head .relates { font-size: .8rem; color: var(--ink-3); }

.vote-list { list-style: none; margin: 0; padding: 0; }
.vote-item {
  display: grid;
  grid-template-columns: 4.6rem 1fr;
  gap: .2rem .7rem;
  padding: .6rem 0;
  border-bottom: 1px dotted var(--rule);
}
.vote-item:last-child { border-bottom: 0; padding-bottom: 0; }

.vote-cast {
  grid-row: span 2;
  align-self: start;
  font-family: var(--sans);
  font-size: .78rem;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  text-align: center;
  padding: .2rem .3rem;
  border: 1px solid var(--ink);
  border-radius: var(--radius);
  color: var(--ink);
}
.vote-yes { background: var(--ink); color: var(--paper); border-color: var(--ink); }
.vote-no  { background: transparent; color: var(--ink); border-color: var(--ink); }
.vote-absent {
  background: repeating-linear-gradient(
    135deg, transparent 0 4px, var(--rule) 4px 5px);
  color: var(--ink-2);
  border-style: dashed;
  border-color: var(--rule-2);
  font-style: italic;
}

.vote-title { margin: 0; font-size: .95rem; line-height: 1.4; min-width: 0; }
.vote-meta {
  grid-column: 2;
  font-size: .78rem;
  color: var(--ink-3);
  display: flex;
  flex-wrap: wrap;
  gap: .1rem .6rem;
  font-variant-numeric: tabular-nums;
}
.vote-meta a { color: var(--ink-2); }
.vote-result { font-weight: 600; }

/* --------------------------------------------------------------------------
   11. Election block
   -------------------------------------------------------------------------- */

.election {
  border: 1px solid var(--rule-2);
  border-radius: var(--radius);
  padding: .9rem;
  margin-top: .5rem;
}
.election-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: .9rem 1rem;
  margin: 0;
}
.election-grid div { min-width: 0; }
.election-grid dt { font-size: .66rem; font-weight: 600; letter-spacing: .08em; text-transform: uppercase; color: var(--ink-3); }
.election-grid dd { margin: 0; font-family: var(--serif); font-size: 1.35rem; font-weight: 700; font-variant-numeric: tabular-nums; line-height: 1.15; }
.election-grid dd .who { display: block; font-family: var(--sans); font-size: .82rem; font-weight: 600; color: var(--ink-2); font-variant-numeric: normal; }

/* --------------------------------------------------------------------------
   12. Tables — scroll inside their own container, stack on mobile
   -------------------------------------------------------------------------- */

.table-scroll {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  max-width: 100%;
  margin: .5rem 0 1rem;
}

table.data {
  width: 100%;
  border-collapse: collapse;
  font-size: .92rem;
}
table.data caption {
  text-align: left;
  font-size: .82rem;
  color: var(--ink-3);
  padding-bottom: .5rem;
}
table.data th, table.data td {
  padding: .55rem .6rem;
  text-align: left;
  vertical-align: top;
  border-bottom: 1px solid var(--rule);
}
table.data thead th {
  border-bottom: 2px solid var(--ink);
  font-size: .68rem;
  font-weight: 700;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--ink-2);
  white-space: nowrap;
  vertical-align: bottom;
}
table.data thead th a {
  color: inherit;
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  gap: .25rem;
  min-height: 1.6rem;
}
table.data thead th a:hover { color: var(--accent); text-decoration: underline; }
/* The sort indicator is rendered by the sorth macro in base.html, which knows
   the active column and direction. A second indicator here printed "Ward ◇↕". */
table.data th.num, table.data td.num { text-align: right; }
table.data tbody tr:hover { background: var(--paper-2); }
table.data .t-head { font-family: var(--serif); font-weight: 700; font-size: 1rem; }
table.data .t-head a { text-decoration: none; color: var(--ink); }
table.data .t-head a:hover { color: var(--accent); text-decoration: underline; }
table.data .t-sub { display: block; font-family: var(--sans); font-size: .78rem; font-weight: 400; color: var(--ink-3); }

/* The honest number gets the emphasis. */
table.data td.excess, table.data th.excess { background: var(--paper-3); }
table.data td.excess { font-weight: 700; font-size: 1.02rem; }

.status {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  font-size: .82rem;
  font-weight: 600;
  white-space: nowrap;
}
.status::before {
  content: "";
  width: .55rem; height: .55rem;
  border-radius: 50%;
  background: var(--ink-3);
  flex: 0 0 auto;
}
.status-ok::before { background: var(--accent); }
.status-ok { color: var(--accent); }
.status-fail::before { background: var(--scale-7); }
.status-fail { color: var(--scale-7); }

/* Mobile: turn marked tables into stacked cards. */
@media (max-width: 719px) {
  .t-stack thead {
    position: absolute;
    width: 1px; height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
  }
  .t-stack, .t-stack tbody, .t-stack tr, .t-stack td { display: block; width: 100%; }
  .t-stack tr {
    border: 1px solid var(--rule-2);
    border-radius: var(--radius);
    padding: .7rem .8rem;
    margin: 0 0 .8rem;
  }
  .t-stack tr:hover { background: transparent; }
  .t-stack td {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    gap: .5rem 1rem;
    align-items: baseline;
    padding: .35rem 0;
    text-align: right;
    border-bottom: 1px dotted var(--rule);
  }
  .t-stack td:last-child { border-bottom: 0; padding-bottom: 0; }
  .t-stack td::before {
    content: attr(data-label);
    text-align: left;
    font-size: .68rem;
    font-weight: 600;
    letter-spacing: .07em;
    text-transform: uppercase;
    color: var(--ink-3);
  }
  .t-stack td.t-head {
    display: block;
    text-align: left;
    padding-top: 0;
    padding-bottom: .5rem;
    border-bottom: 1px solid var(--rule);
  }
  .t-stack td.t-head::before { content: none; }
  .t-stack td.excess { background: transparent; }
  .t-stack td.excess::before { color: var(--ink); }
}

/* --------------------------------------------------------------------------
   13. HTMX feedback
   -------------------------------------------------------------------------- */

.htmx-request.rank-row { background: var(--accent-bg); }
#ward-panel.htmx-request { opacity: .45; transition: opacity .12s linear; }
[aria-busy="true"] { cursor: progress; }

/* --------------------------------------------------------------------------
   14. Wider viewports
   -------------------------------------------------------------------------- */

@media (min-width: 560px) {
  h1 { font-size: 2rem; }
  .field { flex: 0 1 22rem; }
  select, .btn { width: auto; }
  .factbar { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .finding-nums { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .election-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

@media (min-width: 720px) {
  #map { height: 420px; }
  /* The poll map spans the full column rather than sitting beside a list, and
     at 1,445 polygons it needs the extra height to stay legible. */
  #pollmap, #demmap, #histmap, #crimemap { height: 520px; }
  .map-panel > summary { display: none; }      /* map is always shown here */
  .vote-item { grid-template-columns: 5rem 1fr auto; }
  .vote-meta { grid-column: 2 / -1; }
}

@media (min-width: 1000px) {
  h1 { font-size: 2.3rem; }

  /* Map beside the ranked list; list still reads first in the DOM. */
  .map-layout {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr);
    gap: 0 2.2rem;
    align-items: start;
  }
  .map-layout > .map-col { grid-column: 1; min-width: 0; position: sticky; top: 1rem; }
  .map-layout > .list-col { grid-column: 2; min-width: 0; }
  #map { height: 520px; }
  #pollmap, #demmap, #histmap, #crimemap { height: 620px; }

  /* Pains beside the voting record. */
  .panel-split {
    display: grid;
    grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
    gap: 0 2.2rem;
    align-items: start;
  }
  .panel-split > * { min-width: 0; }

  .factbar { grid-template-columns: repeat(5, minmax(0, 1fr)); }
}

/* --------------------------------------------------------------------------
   15. Dark mode — prefers-color-scheme only, no inline theming anywhere
   -------------------------------------------------------------------------- */

@media (prefers-color-scheme: dark) {
  :root {
    --paper:   #15130f;
    --paper-2: #1e1b16;
    --paper-3: #262219;
    --ink:     #f3ede2;
    --ink-2:   #c4bcb0;
    --ink-3:   #8e867a;
    --rule:    #332f28;
    --rule-2:  #4b453b;

    --accent:    #63c3ad;
    --accent-2:  #8ad7c5;
    --accent-bg: #1b2b28;

    /* Same diverging axis, lifted for a dark surface. */
    --scale-1: #6aa8e0;
    --scale-2: #4a7ba6;
    --scale-3: #395a70;
    --scale-4: #3a352c;
    --scale-5: #6f4436;
    --scale-6: #ad5c45;
    --scale-7: #e07257;

    --on-1: #0b1620;
    --on-2: #f3ede2;
    --on-3: #f3ede2;
    --on-4: #f3ede2;
    --on-5: #f3ede2;
    --on-6: #180e0a;
    --on-7: #1a0d08;

    --scale-na: #3a352c;
    --scale-stroke: rgba(243, 237, 226, 0.35);

    /* Status pills on the accounts screen, stepped for a dark surface. */
    /* Destructive-action colours, stepped for the dark surface. */
    --danger:      #e8897c;
    --danger-bg:   #351c18;
    --danger-line: #5e332c;

    --pill-on-bg:   #17303f;  --pill-on-ink:   #a8d2ee;  --pill-on-line:   #2b4f66;
    --pill-warn-bg: #3a2e14;  --pill-warn-ink: #e7c882;  --pill-warn-line: #5e4a1e;
    --pill-off-bg:  #2a2620;  --pill-off-ink:  #a8a099;  --pill-off-line:  #423b33;

    /* The sequential ramp is re-stepped for a dark surface rather than
       inverted: the anchor flips so the darkest surface still reads as the
       smallest magnitude, and every step is chosen against #15130f. Still one
       hue, still monotonic. */
    --dem-1: #12312b;
    --dem-2: #17453c;
    --dem-3: #1d5a4e;
    --dem-4: #2b7568;
    --dem-5: #4d9385;
    --dem-6: #78b0a2;
    --dem-7: #a3cabf;

    /* Re-stepped against the dark surface, anchor flipped, hue held. */
    --crime-1: #2e2113;
    --crime-2: #43301b;
    --crime-3: #5c4324;
    --crime-4: #7d5c30;
    --crime-5: #a37a40;
    --crime-6: #c49e63;
    --crime-7: #e2c08d;

    /* Re-stepped for the dark surface rather than reused: the light pair sits
       outside the dark lightness band. Separation 19.6 / 24.9, contrast passes. */
    --series-1: #3d97cc;
    --series-2: #d2783f;
  }

  .btn { background: var(--accent); color: #10201d; border-color: var(--accent); }
  .btn:hover { background: var(--accent-2); border-color: var(--accent-2); }
  .btn-quiet { background: transparent; color: var(--accent); border-color: var(--rule-2); }
  .vote-yes { background: var(--ink); color: var(--paper); }
  .leaflet-tooltip { background: #f3ede2; color: #15130f; }
  .status-fail, .status-fail::before { color: var(--scale-7); }
  .status-fail::before { background: var(--scale-7); }
}

/* --------------------------------------------------------------------------
   16. The printable one-pager (brief.html)
   Screen styles first, then the black-and-white A4 rules.
   -------------------------------------------------------------------------- */

.brief {
  max-width: 46rem;
  margin: 0 auto;
  padding: 1.25rem 1rem 3rem;
}

.brief-head {
  border-bottom: 2px solid var(--ink);
  padding-bottom: .6rem;
  margin-bottom: .8rem;
}
.brief-kicker {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: .2rem .8rem;
  font-size: .68rem;
  font-weight: 600;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin-bottom: .3rem;
}
.brief h1 { font-size: 1.7rem; margin: 0 0 .1rem; }
.brief-sub { font-size: .95rem; color: var(--ink-2); margin: 0 0 .5rem; }

.brief-facts {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: .5rem .8rem;
  margin: .5rem 0 0;
}
.brief-facts div { min-width: 0; }
.brief-facts dt { font-size: .62rem; font-weight: 600; letter-spacing: .08em; text-transform: uppercase; color: var(--ink-3); }
.brief-facts dd { margin: 0; font-family: var(--serif); font-size: 1.1rem; font-weight: 700; font-variant-numeric: tabular-nums; line-height: 1.2; }

.brief h2 {
  font-size: .82rem;
  font-family: var(--sans);
  font-weight: 700;
  letter-spacing: .1em;
  text-transform: uppercase;
  border-bottom: 1px solid var(--ink);
  padding-bottom: .2rem;
  margin: 1rem 0 .5rem;
}

.brief-item {
  padding: .4rem 0;
  border-bottom: 1px dotted var(--rule);
  break-inside: avoid;
  page-break-inside: avoid;
}
.brief-item:last-child { border-bottom: 0; }
.brief-item-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: .5rem;
}
.brief-item-head h3 { font-size: .98rem; margin: 0; min-width: 0; }
.brief-figs {
  font-size: .82rem;
  color: var(--ink-2);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.brief-item p { margin: .15rem 0 0; font-size: .88rem; line-height: 1.4; max-width: none; }
.brief-item .cite { color: var(--ink-3); font-size: .78rem; }

.brief-votes { margin: 0; padding: 0; list-style: none; }
.brief-vote {
  display: grid;
  grid-template-columns: 3.6rem 1fr;
  gap: .1rem .6rem;
  padding: .3rem 0;
  border-bottom: 1px dotted var(--rule);
  font-size: .86rem;
  break-inside: avoid;
  page-break-inside: avoid;
}
.brief-vote:last-child { border-bottom: 0; }
.brief-vote .vc {
  font-size: .68rem;
  font-weight: 700;
  letter-spacing: .05em;
  text-transform: uppercase;
  border: 1px solid var(--ink);
  border-radius: 2px;
  text-align: center;
  padding: .05rem .2rem;
  align-self: start;
}
.brief-vote .vc.vote-absent { border-style: dashed; font-style: italic; background: none; }
.brief-vote .vm { grid-column: 2; font-size: .74rem; color: var(--ink-3); font-variant-numeric: tabular-nums; }
.brief-tag {
  font-size: .68rem;
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin: .5rem 0 .1rem;
}

.brief-sources { margin: 0; padding-left: 1.2rem; font-size: .72rem; color: var(--ink-2); }
.brief-sources li { margin-bottom: .15rem; }
.brief-sources a { color: inherit; word-break: break-word; }

.brief-foot {
  margin-top: .9rem;
  padding-top: .5rem;
  border-top: 1px solid var(--ink);
  font-size: .7rem;
  color: var(--ink-2);
}
.brief-foot p { margin: 0 0 .15em; max-width: none; }

@media (min-width: 560px) {
  .brief-facts { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

/* ---- Black-and-white A4 ---- */
@media print {
  @page {
    size: A4 portrait;
    margin: 12mm 13mm;
  }

  html, body, .brief {
    background: #fff !important;
    color: #000 !important;
  }

  body { font-size: 8.9pt; line-height: 1.22; }

  /* No backgrounds, no colour, no chrome. */
  *, *::before, *::after {
    background: transparent !important;
    background-image: none !important;
    box-shadow: none !important;
    color: #000 !important;
    text-shadow: none !important;
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
  }

  a, a:visited { color: #000 !important; text-decoration: none; }

  /* Things that never belong on paper. */
  .masthead, .nav, .site-footer, .controls, .map-panel, #map, #pollmap, #demmap, #histmap, #crimemap,
  .legend, .panel-actions, .skip, .no-print { display: none !important; }

  .brief { max-width: none; margin: 0; padding: 0; }

  .brief-head { border-bottom: 1.2pt solid #000 !important; padding-bottom: 3pt; margin-bottom: 4pt; }
  .brief h1 { font-size: 15pt; }
  .brief-sub { font-size: 9.5pt; }
  .brief-kicker { font-size: 7pt; }
  .brief-facts { grid-template-columns: repeat(4, 1fr); gap: 3pt 6pt; margin-top: 3pt; }
  .brief-facts dt { font-size: 6.2pt; }
  .brief-facts dd { font-size: 10pt; }

  .brief h2 {
    font-size: 8pt;
    border-bottom: 0.7pt solid #000 !important;
    margin: 4pt 0 2pt;
    padding-bottom: 1.5pt;
    break-after: avoid;
    page-break-after: avoid;
  }

  .brief-item { padding: 1.2pt 0; border-bottom: 0.4pt dotted #666 !important; }
  .brief-item-head h3 { font-size: 9.4pt; }
  .brief-figs { font-size: 8pt; }
  .brief-item p { font-size: 8.3pt; line-height: 1.22; }
  .brief-item .cite { font-size: 7.2pt; }

  .brief-vote { font-size: 8.1pt; padding: 0.8pt 0; border-bottom: 0.4pt dotted #666 !important; }
  .brief-vote .vc { border: 0.6pt solid #000 !important; font-size: 6.4pt; }
  .brief-vote .vc.vote-absent { border-style: dashed !important; }
  .brief-vote .vm { font-size: 7pt; }
  .brief-tag { font-size: 6.6pt; margin: 2pt 0 0; }

  /* All sources must be visible on paper — never truncated, never collapsed.
     Set in two columns: a long register still fits under the fold. */
  .brief-sources {
    font-size: 6.5pt;
    line-height: 1.18;
    display: block !important;
    columns: 2;
    column-gap: 10pt;
    padding-left: 9pt;
  }
  .brief-sources li { break-inside: avoid; page-break-inside: avoid; }
  details, details > * { display: block !important; }
  details > summary { display: none !important; }

  .brief-foot { font-size: 6.6pt; border-top: 0.7pt solid #000 !important; margin-top: 3.5pt; padding-top: 2.5pt; }

  /* Keep individual ITEMS whole. Sections and lists must stay breakable —
     making them atomic pushes a whole section to page 2 and wastes the sheet. */
  h1, h2, h3, h4 { break-after: avoid; page-break-after: avoid; }
  .brief-item, .brief-vote, .brief-facts, li, tr {
    break-inside: avoid;
    page-break-inside: avoid;
  }
  .brief-section, .brief-votes, .brief-sources {
    break-inside: auto;
    page-break-inside: auto;
  }
  .brief-section { margin-bottom: 0; }
}

/* Methodology warning: a claim that needs a caveat before it is repeated. */
.section-note.warn {
  border-left: 3px solid var(--scale-6, #d97a5f);
  padding-left: .75rem;
  color: var(--ink, #1a1714);
}

/* ---- Sortable table headers -------------------------------------------- */
.data th .sortlink {
  display: inline-flex;
  align-items: baseline;
  gap: .35em;
  color: inherit;
  text-decoration: none;
  border-bottom: 1px dotted currentColor;
}
.data th .sortlink:hover { border-bottom-style: solid; }
.data th .sortlink.is-active { font-weight: 700; border-bottom-style: solid; }
.data th .sortarrow { font-size: .8em; opacity: .35; }
.data th .sortlink.is-active .sortarrow { opacity: 1; }
.data th[aria-sort] { background: var(--tint, rgba(0,0,0,.03)); }

/* Two-up section grid used on the overview. Collapses on narrow screens. */
.grid-2 { display: grid; gap: 1.5rem; }
@media (min-width: 900px) { .grid-2 { grid-template-columns: 1fr 1fr; } }
.statband dd { font-size: 1.65rem; line-height: 1.15; }
.formula {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: .86rem; line-height: 1.7;
  padding: .8rem 1rem; overflow-x: auto;
  border-left: 3px solid var(--accent, #14594e);
  background: var(--tint, rgba(0,0,0,.03));
}

/* ---- Column definitions ------------------------------------------------
   One collapsed strip above each table. Closed by default so the table stays
   the first thing on screen; opened once, it names every column, its unit and
   its source. Forced open when printed, because a printed table has no
   affordance to click. */
.fielddefs {
  border: 1px solid var(--rule, rgba(0,0,0,.12));
  border-radius: 3px;
  margin: 0 0 .9rem;
  background: var(--paper-2, rgba(0,0,0,.02));
}
.fielddefs > summary {
  cursor: pointer;
  padding: .5rem .75rem;
  font-size: .72rem;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--ink-2);
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  align-items: baseline;
  min-height: 2.75rem;          /* touch target */
  align-content: center;
  flex-wrap: wrap;
}
.fielddefs > summary:hover { color: var(--accent); }
.fielddefs > summary .count {
  font-weight: 600;
  letter-spacing: .08em;
  color: var(--ink-3);
  text-transform: none;
}
.fielddefs > dl {
  margin: 0;
  padding: 0 .75rem .75rem;
  border-top: 1px solid var(--rule, rgba(0,0,0,.12));
  display: grid;
  gap: .55rem 1.25rem;
}
.fielddefs > dl > div { padding-top: .55rem; }
.fielddefs dt {
  font-size: .78rem;
  font-weight: 700;
  letter-spacing: .04em;
  color: var(--ink);
  margin: 0 0 .15rem;
}
.fielddefs dd {
  margin: 0;
  font-size: .85rem;
  line-height: 1.5;
  color: var(--ink-2);
  max-width: var(--measure);
}
@media (min-width: 760px) {
  .fielddefs > dl { grid-template-columns: 1fr 1fr; }
}
@media print {
  .fielddefs > summary { display: none; }
  .fielddefs > dl { display: block !important; border-top: 0; }
}


/* --------------------------------------------------------------------------
   Charts (overview). Inline SVG, no library. Marks are thin, the grid is
   recessive, and every mark carries a <title> so the browser shows a tooltip
   with no JavaScript.
   -------------------------------------------------------------------------- */

.chart {
  margin: .4rem 0 .6rem;
  width: 100%;
  overflow-x: auto;              /* narrow phones scroll the chart, not the page */
}
.chart svg { display: block; width: 100%; height: auto; min-width: 320px; }

.chart .grid line { stroke: var(--rule); stroke-width: 1; }
.chart .axis-y text,
.chart .axis-x text {
  font-family: var(--sans);
  font-size: 11px;
  fill: var(--ink-3);
}
.chart .baseline { stroke: var(--rule-2); stroke-width: 1; }

.chart .series-line { fill: none; stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
/* 8px marker: the spec floor, and a poll is a discrete observation that should
   read as a point rather than as a bend in a line. */
.chart .series-dot { r: 4; stroke: var(--paper); stroke-width: 2; }
.chart .series-end {
  font-family: var(--sans);
  font-size: 12px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.chart .bar { rx: 4; }           /* 4px rounded data-end, anchored to baseline */
.chart .bar-label,
.chart .bar-value {
  font-family: var(--sans);
  font-size: 11.5px;
  fill: var(--ink-2);
  font-variant-numeric: tabular-nums;
}
.chart .bar-value { font-weight: 700; }

.chart-legend {
  display: flex;
  flex-wrap: wrap;
  gap: .25rem 1rem;
  margin: 0 0 .3rem;
  padding: 0;
  list-style: none;
  font-size: .82rem;
  color: var(--ink-2);
}
.chart-legend li { display: flex; align-items: center; gap: .35rem; }
.chart-legend .swatch {
  width: 12px; height: 12px; border-radius: 2px; flex: none;
}

@media print { .chart { break-inside: avoid; } }


/* --------------------------------------------------------------------------
   Grouped nav dropdowns.
   Fifteen links will not fit one row at 1160px, so the secondary pages sit in
   two <details> groups. Nested details rather than a JS menu: the navigation
   has to work with scripts disabled, and summary already gets keyboard focus
   and toggling for free.
   -------------------------------------------------------------------------- */

/* The group carries the current-page marker so the closed row still says
   where you are. */

@media (min-width: 720px) {
  .navgroup { position: relative; }
}

@media (max-width: 719px) {
  /* Inside the phone menu the groups are just labelled sections — a dropdown
     inside a dropdown is not navigation. */
}

/* --------------------------------------------------------------------------
   Masthead + navigation
   --------------------------------------------------------------------------
   Phone: brand left, menu button hard right on the same row, and the link list
   drops as an absolutely positioned panel under the whole header.
   Desktop: the button is hidden and the list becomes a static bar below the
   brand.

   The brand is NOT inside the <details>: a closed <details> hides every child
   except its <summary>, which blanked the masthead on mobile.

   Plain links and dropdown summaries both read --nav-h so their underlines
   share a baseline; when they did not, "Ward data" underlined below the rule.
   -------------------------------------------------------------------------- */

.masthead-inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  position: relative;              /* anchor for the phone dropdown panel */
  padding: 0 1rem;
}

.brandline {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: .1rem .8rem;
  padding: .8rem 0 .6rem;
}

/* The masthead follows the page down on every screen size. These pages are
   long tables; without this, changing view meant scrolling back to the top
   first. `top: 0` plus a z-index above the map panes keeps the dropdowns and
   the Leaflet controls from fighting. */
/* The masthead is sticky, so an in-page anchor would otherwise land UNDER it
   and the reader would see the middle of the section they jumped to. This
   reserves the header's height on every anchor jump, which fixes the Child
   care / Bike Share links on /services and every other #anchor on the site. */
html { scroll-padding-top: 7rem; }
@media (max-width: 719px) { html { scroll-padding-top: 4.75rem; } }

.masthead {
  position: sticky;
  top: 0;
  z-index: 80;
  background: var(--paper);
}

.navbox { margin-left: auto; }

.navbox > summary {
  display: flex;
  align-items: center;
  min-height: var(--tap);
  padding: 0 0 0 .6rem;
  font-family: var(--sans);
  font-size: .8rem;
  font-weight: 700;
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--ink-2);
  cursor: pointer;
  list-style: none;
  white-space: nowrap;
}
.navbox > summary::-webkit-details-marker { display: none; }
.navbox > summary:hover { color: var(--ink); }
/* Label carries its own spacing — see the gap note above. */
.navbox > summary > span { margin-right: .55rem; }

.navbox > summary::before {
  content: "";
  order: 2;
  width: 1.15rem; height: 2px;
  background: currentColor;
  box-shadow: 0 6px 0 currentColor, 0 -6px 0 currentColor;
  flex: none;
}
.navbox[open] > summary::before { box-shadow: none; transform: rotate(45deg); }
.navbox[open] > summary::after {
  content: "";
  order: 3;
  width: 1.15rem; height: 2px;
  background: currentColor;
  transform: rotate(-45deg);
  margin-left: -1.15rem;
  flex: none;
}

.navlist ul { display: flex; list-style: none; margin: 0; padding: 0; }

.navlist a {
  display: flex;
  align-items: center;
  min-height: var(--nav-h);
  padding: 0 .7rem;
  color: var(--ink-2);
  text-decoration: none;
  font-size: .92rem;
  font-weight: 600;
  white-space: nowrap;
  border-bottom: 2px solid transparent;
}
.navlist a:hover { color: var(--ink); }
.navlist a[aria-current="page"] { color: var(--ink); border-bottom-color: var(--accent); }

/* --------------------------------------------------------------------------
   Grouped navigation.

   The five groups are the five permission groups in app/auth.py, same names,
   same order, so the header and the Accounts screen can be read against each
   other. A group with nothing granted in it is not rendered at all (see
   NAV_GROUPS in app/main.py), so these rules never style an empty one.

   Each group is a <details>: on a wide screen its summary is a bar item and
   the panel drops beneath it; on a phone the panel is forced open and the
   whole thing reads as a labelled list, because a dropdown inside the
   hamburger would put every link two taps away.
   -------------------------------------------------------------------------- */

.navdrop > summary {
  display: flex;
  align-items: center;
  gap: .35rem;
  min-height: var(--nav-h);
  padding: 0 .85rem;
  color: var(--ink-2);
  font-size: .92rem;
  font-weight: 600;
  white-space: nowrap;
  cursor: pointer;
  list-style: none;
  border-bottom: 2px solid transparent;
}
.navdrop > summary::-webkit-details-marker { display: none; }
.navdrop > summary::after {
  content: "";
  width: 0; height: 0;
  border-left: 4px solid transparent;
  border-right: 4px solid transparent;
  border-top: 4px solid currentColor;
  opacity: .65;
}
.navdrop > summary:hover { color: var(--ink); }
/* The group holding the current page keeps the underline while shut, so the
   bar still says where you are with every panel closed. */
.navdrop[data-current="true"] > summary { color: var(--ink); border-bottom-color: var(--accent); }
.navdrop-panel { list-style: none; margin: 0; padding: 0; }


/* ---- Phone: the list is a dropdown panel under the header ---- */
@media (max-width: 719px) {
  /* One row: brand stacked on the left, button hard right. Left to wrap, the
     subtitle filled the row and pushed the button onto a second line. */
  .masthead-inner { flex-wrap: nowrap; align-items: center; }
  .brandline {
    flex-direction: column;
    align-items: flex-start;
    gap: 0;
    padding: .6rem 0;
  }
  .brandline .masthead-note { font-size: .64rem; letter-spacing: .06em; }
  .brandline .wordmark { font-size: .98rem; white-space: nowrap; }

  .navbox[open] > .navlist {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    z-index: 70;
    background: var(--paper);
    border-top: 1px solid var(--rule);
    border-bottom: 1px solid var(--rule-2);
    box-shadow: 0 10px 24px rgba(20, 18, 15, .16);
    padding: .35rem 0 .6rem;
    max-height: 78vh;
    overflow-y: auto;
  }
  .navlist ul { flex-direction: column; }
  .navlist a {
    border-bottom: 0;
    border-left: 2px solid transparent;
    padding-left: 1.4rem;
  }
  /* Each group is a labelled block; the heading sits above its own links and
     a rule separates it from the group before, so the drop-down reads as five
     sections rather than one long undifferentiated list. */
  .navdrop { border-top: 1px solid var(--rule); margin-top: .3rem; }
  /* Both rules are needed: older engines hide a closed <details> with
     display:none, Chromium 127+ uses content-visibility on ::details-content.
     Shipping only one left the phone menu empty. */
  .navdrop::details-content { content-visibility: visible !important; }
  .navdrop > .navdrop-panel { display: block !important; }
  .navdrop > summary {
    min-height: 2.2rem;
    padding-left: 1rem;
    font-size: .7rem;
    letter-spacing: .09em;
    text-transform: uppercase;
    color: var(--ink-3);
    border-bottom: 0;
    pointer-events: none;      /* it is a heading here, not a control */
  }
  .navdrop > summary::after { display: none; }
  .navdrop[data-current="true"] > summary { color: var(--ink-2); }
  .navdrop > .navdrop-panel a { padding-left: 1.4rem; }
  .navlist a[aria-current="page"] {
    border-left-color: var(--accent);
    background: var(--accent-bg);
  }
}

@media (min-width: 720px) {
  /* Forcing a closed <details> open needs BOTH rules: older engines hide the
     children with display:none, Chromium 127+ uses content-visibility on
     ::details-content. Shipping only one made the whole nav vanish. */
  .navbox::details-content { content-visibility: visible !important; }
  .navbox > summary { display: none; }
  .navbox { flex: 0 0 100%; margin-left: 0; }
  .navlist {
    display: block !important;
    border-top: 1px solid var(--rule);
    margin: 0 -1rem;
    padding: 0 1rem;
  }
  /* All 16 links on ONE row. The bar is allowed to run wider than the 1160px
     text measure, because a navigation is a control strip and does not have to
     obey the reading width, and the type is stepped down so the set fits.
     It still WRAPS rather than truncating if a window is too narrow for the
     full set: a second row is acceptable, a hidden link is not. */
  /* A single row of group labels. Nothing wraps to a second line now, because
     five labels fit where nineteen links did not. */
  .navlist {
    display: flex !important;
    flex-wrap: wrap;
    align-items: stretch;
    /* Right-aligned: the wordmark anchors the left of the masthead, so the
       controls sit at the opposite end rather than colliding with it. */
    justify-content: flex-end;
  }
  .navgroup { position: relative; }
  .navgroup > .navdrop { display: flex; flex-direction: column; height: 100%; }

  .navdrop[open] > .navdrop-panel {
    position: absolute;
    top: 100%;
    left: 0;
    z-index: 60;
    min-width: 12.5rem;
    background: var(--paper);
    border: 1px solid var(--rule);
    border-radius: var(--radius);
    box-shadow: 0 8px 24px rgba(20, 18, 15, .14);
    padding: .25rem 0;
  }
  .navdrop[open] > .navdrop-panel a {
    border-bottom: 0;
    min-height: 2.3rem;
    padding: 0 1rem;
    font-size: .875rem;
  }
  .navdrop[open] > .navdrop-panel a:hover { background: var(--paper-2); }
  .navdrop[open] > .navdrop-panel a[aria-current="page"] {
    color: var(--ink); background: var(--accent-bg);
  }
  /* Panels hang from the right edge of their label now that the bar is
     right-aligned, so none of them can run off the side of the window. */
  .navdrop[open] > .navdrop-panel { left: auto; right: 0; }
  .navgroup:first-child .navdrop[open] > .navdrop-panel { left: auto; right: 0; }
}


@media (min-width: 1100px) {
  /* Let the masthead row exceed the 1160px reading measure so the full nav
     fits on one line. Content keeps the narrower measure; only this strip
     widens. */
  .masthead .wrap.masthead-inner { max-width: 1400px; }
  .navlist a { padding: 0 .62rem; font-size: .875rem; }
}

/* --------------------------------------------------------------------------
   Sign-in page. Intentionally anonymous: no wordmark, no description, no
   navigation. The domain is not advertising what it is for.
   -------------------------------------------------------------------------- */

.signin-page { background: var(--paper-2); }

.signin {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1.5rem;
}

.signin-card {
  width: 100%;
  max-width: 22rem;
  background: var(--paper);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: 1.6rem 1.5rem 1.8rem;
  box-shadow: 0 10px 30px rgba(20, 18, 15, .08);
}

.signin-card h1 {
  font-size: 1.25rem;
  margin: 0 0 1.1rem;
}

.signin-card label {
  display: block;
  font-family: var(--sans);
  font-size: .72rem;
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin: .9rem 0 .3rem;
}

.signin-card input[type="email"],
.signin-card input[type="password"] {
  width: 100%;
  min-height: var(--tap);
  padding: 0 .7rem;
  font-family: var(--sans);
  font-size: 1rem;
  color: var(--ink);
  background: var(--paper);
  border: 1px solid var(--rule-2);
  border-radius: var(--radius);
}
.signin-card input:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

.signin-card .btn {
  width: 100%;
  margin-top: 1.3rem;
  min-height: var(--tap);
  justify-content: center;
}

.signin-error {
  margin: 0 0 .2rem;
  padding: .6rem .7rem;
  font-size: .88rem;
  color: var(--ink);
  background: var(--accent-bg);
  border-left: 3px solid var(--scale-7);
  border-radius: var(--radius);
}

/* The signed-in identity strip in the masthead. */
.whoami {
  display: flex;
  align-items: center;
  gap: .6rem;
  font-size: .74rem;
  color: var(--ink-3);
  white-space: nowrap;
}
.whoami a { color: var(--ink-2); }
.signin-hint {
  margin: .8rem 0 0;
  font-size: .8rem;
  color: var(--ink-3);
}

/* Account administration: the permission grid. */
.grantgrid {
  display: grid;
  grid-template-columns: 1fr;
  gap: .1rem .9rem;
  margin: .5rem 0 .9rem;
}
@media (min-width: 560px)  { .grantgrid { grid-template-columns: 1fr 1fr; } }
@media (min-width: 1000px) { .grantgrid { grid-template-columns: repeat(3, 1fr); } }

.grant {
  display: flex;
  align-items: center;
  gap: .5rem;
  min-height: 2rem;
  font-size: .88rem;
  color: var(--ink-2);
  cursor: pointer;
}
.grant input { width: 16px; height: 16px; flex: none; accent-color: var(--accent); }

.rowactions { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: .6rem; }


/* Chart legend swatches. These carry a class rather than an inline style
   attribute so the Content-Security-Policy can forbid inline styles outright:
   style-src 'self' with no 'unsafe-inline'. */
.chart-legend .swatch-1 { background: var(--series-1, #1668a8); }
.chart-legend .swatch-2 { background: var(--series-2, #b85417); }

.useraccount { border-top: 1px solid var(--rule); padding-top: 1rem; }

/* --------------------------------------------------------------------------
   Account administration: the grouped permission editor.

   Twenty grants is too many to read as a flat list, so they are grouped by
   subject and each carries a one-line description of what it opens. The
   description is the point: "Canvass" alone does not tell you the box hands
   over addresses of social housing blocks.

   Mobile first. One column of grants below 620px with the help text stacked
   under its label, two above that, three on a wide screen.
   -------------------------------------------------------------------------- */

.grantbox > summary { font-weight: 600; }
.grantpanel { margin-top: .8rem; }

.presetbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .4rem;
  padding-bottom: .9rem;
  margin-bottom: .3rem;
  border-bottom: 1px solid var(--rule);
}
.presetbar .label { margin: 0 .3rem 0 0; }

.btn-tiny {
  font-size: .74rem;
  padding: .22rem .55rem;
  line-height: 1.3;
}

.grantgroup {
  border: 1px solid var(--rule);
  border-radius: 6px;
  padding: .2rem .9rem .7rem;
  margin: .9rem 0 0;
  min-width: 0;          /* a fieldset defaults to min-content and will not
                            shrink inside a grid, which overflows on a phone */
}
.grantgroup legend {
  display: flex;
  align-items: center;
  gap: .6rem;
  padding: 0 .4rem;
  font-family: var(--sans);
  font-size: .78rem;
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--ink-3);
}

/* Overrides the two- and three-column rules above: with help text under every
   label, three columns is unreadable until the screen is genuinely wide. */
.grantgroup .grantgrid {
  grid-template-columns: 1fr;
  gap: .15rem 1.4rem;
  margin: .3rem 0 0;
}
@media (min-width: 620px)  { .grantgroup .grantgrid { grid-template-columns: 1fr 1fr; } }
@media (min-width: 1080px) { .grantgroup .grantgrid { grid-template-columns: repeat(3, 1fr); } }

/* The grant row itself: checkbox top-aligned against two stacked lines. */
.grantgroup .grant {
  align-items: flex-start;
  min-height: 0;
  padding: .45rem 0;
  gap: .6rem;
}
.grantgroup .grant input { margin-top: .15rem; }

.grant-text { display: flex; flex-direction: column; gap: .1rem; min-width: 0; }
.grant-label { font-weight: 600; color: var(--ink); }
.grant-help {
  font-size: .78rem;
  line-height: 1.4;
  color: var(--ink-3);
  text-wrap: pretty;
}

/* The read-only summary of what an account currently holds. */
.grantsummary {
  display: flex;
  flex-wrap: wrap;
  gap: .35rem;
  margin: .6rem 0 .9rem;
}

.pill {
  display: inline-block;
  padding: .16rem .5rem;
  border-radius: 999px;
  border: 1px solid var(--rule);
  font-family: var(--sans);
  font-size: .74rem;
  line-height: 1.5;
  white-space: nowrap;
}
.pill-on   { background: var(--pill-on-bg,   #e8f1f8); color: var(--pill-on-ink,   #14496f); border-color: var(--pill-on-line,   #bcd6e8); }
.pill-warn { background: var(--pill-warn-bg, #fbf0dd); color: var(--pill-warn-ink, #6b4708); border-color: var(--pill-warn-line, #e6cf9f); }
.pill-off  { background: var(--pill-off-bg,  #f0eeec); color: var(--pill-off-ink,  #5c5854); border-color: var(--pill-off-line,  #d8d3ce); }


/* --------------------------------------------------------------------------
   Footer. One definition in base.html; the printable brief mirrors its wording
   in print styles so the two do not drift.
   -------------------------------------------------------------------------- */

/* The footer is a row of links and one paragraph now; the prose it used to
   carry lives on /about. Mobile first: links wrap and stay 44px-tappable. */
.footer-links {
  display: flex;
  flex-wrap: wrap;
  gap: .2rem 1.4rem;
  padding-top: .4rem;
}
.footer-links a {
  display: inline-block;
  padding: .5rem 0;
  font-family: var(--sans);
  font-size: .82rem;
  font-weight: 600;
}

.footer-note {
  max-width: 68ch;
  margin: .3rem 0 .9rem;
  font-size: .82rem;
  line-height: 1.55;
  color: var(--ink-3);
  text-wrap: pretty;
}

.footer-base {
  display: flex;
  flex-wrap: wrap;
  gap: .3rem 1.2rem;
  margin: 1.1rem 0 0;
  padding-top: .8rem;
  border-top: 1px solid var(--rule);
  font-size: .76rem;
  color: var(--ink-3);
  max-width: none;
}

/* --------------------------------------------------------------------------
   Ward brief additions: the poll table, the one-line facts and the pressure
   rows. Kept deliberately plain — this sheet is printed in black and white and
   carried to a door, so nothing here depends on colour to be readable.
   -------------------------------------------------------------------------- */

.brief-table {
  width: 100%;
  border-collapse: collapse;
  margin: .35rem 0 .5rem;
  font-size: .82rem;
}
.brief-table th,
.brief-table td {
  padding: .28rem .5rem .28rem 0;
  text-align: left;
  vertical-align: baseline;
  border-bottom: 1px solid var(--rule);
}
.brief-table th {
  font-family: var(--sans);
  font-size: .68rem;
  font-weight: 700;
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--ink-3);
}
.brief-table td.num, .brief-table th.num { text-align: right; font-variant-numeric: tabular-nums; }
.brief-table td.cite { font-size: .76rem; color: var(--ink-3); }
.brief-table tr:last-child td { border-bottom: 0; }

.brief-line {
  margin: .25rem 0;
  font-size: .86rem;
  line-height: 1.5;
  text-wrap: pretty;
}

/* --------------------------------------------------------------------------
   Back to top. Bottom right, clear of the content column, revealed only once
   there is something to scroll back from.
   -------------------------------------------------------------------------- */

.totop {
  position: fixed;
  right: max(1rem, env(safe-area-inset-right));
  bottom: max(1rem, env(safe-area-inset-bottom));
  z-index: 75;
  display: flex;
  align-items: center;
  gap: .35rem;
  /* 44px minimum: this sits under a thumb on a phone. */
  min-height: 44px;
  padding: 0 .85rem;
  border: 1px solid var(--rule-2);
  border-radius: 999px;
  background: var(--paper);
  color: var(--ink-2);
  font-family: var(--sans);
  font-size: .8rem;
  font-weight: 600;
  cursor: pointer;
  box-shadow: 0 4px 14px rgba(20, 18, 15, .16);
}
.totop:hover { color: var(--ink); border-color: var(--ink-3); }
.totop:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.totop[hidden] { display: none; }

/* On a narrow screen the label is dropped to a bare arrow so the button does
   not sit over a table's right-hand column. */
@media (max-width: 480px) {
  .totop { padding: 0; width: 44px; justify-content: center; }
  .totop span { display: none; }
}

@media print { .totop { display: none !important; } }

/* --------------------------------------------------------------------------
   Accounts screen: two columns.

   Identity, status and the destructive controls on the left; the permission
   grid on the right. Stacked in one column the buttons ended up below a
   nineteen-checkbox grid, which is where nobody looks.
   -------------------------------------------------------------------------- */

.acct {
  display: grid;
  grid-template-columns: 1fr;      /* phone: one column, side block first */
  gap: 1.1rem 2rem;
  align-items: start;
}
@media (min-width: 900px) {
  .acct { grid-template-columns: minmax(15rem, 19rem) 1fr; }
  /* The permission grid can be tall; the left column stays put beside it. */
  .acct-side { position: sticky; top: calc(var(--nav-h) + 3.2rem); }
}

.acct-side > h3 {
  margin: 0 0 .15rem;
  border: 0;
  padding: 0;
}
.acct-email {
  margin: 0 0 .5rem;
  font-size: .82rem;
  color: var(--ink-3);
  word-break: break-word;
}

.acct-pills { display: flex; flex-wrap: wrap; gap: .3rem; margin: 0 0 .7rem; }

.acct-meta { margin: 0 0 .8rem; }
.acct-meta > div {
  display: flex;
  justify-content: space-between;
  gap: .8rem;
  padding: .28rem 0;
  border-bottom: 1px solid var(--rule);
  font-size: .82rem;
}
.acct-meta dt { color: var(--ink-3); }
.acct-meta dd { margin: 0; text-align: right; }

.acct-main > .grantsummary:first-child { margin-top: 0; }

/* The delete control. Folded away, and visually separated from the reversible
   buttons above it. */
.acct-danger {
  margin-top: .9rem;
  padding-top: .7rem;
  border-top: 1px solid var(--rule);
}
.acct-danger > summary {
  cursor: pointer;
  font-family: var(--sans);
  font-size: .8rem;
  font-weight: 600;
  color: var(--ink-3);
}
.acct-danger > summary:hover { color: var(--danger, #a3352a); }
.acct-danger .field { margin: .5rem 0 .6rem; }

.btn-danger {
  border-color: var(--danger-line, #d8a49c);
  color: var(--danger, #a3352a);
}
.btn-danger:hover {
  background: var(--danger-bg, #fbeeec);
  border-color: var(--danger, #a3352a);
}

/* The current page named beside its group in the closed bar: "Wards / Ward
   profiles". Separator is drawn in CSS so it never lands in copied text. */
.navdrop-here {
  color: var(--ink-3);
  font-weight: 600;
}
.navdrop-here::before {
  content: "/";
  margin: 0 .3rem;
  color: var(--rule-2);
}
.navdrop[data-current="true"] > summary .navdrop-here { color: var(--ink-2); }

/* On a phone the group is a heading above its links, so repeating the page
   name there is noise — the link itself is already marked current. */
@media (max-width: 719px) {
  .navdrop-here { display: none; }
}

/* --------------------------------------------------------------------------
   Masthead on a phone.

   Name + Accounts + Password + Sign out + the menu button do not fit on one
   360px line, so below 720px the account links move INSIDE the menu panel and
   the masthead copy is hidden. Above 720px the reverse.
   -------------------------------------------------------------------------- */

.navaccount { display: none; }

@media (max-width: 719px) {
  .whoami { display: none; }
  .navaccount { display: block; }
  .navaccount-who {
    display: block;
    padding: .45rem 1.4rem .1rem;
    font-size: .82rem;
    font-weight: 600;
    color: var(--ink-2);
  }
}

/* --------------------------------------------------------------------------
   Tabs. Server-rendered links, not a widget: the page reloads on every save,
   so a tab is just which view the next render shows — which also makes each
   one bookmarkable and keeps the back button working.
   -------------------------------------------------------------------------- */

.tabs {
  display: flex;
  flex-wrap: wrap;
  gap: .15rem;
  margin: 0 0 1.1rem;
  border-bottom: 1px solid var(--rule);
}
.tab {
  display: flex;
  align-items: center;
  gap: .4rem;
  min-height: 44px;                /* thumb-sized on a phone */
  padding: 0 .9rem;
  border: 1px solid transparent;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  color: var(--ink-3);
  text-decoration: none;
  font-family: var(--sans);
  font-size: .88rem;
  font-weight: 600;
}
.tab:hover { color: var(--ink); }
.tab.is-active {
  color: var(--ink);
  border-bottom-color: var(--accent);
}
.tab .count {
  font-size: .72rem;
  font-weight: 600;
  color: var(--ink-3);
  background: var(--paper-2);
  border-radius: 999px;
  padding: .1rem .45rem;
}
