/* /elections/map/: layout, toolbar, legend, district fills, panel, tooltip.
   Fills come from the party tokens so the map matches every badge on the
   site and follows dark mode. Leading and vacant use SVG patterns rather
   than opacity so they survive greyscale and colour-blind viewing. */
.map-intro { max-width: 70ch; color: var(--ink-600); margin: 0 0 var(--sp-2); }
/* The status line is the text that changes on election night: body ink,
   not meta grey. */
.map-status { margin: 0 0 var(--sp-3); font-size: var(--fs-sm); color: var(--ink-700); }
.map-status .live-dot { margin-right: 6px; }
.map-feed-alert { margin: 0 0 var(--sp-3); }
/* Nine states vote on new lines in 2026 (election_2026.REDRAWN_2026). */
.map-redrawn { margin: 0 0 var(--sp-3); padding-left: var(--sp-3); border-left: 3px solid var(--civic-amber); }
.map-redrawn-note { margin: var(--sp-2) 0 0; }
.map-sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }

.map-toolbar { display: flex; flex-wrap: wrap; gap: var(--sp-3) var(--sp-4); align-items: flex-end; margin: 0 0 var(--sp-3); }
.map-zip label, .map-jump-form label { display: block; font-size: var(--fs-sm); color: var(--ink-600); margin-bottom: 4px; }
.map-zip-row { display: flex; gap: var(--sp-2); }
.map-zip-row input { width: 9.5em; min-height: 44px; }
.map-zip-row .btn { min-height: 44px; }
.map-zip-error { margin: 4px 0 0; color: var(--danger); }
.map-modes { display: flex; gap: var(--sp-2); }
.map-modes .btn { min-height: 44px; }
.map-modes .btn.is-active { background: var(--ink-900); color: var(--ink-50); border-color: var(--ink-900); }
.map-modes .btn.is-inert { opacity: 0.55; cursor: not-allowed; }
.map-modes .btn .live-dot { margin-left: 6px; }
.map-jump-form { margin-left: auto; }
.map-jump-form select { max-width: 300px; min-height: 44px; }
/* The ZIP field and the district picker were browser-default boxes
   (square, no inner padding) beside the rounded Find button. 16px text
   keeps iOS from zooming the page when the field takes focus. */
.map-zip-row input,
.map-jump-form select {
  border: 1px solid var(--ink-300);
  border-radius: var(--r-md);
  padding: 8px var(--sp-3);
  background-color: var(--paper);
  color: var(--ink-900);
  font-size: 16px;
}

.map-legend { list-style: none; display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-3); margin: 0 0 var(--sp-2); padding: 0; font-size: var(--fs-sm); color: var(--ink-600); }
.map-legend strong { color: var(--ink-900); }
.swatch { display: inline-block; width: 14px; height: 14px; border-radius: 2px; vertical-align: -2px; margin-right: 4px; border: 1px solid var(--ink-300); }
.swatch-D { background: var(--party-D); border-color: var(--party-D); }
.swatch-R { background: var(--party-R); border-color: var(--party-R); }
.swatch-I { background: var(--party-I); border-color: var(--party-I); }
.swatch-lead-D { background: repeating-linear-gradient(45deg, var(--party-D) 0 3px, var(--ink-100) 3px 6px); }
.swatch-lead-R { background: repeating-linear-gradient(45deg, var(--party-R) 0 3px, var(--ink-100) 3px 6px); }
.swatch-vacant { background: radial-gradient(var(--ink-500) 1px, var(--ink-100) 1.5px); background-size: 5px 5px; }
.swatch-nodata { background: var(--ink-200); }
/* A state with no live feed reads as a different thing from a district
   whose count has not started: lighter, and struck through with a
   diagonal so it is not only a shade apart. */
.swatch-nofeed { background: repeating-linear-gradient(45deg, var(--ink-100) 0 3px, var(--ink-300) 3px 4px); }
.swatch-flip { background: transparent; border: 2px solid var(--ink-900); }

.map-balance { position: relative; height: 10px; border-radius: 5px; overflow: visible; display: flex; background: var(--ink-200); margin: 0 0 var(--sp-4); }
.map-balance .seg { display: block; height: 100%; transition: width 0.4s; }
.map-balance .seg:first-child { border-radius: 5px 0 0 5px; }
.map-balance .seg:last-of-type { border-radius: 0 5px 5px 0; }
.map-balance .seg-D { background: var(--party-D); }
.map-balance .seg-lead-D { background: repeating-linear-gradient(45deg, var(--party-D) 0 3px, var(--ink-100) 3px 6px); }
.map-balance .seg-I { background: var(--party-I); }
.map-balance .seg-open { background: var(--ink-200); }
.map-balance .seg-lead-R { background: repeating-linear-gradient(45deg, var(--party-R) 0 3px, var(--ink-100) 3px 6px); }
.map-balance .seg-R { background: var(--party-R); }
.map-balance .majority-mark { position: absolute; left: 50%; top: -3px; width: 2px; height: 16px; background: var(--ink-900); }
.map-balance .majority-mark span { position: absolute; top: 18px; left: 50%; transform: translateX(-50%); font-size: var(--fs-xs); color: var(--ink-600); white-space: nowrap; }
.map-balance + .map-layout { margin-top: var(--sp-6); }

.map-layout { display: grid; grid-template-columns: minmax(0, 1fr) 340px; gap: var(--sp-4); align-items: start; }
/* The map is the page. The site container is sized for reading, not for a
   435-district map, so this page alone widens it and lets the stage grow
   until it would run off the bottom of the screen (the SVG keeps its 960 by
   600 aspect, so width is what sets height). */
.map-page > .container { max-width: 1760px; }
.map-stage { max-width: calc(86vh * 1.6); }
/* The zoom buttons sit over the top-left of the stage; the hint starts
   clear of them (44px button + 8px offset + a gap). */
.map-hint { font-size: var(--fs-xs); color: var(--ink-500); margin: var(--sp-1) 0 0; padding-left: 64px; }
/* Phones hide .map-hint and the panel hint sits in the closed sheet, so
   this line carries the tap instructions there (see election_map.js). */
.map-hint-phone { display: none; }
.map-stage { position: relative; outline: none; border-radius: var(--r-md); }
.map-stage:focus-visible { box-shadow: 0 0 0 3px var(--civic-blue-soft); }
.map-stage svg.map-defs { position: absolute; width: 0; height: 0; }
.map-stage > svg:not(.map-defs) { width: 100%; height: auto; display: block; touch-action: pan-y; }
.map-stage.is-zoomed > svg:not(.map-defs) { touch-action: none; cursor: grab; }
.map-stage.is-dragging > svg:not(.map-defs) { cursor: grabbing; }
.map-stage #districts path, .map-stage #states path { vector-effect: non-scaling-stroke; }
.map-stage #districts path { fill: var(--ink-200); stroke: var(--ink-50); stroke-width: 0.5; cursor: pointer; transition: fill 0.15s; }
.map-stage #districts path.party-D { fill: var(--party-D); }
.map-stage #districts path.party-R { fill: var(--party-R); }
.map-stage #districts path.party-I { fill: var(--party-I); }
.map-stage #districts path.party-L, .map-stage #districts path.party-G, .map-stage #districts path.party-X { fill: url(#hatch-other); }
.map-stage #districts path.leading.party-D { fill: url(#hatch-D); }
.map-stage #districts path.leading.party-R { fill: url(#hatch-R); }
.map-stage #districts path.leading.party-I { fill: url(#hatch-I); }
.map-stage #districts path.vacant { fill: url(#dots-vacant); }
.map-stage #districts path.unknown, .map-stage #districts path.nodata { fill: var(--ink-200); }
.map-stage #districts path.nofeed { fill: url(#hatch-nofeed); }
.map-stage #districts path.flip { stroke: var(--ink-900); stroke-width: 1.1; }
.map-stage #states path.hover { stroke: var(--ink-900); stroke-width: 1.6; fill: none; pointer-events: none; }
.map-stage #districts path.zip-hit { stroke: var(--ink-900); stroke-width: 1.4; }
.map-stage #districts path.cursor { stroke: var(--ink-900); stroke-width: 1.6; stroke-dasharray: 3 2; }
.map-stage #districts path:hover { stroke: var(--warning); stroke-width: 1.6; }
/* The pinned district: ink, not amber, so the outline clears 3:1 against
   every party fill in both themes, and wider than .zip-hit (1.4) and the
   dashed .cursor (1.6) so the three stay distinct. */
.map-stage #districts path.selected { stroke: var(--ink-900); stroke-width: 2.6; }
.map-stage #states path { stroke: var(--ink-700); stroke-width: 0.7; fill: none; }
.hatch-bg { fill: var(--ink-100); }
.hatch-fill-D { fill: var(--party-D); }
.hatch-fill-R { fill: var(--party-R); }
.hatch-fill-I { fill: var(--party-I); }
.hatch-fill-other { fill: var(--ink-600); }
.hatch-fill-nofeed { fill: var(--ink-300); }
.dot-vacant { fill: var(--ink-500); }
/* election_map.js keeps the labels at 11px on screen at every zoom; the
   halo must not scale either, or at 11x it is a 30px white disc over the
   small districts a reader zoomed in to pick. */
#map-labels text { font-family: var(--font-body); fill: var(--ink-900); paint-order: stroke; stroke: var(--ink-50); stroke-width: 2.5px; stroke-linejoin: round; text-anchor: middle; pointer-events: none; vector-effect: non-scaling-stroke; }

.map-zoom { position: absolute; top: var(--sp-2); left: var(--sp-2); z-index: 2; display: flex; flex-direction: column; gap: 4px; }
.map-zoom-btn { width: 44px; height: 44px; border: 1px solid var(--ink-300); background: var(--ink-50); color: var(--ink-900); border-radius: var(--r-sm); font-size: var(--fs-lg); line-height: 1; cursor: pointer; }
.map-zoom-btn.map-zoom-reset { font-size: var(--fs-xs); width: auto; padding: 0 8px; }
.map-zoom-btn:hover { background: var(--ink-100); }

.map-tip { position: absolute; z-index: 3; pointer-events: none; max-width: 260px; padding: 6px 10px; border-radius: var(--r-sm); background: var(--ink-900); color: var(--ink-50); font-size: var(--fs-sm); line-height: 1.35; box-shadow: var(--shadow-md); }
.map-tip .party-badge { margin-left: 4px; }

.map-panel { position: sticky; top: calc(var(--nav-h) + var(--sp-4)); max-height: calc(100vh - var(--nav-h) - var(--sp-8)); overflow: auto; }
.map-panel-close { position: absolute; top: var(--sp-2); right: var(--sp-2); width: 44px; height: 44px; border: 0; background: transparent; color: var(--ink-700); border-radius: var(--r-full); cursor: pointer; }
.map-panel-close:hover { background: var(--ink-100); }
.map-panel-title { font-size: var(--fs-lg); margin: 0 0 var(--sp-2); padding-right: 44px; }
.map-panel-head { display: flex; gap: var(--sp-3); align-items: center; margin: 0 0 var(--sp-2); }
.map-portrait { width: 56px; height: 56px; border-radius: var(--r-full); object-fit: cover; background: var(--ink-200); flex-shrink: 0; }
.map-portrait-blank { display: inline-flex; align-items: center; justify-content: center; font-weight: 700; color: var(--ink-600); }
.map-panel-member { font-size: var(--fs-base); margin: 0; }
.map-panel-sub { font-size: var(--fs-sm); text-transform: uppercase; letter-spacing: 0.04em; color: var(--ink-600); margin: var(--sp-3) 0 var(--sp-2); }
.map-stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--sp-2); margin: var(--sp-2) 0; }
.map-stat { background: var(--ink-100); border-radius: var(--r-sm); padding: 8px; text-align: center; }
.map-stat strong { display: block; font-size: var(--fs-lg); color: var(--ink-900); }
.map-stat span { font-size: var(--fs-xs); color: var(--ink-600); }
.map-bar { height: 6px; border-radius: 3px; background: var(--ink-200); overflow: hidden; margin: 4px 0; }
.map-bar > span { display: block; height: 100%; background: var(--ink-600); }
.cand-row { padding: 8px 0; border-top: 1px solid var(--ink-200); font-size: var(--fs-sm); }
.cand-row.is-winner { font-weight: 600; }
.cand-line { display: flex; justify-content: space-between; gap: var(--sp-2); }
.cand-votes { white-space: nowrap; color: var(--ink-700); font-variant-numeric: tabular-nums; }
.cand-bar { height: 8px; border-radius: 4px; background: var(--ink-100); overflow: hidden; margin-top: 4px; }
.cand-bar > span { display: block; height: 100%; }
.cand-bar > span.party-D { background: var(--party-D); }
.cand-bar > span.party-R { background: var(--party-R); }
.cand-bar > span.party-I { background: var(--party-I); }
.cand-bar > span.party-X { background: var(--ink-500); }
.cand-row:not(.is-winner) .cand-bar > span { opacity: 0.45; }
.map-chip { display: inline-block; padding: 2px 8px; border-radius: var(--r-full); font-size: var(--fs-xs); background: var(--ink-100); color: var(--ink-800); margin-left: 6px; }
.map-panel-actions { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-top: var(--sp-3); }
.map-zip-choices { display: flex; flex-direction: column; gap: var(--sp-2); margin: var(--sp-2) 0; }
.map-home { margin: var(--sp-2) 0 0; font-size: var(--fs-sm); }
.map-sources { margin-top: var(--sp-4); }

@media (max-width: 900px) {
  .map-hint { display: none; }
  .map-hint-phone { display: block; margin: 0; }
  /* On a phone the zoom buttons sit in a row above the map instead of over
     the West Coast districts. */
  .map-zoom { position: static; flex-direction: row; justify-content: flex-end; margin: 0 0 var(--sp-2); }
  .map-page > .container { max-width: none; }
  .map-layout { grid-template-columns: 1fr; }
  .map-jump-form { margin-left: 0; width: 100%; }
  .map-jump-form select { max-width: none; width: 100%; }
  .map-zip { width: 100%; }
  .map-zip-row input { flex: 1; }
  .map-panel { position: fixed; left: 0; right: 0; bottom: 0; top: auto; max-height: 48vh; z-index: 1030; border-radius: 12px 12px 0 0; box-shadow: var(--shadow-lg); transform: translateY(105%); transition: transform 0.25s ease-out; }
  .map-panel.is-open { transform: translateY(0); }
  .map-panel .card-body::before { content: ''; display: block; width: 36px; height: 4px; border-radius: 2px; background: var(--ink-300); margin: 0 auto var(--sp-2); }
  body.map-sheet-open { padding-bottom: 48vh; }
}
@media (prefers-reduced-motion: reduce) {
  .map-stage #districts path, .map-balance .seg, .map-panel { transition: none; }
}
