html { scroll-behavior: smooth; }
html, body { margin: 0; min-height: 100%; }
body { font-family: var(--body); line-height: 1.5; color: var(--text); }
/* form controls don't inherit the body face by default (they'd fall back
   to the platform's Arial) */
button, input, select, textarea { font-family: inherit; color: inherit; }
/* Height of the sticky site header, in one place: #sitehead sets its own
   height from this and #map-wrap subtracts it to fill the rest of the screen.
   Declared here rather than in the token block below, which is generated. */
:root { --sitehead-h: 56px; }

/* The map owns the first viewport; the planning section scrolls in below
   (the panel is absolute within the map wrapper, so it scrolls away with
   the map rather than following the reader down the page). */
/* Full viewport height minus the sticky site header, so the map fills the
   screen at any window size. Was 70vh, deliberately short so the top of the
   analyses showed on load and the page said there was more below it — that
   cue is now carried by the scroll hint alone. dvh so mobile browser chrome
   collapsing does not leave a gap; the vh line above it is the fallback for
   engines without dvh. */
#map-wrap { position: relative;
  /* the header's 1px bottom border sits outside its declared height */
  height: calc(100vh - var(--sitehead-h) - 1px);
  height: calc(100dvh - var(--sitehead-h) - 1px);
  min-height: 460px;
  overflow: hidden; }
#map { position: absolute; inset: 0; }

#loading {
  position: absolute; top: 12px; left: 50%; transform: translateX(-50%);
  background: rgba(0,0,0,0.75); color: #fff; padding: 7px 14px 9px;
  border-radius: 6px; font-size: 13.5px; z-index: 5;
  min-width: 232px; text-align: center;
}
#load-text { display: block; margin-bottom: 6px; }
#load-track {
  display: block; height: 3px; border-radius: 2px;
  background: rgba(255, 255, 255, 0.22); overflow: hidden;
}
#load-bar {
  display: block; height: 100%; width: 0%;
  background: var(--brand-accent, #9edfff);
  transition: width .35s ease;
  position: relative; overflow: hidden;
}
/* The traffic file is 58% of the load, so the bar necessarily holds still
   for most of the wait. The sheen says work is continuing without claiming a
   precision the numbers do not have. */
#loading.working #load-bar::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(90deg, transparent, rgba(255,255,255,.85), transparent);
  animation: loadsheen 1.1s linear infinite;
}
@keyframes loadsheen {
  from { transform: translateX(-100%); }
  to   { transform: translateX(100%); }
}
@media (prefers-reduced-motion: reduce) {
  #load-bar { transition: none; }
  #loading.working #load-bar::after { animation: none; }
}

#panel {
  position: absolute; top: 12px; left: 12px; width: 300px;
  background: #fff; border-radius: 10px; padding: 16px;
  box-shadow: 0 2px 12px rgba(0,0,0,0.2); font-size: 13.5px; z-index: 5;
  /* border-box + a WRAP-relative cap: the old 100vh cap let the panel
     overhang the report below the map (pack 03.1 changes 2-3) */
  box-sizing: border-box;
  max-height: calc(100% - 24px); overflow-y: auto;
  /* the content is usually taller than the box — say so (change 4) */
  scrollbar-width: thin; scrollbar-color: #c3ccd3 transparent;
}
#panel::-webkit-scrollbar { width: 6px; }
#panel::-webkit-scrollbar-thumb { background: #c3ccd3; border-radius: 3px; }

#brand-title { font-weight: 600; margin-bottom: 4px; }
.subtitle { color: #555; margin-bottom: 12px; }

.ctrl-group { margin-bottom: 14px; }
.ctrl-group label { display: block; font-weight: 600; margin-bottom: 4px; }
.ctrl-group input[type="range"] { width: 100%; }
.inline-label { display: inline !important; font-weight: 600; margin-left: 10px; }

/* Collapsible sidebar: the button lives in the panel's top-right corner;
   collapsed, the panel shrinks to just that button so the city gets the
   whole viewport back. */
#panel-collapse {
  position: absolute; top: 10px; right: 10px; z-index: 2;
  width: 24px; height: 24px; padding: 0;
  border: 1px solid #ccc; border-radius: 6px; background: #f5f5f5;
  color: #333; font-size: 12px; line-height: 1; cursor: pointer;
}
#panel-collapse:hover { background: #eee; }
#panel.collapsed { width: auto; padding: 6px; }
#panel.collapsed > *:not(#panel-collapse) { display: none; }
#panel.collapsed #panel-collapse { position: static; width: 30px; height: 30px; }

.date-row { display: flex; gap: 12px; }
.date-row > div { flex: 1; min-width: 0; }
.date-row label { display: block; font-weight: 600; margin-bottom: 4px; }
.date-row input[type="range"] { width: 100%; }
/* Was pale blue text on a translucent blue tint — written for the dark panel
   this used to live in, and barely legible on the white one. It now takes the
   same solid accent fill as the "Explore the map" button, with the ink dark
   against it and at normal weight: it is a note about the scenario, not a
   warning to be shouted. */
/* Which of the three days you are looking at: past, present, or projected.
   One box, three states — the title carries the difference and the fill
   backs it up, cooling from the solid accent of "now" to a tint for what is
   only modelled and to neutral grey for what is already over. */
#scenario-note {
  margin: 2px 0 10px;
  padding: 9px 11px;
  border-radius: 8px;
  background: var(--brand-accent, #9edfff);
  border: 0;
  color: var(--brand-deep, #051c23);
  font-size: 12px;
  line-height: 1.45;
  font-weight: 400;
}
/* One height whatever the date says. The box sits directly above the year
   slider, so a message that ran to four lines on one date and two on the
   next shifted every control under it as you dragged. Three lines, fixed,
   with anything longer cropped to an ellipsis — the full text stays on the
   element's title for hover. */
#scenario-note {
  display: -webkit-box; -webkit-box-orient: vertical;
  -webkit-line-clamp: 3; line-clamp: 3;
  overflow: hidden;
  box-sizing: border-box;
  height: var(--note-h);
}
#scenario-note[hidden] { display: none; }   /* -webkit-box outranks [hidden] */
#scenario-note b { font-weight: 600; }
#scenario-note[data-state="past"]     { background: #e4e8ec; }
#scenario-note[data-state="present"]  { background: var(--brand-accent, #9edfff); }
#scenario-note[data-state="future"]   { background: var(--brand-soft, #cdeeff); }
#scenario-note[data-state="election"] { background: var(--brand-accent, #9edfff); }

.play-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.play-row button {
  padding: 6px 12px; border-radius: 6px; border: 1px solid #ccc;
  background: #f5f5f5; cursor: pointer; font-size: 13.5px;
}
.play-row button:hover { background: #eee; }
.play-row input[type="range"] { flex: 1; min-width: 60px; width: auto; }

/* Three figures across, each a number over its label. Was three sentences
   stacked, which buried the figures in the middle of the words. */
/* Three equal COLUMNS looked unevenly spaced, because the contents are not
   equal: "80" leaves most of its column empty while "~7,790" fills it, so the
   gap after the short figure read as a hole. Size each tile to its own
   content and let the free space fall between them instead — then the gaps
   are equal by construction, which is what the eye is actually measuring.
   Every label carries its own <br>, so nothing re-wraps underneath this. */
#stats {
  display: flex; justify-content: space-between; align-items: flex-start;
  gap: 10px; margin: 2px 0 16px;
}
#stats .stat { min-width: 0; }
#stats .stat span { white-space: nowrap; }
#stats .stat[hidden] { display: none; }
#stats .stat b {
  display: block;
  font-size: 21px; font-weight: 600; line-height: 1.1;
  letter-spacing: -.02em; color: var(--brand, #064d90);
  /* the figures are rewritten every frame — fixed-width digits stop the
     column jittering as they change */
  font-variant-numeric: tabular-nums;
}
#stats .stat span {
  display: block; margin-top: 3px;
  font-size: 10.5px; line-height: 1.3; font-weight: 400; color: #6b7a83;
}

#histogram {
  display: flex; align-items: flex-end; gap: 2px; height: 48px;
  border-bottom: 1px solid #ddd; padding-bottom: 2px;
}
.hbar { flex: 1; background: #ccc; border-radius: 2px 2px 0 0; min-height: 2px; }
/* The bar for the hour you are looking at. Was an off-brand purple; it is the
   same interactive blue as the sliders that move it. */
.hbar.active { background: var(--brand-line, #008cff); }

#legend, #congestion-legend { list-style: none; margin: 0; padding: 0; }
.legend-row { display: flex; align-items: center; gap: 8px; padding: 2px 0; }
.swatch { width: 12px; height: 12px; border-radius: 3px; flex-shrink: 0; }

.chk-label { display: flex !important; align-items: center; gap: 6px; margin-bottom: 6px !important; }
.chk-label input[type="checkbox"] { margin: 0; }

/* ── tick / cross checkboxes (2026-09-15) ─────────────────────────────────
   No box: a blue tick when on, a black ✕ when off. The UA box is removed
   with appearance:none, so both marks are background images on the input. */
#panel .chk-label input[type="checkbox"] {
  appearance: none; -webkit-appearance: none;
  width: 17px; height: 17px; flex: none; margin: 0; padding: 0;
  border: 0; background: transparent center / 15px 15px no-repeat;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 18 18' fill='none' stroke='%23008cff' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2.5 9.5L6.8 14 15.5 4'/%3E%3C/svg%3E");
  cursor: pointer;
}
#panel .chk-label input[type="checkbox"]:not(:checked) {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 18 18' fill='none' stroke='%23051c23' stroke-width='2.4' stroke-linecap='round'%3E%3Cpath d='M4 4L14 14'/%3E%3Cpath d='M14 4L4 14'/%3E%3C/svg%3E"); background-size: 15px 15px;
}

/* the activity list uses the same marks instead of colour squares, at both
   the category and the tier-2 level. Selected rows tick; the rest are blank
   but still hold the column so the labels line up. */
#activity-legend .swatch {
  /* unticked rows carry the black ✕, the same pair of marks as the layer
     checkboxes above */
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 18 18' fill='none' stroke='%23051c23' stroke-width='2.4' stroke-linecap='round'%3E%3Cpath d='M4 4L14 14'/%3E%3Cpath d='M14 4L4 14'/%3E%3C/svg%3E") center / 15px 15px no-repeat !important;
  border-radius: 0; width: 15px; height: 15px;
}
#activity-legend .key-cat-head.sel > .key-main > .swatch,
#activity-legend .key-kids li.sel > .key-main > .swatch {
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 18 18' fill='none' stroke='%23008cff' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2.5 9.5L6.8 14 15.5 4'/%3E%3C/svg%3E") center / 15px 15px no-repeat !important;
}
#congestion-legend { margin-top: 4px; }

.note { color: var(--text-muted); font-size: 12px; line-height: 1.4; }

#maptip {
  position: absolute; bottom: 16px; left: 50%; transform: translateX(-50%);
  background: rgba(0, 0, 0, 0.5); color: #fff; padding: 8px 14px;
  border-radius: 8px; font-size: 12px; z-index: 6; line-height: 1.5;
  text-align: center;
}

#activity-legend { list-style: none; margin: 4px 0 0; padding: 0; }
.sub-note { font-size: 12px; color: #888; margin: -2px 0 4px 22px; font-style: italic; }


#maptip .muted { color: #9aa5b1; font-size: 12px; }

/* Resident card, matching the one on cambium.ai: anchored beside the pointer
   rather than in the fixed bottom-centre slot, dark teal ground, label/value
   rows, and the sources the population is actually built from. The transit
   tooltip keeps the plain banner — it is glanceable and not tied to a point. */
#maptip.at-cursor {
  /* absolute in #map-wrap, both axes container-relative — a fixed card
     drifted with scroll and slid under the activity key (pack 03.2) */
  position: absolute; bottom: auto; left: 0; top: 0; transform: none;
  z-index: 9;
  text-align: left; width: 254px; max-width: 254px;
  /* Transparent to the mouse while it is only a hover preview. The card is
     drawn next to the pointer and is far bigger than the dot, so the pointer
     ends up inside it — and with pointer-events on, the card swallowed the
     very click that was meant to pin it. Only a pinned card takes the mouse,
     which it needs for its close button. */
  pointer-events: none;
  background: rgba(0, 0, 0, 0.5);
  backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px);
  border-radius: 10px; padding: 13px 14px 12px;
  box-shadow: 0 6px 24px rgba(0,0,0,0.55);
  font-size: 12px; line-height: 1.5;
}
.rc-head { display: flex; align-items: baseline; gap: 8px; padding-right: 20px; }
.rc-head b { color: #fff; font-size: 13.5px; font-weight: 600; }
.rc-tag {
  margin-left: auto; color: #9edfff; font-size: 12px; font-weight: 600;
  letter-spacing: .11em; text-transform: uppercase;
}
.rc-sub { color: #8fb0c9; font-size: 12px; margin: 2px 0 10px; }

.rc-rows { border-top: 1px solid rgba(255,255,255,0.07); padding-top: 8px; }
.rc-row { display: flex; justify-content: space-between; gap: 12px; padding: 2.5px 0; }
.rc-row span { color: #8fb0c9; }
/* Values right-align into a column, so the eye can run down them. */
.rc-row b { color: #fff; font-weight: 500; text-align: right; }

.rc-srchead {
  margin: 11px 0 7px; padding-top: 10px;
  border-top: 1px solid rgba(255,255,255,0.07);
  color: #9edfff; font-size: 12px; font-weight: 600;
  letter-spacing: .11em; text-transform: uppercase;
}
.rc-pills { display: flex; flex-wrap: wrap; gap: 5px; }
.rc-pill {
  background: rgba(158, 223, 255, 0.12); color: #9edfff;
  border: 1px solid rgba(158, 223, 255, 0.26);
  border-radius: 5px; padding: 2px 7px; font-size: 12px;
}
.rc-more { color: #6b8385; font-size: 12px; margin-top: 7px; }

#maptip.at-cursor.pinned { pointer-events: auto; }
/* pinned to the card's own corner rather than riding at the end of the head
   row, where a long name or a wrapped tag pushed it down and inwards */
#maptip-close {
  position: absolute; top: 8px; right: 9px; margin: 0; padding: 0 4px;
  background: none; border: 0; color: #9aa5b1; cursor: pointer;
  font-size: 15px; line-height: 1.1;
}
#maptip-close:hover { color: #fff; }

/* Street search — a static index baked from Overture addresses; no geocoder,
   no backend, same as everything else on this page. */
#street-search {
  width: 100%; box-sizing: border-box; padding: 7px 10px;
  border: 1px solid #ccc; border-radius: 6px; font-size: 13.5px;
}
/* the app's own focus blue — this was the last off-brand purple left */
#street-search:focus {
  outline: 2px solid var(--brand-line, #008cff); outline-offset: 0;
  border-color: transparent;
}
#street-results {
  list-style: none; margin: 4px 0 0; padding: 0;
  border: 1px solid #ddd; border-radius: 6px; overflow: hidden;
}
#street-results li {
  padding: 6px 10px; cursor: pointer; font-size: 13.5px;
  display: flex; justify-content: space-between; gap: 8px;
}
#street-results li:hover, #street-results li.active { background: #f1ebfe; }
#street-results .loc { color: #888; font-size: 12px; }


/* ---- MA comprehensive-planning section, below the simulation ---- */


/* ══ report: the ten comprehensive-planning analyses ══ */
/* Planning-vertical palette (cambium.ai/planning) — pack 01, 2026-09-09.
   ONE accent: --brand. The master-brand green does not appear here. */
:root{
  /* brand — blue. one accent, four tints */
  --brand:            #064d90;   /* primary accent: stats, eyebrows, rules */
  --brand-line:       #008cff;   /* interactive: links, hover, focus       */
  --brand-accent:     #9edfff;   /* accent on dark grounds                 */
  --brand-soft:       #cdeeff;   /* tinted fills, callout backgrounds      */
  --brand-deep:       #051c23;   /* ink, and the dark ground               */

  /* text — ink at alpha, not separate greys */
  --text:             #051c23;
  --text-body:        #051c23cc;
  --text-muted:       #051c239e;

  /* surfaces */
  --surface:          #ffffff;
  --surface-grey:     #ededed;
  --surface-stone:    #f1f2ed;
  --surface-greyblue: #d8dddf;

  /* lines */
  --border:           #0000001a;
  --border-soft:      #00000010;

  /* semantic — real state only, never decoration */
  --destructive:      #d4183d;

  --radius:           .625rem;

  --font-display:     "Bricolage Grotesque","Geist",sans-serif;
  --font-sans:        "Geist",ui-sans-serif,system-ui,sans-serif;
  /* No mono is vendored — vendor/fonts ships Geist and Bricolage Grotesque
     only — so this stack fell through to whatever mono the OS happened to
     have, which is why the labels rendered in a face that is not the
     page's. Geist is the font the page actually loads. */
  --font-mono:        "Geist",ui-sans-serif,system-ui,sans-serif;

  /* dark grounds: map chrome + hero, derived from deep/accent */
  --overlay-ground:       rgba(5,28,35,.94);
  --overlay-ground-solid: rgba(5,28,35,.97);
  --overlay-line:         rgba(158,223,255,.18);
  --overlay-shadow:       0 6px 24px rgba(0,0,0,.55);
  --on-dark:              #ffffff;
  --on-dark-body:         #ffffffcc;
  --on-dark-muted:        #ffffff99;
  --on-dark-accent:       var(--brand-accent);

  /* compatibility aliases — migrate call sites, then delete */
  --ink:        var(--brand-deep);
  --deep:       var(--brand-deep);
  --darkground: var(--brand-deep);
  --paleblue:   var(--brand-accent);
  --accent:     var(--brand);         /* was an off-brand blue             */
  --mint:       var(--brand);         /* was the MASTER-brand green        */
  --sand:       var(--brand);         /* tan — no planning equivalent      */
  --warn:       var(--brand);         /* amber — no planning equivalent    */
  --hot:        var(--destructive);   /* error/failure states only         */
  --dim:        var(--text-body);
  --mute:       var(--text-muted);    /* was #6a828e — failed AA 4.04:1    */
  --line:       var(--border);
  --line-soft:  var(--border-soft);
  --ground:     #ececf0;
  --raised:     var(--surface-grey);
  /* Vertical rhythm: three steps on a 16px grid. Every block-level gap in
     the report is one of these. Type-level gaps inside a component (a
     caption under a heading, a bullet's leading) stay hand-set, and the
     few optical alignment offsets say so where they are written. */
  --sp-s:       16px;
  --sp-m:       32px;
  --sp-l:       64px;
  --r:          10px;
  --display:    var(--font-display);
  --body:       var(--font-sans);
  --mono:       var(--font-mono);
}
#report *{box-sizing:border-box}
#report{background:var(--ground);color:var(--ink);margin:0;font:15px/1.65 var(--body);
  -webkit-font-smoothing:antialiased}
#report h1,#report h2,#report h3,#report h4{font-family:var(--display);letter-spacing:-.02em;text-wrap:balance;margin:0}
#report a{color:var(--accent)}
#report .mono{font-family:var(--mono)}
#report .num{font-variant-numeric:tabular-nums}
#report .wrap{max-width:1180px;margin:0 auto;padding:0 24px}
#report header.top{border-bottom:1px solid var(--line-soft);position:sticky;top:0;z-index:50;
  background:rgba(5,28,35,.93);backdrop-filter:blur(12px)}
#report .topin{display:flex;align-items:center;gap:14px;height:60px;max-width:1180px;
  margin:0 auto;padding:0 24px}
#report .brand{display:flex;align-items:center;gap:9px;white-space:nowrap;flex:none;font-family:var(--display);
  font-weight:700;font-size:17px;letter-spacing:-.02em}
#report .brand .dot{width:9px;height:9px;border-radius:50%;background:var(--mint);
  box-shadow:0 0 12px rgba(158,223,255,.6)}
#report .brand span{color:var(--dim);font-weight:400}
#report .topnav{margin-left:auto;display:flex;gap:6px;overflow-x:auto;scrollbar-width:none}
#report .topnav::-webkit-scrollbar{display:none}
#report .topnav a{font:500 13.5px/1 var(--mono);color:var(--mute);text-decoration:none;
  padding:7px 9px;border-radius:7px;white-space:nowrap;transition:.15s}
#report .topnav a:hover{color:var(--accent);background:var(--raised)}
#report .topnav a.on{color:var(--ground);background:var(--accent)}
#report .hero{padding:var(--sp-l) 0 var(--sp-m);border-bottom:1px solid var(--line-soft);position:relative;
  overflow:hidden}
#report .hero::after{content:"";position:absolute;inset:0;pointer-events:none;
  background:radial-gradient(760px 340px at 82% -8%,rgba(158,223,255,.13),transparent 68%),
             radial-gradient(520px 280px at 8% 108%,rgba(158,223,255,.07),transparent 70%)}
#report .hero .in{position:relative;z-index:1;max-width:1180px;margin:0 auto;padding:0 24px}
#report .eyebrow{font:600 13.5px/1.5 var(--display);letter-spacing:.075em;text-transform:uppercase;
  color:var(--mint);margin-bottom:18px}
/* On the dark ground the accent blue all but disappears, so the eyebrow is
   set in white there. */
#report .rintro .eyebrow{color:#fff}
#report .hero h1{font-size:32px;line-height:1.15;max-width:24ch;font-weight:400}
#report .hero h1 em{font-style:normal;color:var(--accent)}
#report .hero p{color:var(--dim);font-size:17px;max-width:65ch;margin:20px 0 0;line-height:1.6}
#report .herostats{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));
  gap:1px;margin-top:44px;background:var(--line-soft);border:1px solid var(--line-soft);
  border-radius:var(--r);overflow:hidden}
#report .herostats div{background:var(--surface);padding:17px 18px}
#report .herostats b{display:block;font-family:var(--display);font-size:32px;font-weight:700;font-variant-numeric:tabular-nums;
  line-height:1.1;letter-spacing:-.03em}
#report .herostats span{display:block;font:500 12px/1.4 var(--mono);color:var(--mute);
  text-transform:uppercase;letter-spacing:.07em;margin-top:7px}
#report section.an{padding:var(--sp-l) 0 var(--sp-s);border-bottom:1px solid var(--line-soft)}
#report section.an:last-of-type{border-bottom:none}
#report .anhead{display:flex;gap:20px;align-items:flex-start;margin-bottom:8px}
/* A circle, and sized for two digits: 50% rather than a 9px radius, and a
   step down in type so "01" sits in it as comfortably as "1" did. */
#report .annum{font-family:var(--display);font-size:13px;font-weight:700;color:#ffffff;
  background:var(--accent);width:34px;height:34px;border-radius:50%;flex:none;
  display:flex;align-items:center;justify-content:center;letter-spacing:.02em;
  /* Centred on the h2's x-height, not its line box. Bricolage Grotesque at
     32px/1.15: baseline 28.9px down the box, x-height 16.5px, so the x-band
     centres 20.6px down — half the 34px disc above that is 3.6px. */
  margin-top:3.6px}
#report .anhead h2{font-size:32px;font-weight:400;line-height:1.15}
#report .ansub{color:var(--dim);max-width:65ch;margin:12px 0 0;font-size:15px}
#report .req{border-left:2px solid var(--mint);background:linear-gradient(90deg,
  rgba(6,77,144,.06),transparent 70%);padding:13px 18px;margin:22px 0 26px;
  border-radius:0 10px 10px 0}
#report .req b{font:600 13.5px/1 var(--display);letter-spacing:.075em;text-transform:uppercase;
  color:var(--mint);display:block;margin-bottom:14px}
/* The requirement is the section's premise, so it is set in the display face
   at the section title's weight rather than as body copy. */
#report .req p{margin:0;color:var(--brand);font-family:var(--display);
  font-size:18px;font-weight:400;line-height:1.5;letter-spacing:-.005em;
  max-width:65ch}
#report .stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:var(--sp-s);
  margin:0 0 var(--sp-m)}
/* Stat boxes sit on the dark ground: figure in the pale brand blue, label
   in white. mint/sand/warn all alias to --brand, which is unreadable on the
   dark ground, so they take the pale blue too. */
#report .stat{background:var(--darkground);border:1px solid var(--darkground);border-radius:12px;
  padding:15px 17px}
#report .stat b{display:block;font-family:var(--display);font-size:20px;font-weight:700;font-variant-numeric:tabular-nums;
  line-height:1.12;letter-spacing:-.025em;color:var(--brand-accent)}
#report .stat{box-shadow:0 1px 2px rgba(5,28,35,.05)}
/* History has three figures, not four, so the fourth cell would be a hole.
   The 87k box takes the whole left column instead, its figure on the same
   line as the growth figure beside it, and the two that are read together —
   the 2020 count and the growth that produced it — stack on the right. */
#report #history .secgrid > .stats{
  grid-template-columns:repeat(2,minmax(0,1fr))}
#report #history .stats > .stat:nth-child(1){grid-column:2;grid-row:2}
#report #history .stats > .stat:nth-child(2){grid-column:2;grid-row:1}
#report #history .stats > .stat:nth-child(3){grid-column:1;grid-row:1/span 2}
/* Projection has three figures too: 718k takes the left column and the two
   dates it is read against stack on the right. */
#report #projection .secgrid > .stats{
  grid-template-columns:repeat(2,minmax(0,1fr))}
#report #projection .stats > .stat:nth-child(1){grid-column:1;grid-row:1/span 2}
#report #projection .stats > .stat:nth-child(2){grid-column:2;grid-row:1}
#report #projection .stats > .stat:nth-child(3){grid-column:2;grid-row:2}
/* Market's four run over three rows, each column a tall box and a short one:
   the two money figures lead, and the counts they explain sit against them.
     $789k (1-2) | $3,469 (1)
                 | 11k (2-3)
     20k   (3)   |
*/
#report #market .secgrid > .stats{
  grid-template-columns:repeat(2,minmax(0,1fr))}
#report #market .stats > .stat:nth-child(1){grid-column:1;grid-row:1/span 2}
#report #market .stats > .stat:nth-child(2){grid-column:2;grid-row:1}
#report #market .stats > .stat:nth-child(3){grid-column:1;grid-row:3}
#report #market .stats > .stat:nth-child(4){grid-column:2;grid-row:2/span 2}
/* The spanning box's contents sit at the foot of it, so its figure and its
   label land on the lines of the box in the second row rather than floating
   in the middle of two rows' worth of empty ground. */
#report #history .stats > .stat:nth-child(3),
#report #projection .stats > .stat:nth-child(1),
#report #market .stats > .stat:nth-child(1),
#report #market .stats > .stat:nth-child(4){
  display:flex;flex-direction:column;justify-content:flex-end}
#report .stat.mint b,#report .stat.sand b,#report .stat.warn b{
  color:var(--brand-accent)}
#report .stat span{display:block;font:500 12px/1.45 var(--mono);color:#fff;
  text-transform:uppercase;letter-spacing:.06em;margin-top:8px}
/* The module grid ends flush with its opening card, so the row after it
   supplies the gap itself. */
#report .statmod-in > .secgrid + .grid{margin-top:var(--sp-s)}
#report .grid{display:grid;gap:var(--sp-s);margin-bottom:var(--sp-s);align-items:stretch}
#report .g2{grid-template-columns:repeat(auto-fit,minmax(330px,1fr))}

/* ── the full-bleed stats + charts module ──────────────────────────────
   From the stats boxes down, the section leaves .rwrap's 1180px column and
   runs the width of the viewport. The negative margins do the break-out
   (`calc(50% - 50vw)` on each side) rather than `width:100vw`, which would
   be wider than the page whenever a classic scrollbar is showing. */
/* No break-out any more. The module sits in the section's own box, which is
   what puts the requirement label on the same left edge as the standfirst —
   #report-main carries a 32px padding that a viewport-width break-out
   escapes, leaving the module 32px to the left of everything above it. */
#report .statmod{margin-left:0;margin-right:0}
/* The module takes the section's own width, so everything in it lines up
   with the heading and standfirst above. */
#report .statmod-in{max-width:none;margin:0;padding:0}
/* ── the section's opening module ──────────────────────────────────────
   One grid:

     requirement   |  methodology
     sources ......................  (spans the pair)
     opening card  |  stats
                   |  second card

   Sources spanning both columns is what puts the opening card and the stats
   on the same line — they are the row beneath it. */
/* Explicit row sizes. Without them the opening card, which spans rows 3 and
   4, hands its surplus height to BOTH rows equally, and the gap between the
   stats and the chart beside it came out anywhere from 16px to 66px
   depending on how tall the map was. Pinning row 3 to its content sends all
   of the surplus into row 4, where it grows the chart instead of the gap. */
#report .secgrid{display:grid;
  grid-template-columns:minmax(0,1.15fr) minmax(0,0.85fr);
  grid-template-rows:auto var(--sp-m) min-content minmax(0,1fr);
  gap:0 34px;align-items:start;margin:var(--sp-m) 0 0}
/* Row 2 carries the gap for both columns now that the sources have moved
   to the foot of the section, so the requirement needs no margin of its own. */
#report .secgrid > .req{grid-column:1;grid-row:1;margin:0}
#report .secgrid > .reqside{grid-column:2;grid-row:1;margin:0;min-width:0}
#report .secgrid > .src{grid-column:1/-1;grid-row:2;margin:var(--sp-m) 0 var(--sp-m);
  overflow-wrap:anywhere}
/* The opening card runs the full height of the column beside it, so it ends
   level with the last card in that column.
   A MAP is the exception: it keeps its aspect ratio, so stretching its card
   would add empty height below the map rather than a bigger map. */
/* min-height:0 so the card does not force the row taller than the column
   beside it: the row is then sized by the stats and the chart inside takes
   whatever is left, which is what makes the two columns the same height. */
#report .secgrid > .secmap{grid-column:1;grid-row:3/span 2;align-self:stretch;
  min-height:0}
/* flex-basis 0, not auto: with `height:auto` the svg's own aspect-ratio
   height is what sizes the row, so the card stayed taller than the column
   beside it. At basis 0 it contributes nothing and grows into what is left. */
#report .secgrid > .secmap > svg{flex:1 1 0;min-height:0}
#report .secgrid > .secmap:has(.mapwrap){align-self:start}
/* Households opens on a seven-bar chart, not a map: no stretching to fill a
   column, and a cap so the 210-unit drawing is not blown up to fill a card. */
#report #households .secgrid > .secmap > svg{flex:0 1 auto;max-height:340px}
/* grid-auto-rows:1fr, so every box is as tall as the tallest: a two-line
   label made its row taller than the row beside it and the four came out at
   two different heights. */
#report .secgrid > .stats{grid-column:2;grid-row:3;margin:0;
  grid-template-columns:repeat(2,minmax(0,1fr));grid-auto-rows:1fr;
  gap:16px;align-content:start}
#report .secgrid > .secchart{grid-column:2;grid-row:4;margin:var(--sp-s) 0 0}
/* A second card marked .wide takes the whole width under the pair instead of
   the right column, and the opening card then stops spanning — it ends level
   with the stats beside it, and the wide card runs under both. */
#report .secgrid:has(> .secchart.wide) > .secmap{grid-row:3;align-self:stretch}
#report .secgrid > .secchart.wide{grid-column:1/-1;grid-row:4}
/* Two charts abreast inside one full-width card, on the module's own gutter. */
#report .chartpair{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);
  gap:var(--sp-s) 34px}
@media (max-width:767px){#report .chartpair{grid-template-columns:minmax(0,1fr)}}
/* :not(.cardhead) — the card head is a div too, and matching it here gave the
   title flex:1, which grew it to 80px and pushed the chart down the card.
   The opening card's chart is wrapped in a div with its own key under it, and
   an auto-height wrapper is what sizes the row — so the card came out 126px
   taller than the stats beside it. Passing the flex down through the wrapper
   lets the chart shrink and the row settle on the stats' height. */
#report .secgrid:has(> .secchart.wide) > .secmap > div:not(.cardhead),
#report .secgrid > .secmap.legendleft > div:not(.cardhead){
  flex:1 1 0;min-height:0;display:flex}
/* :not(.legendleft) — this selector carries :has(), whose argument counts
   towards specificity, so without it it outranked the key-beside-the-chart
   rules below and forced that row back into a column. */
#report .secgrid:has(> .secchart.wide) > .secmap:not(.legendleft) > div:not(.cardhead){
  flex-direction:column}
#report .secgrid:has(> .secchart.wide) > .secmap:not(.legendleft) > div:not(.cardhead) > svg{
  flex:1 1 0;min-height:0}
/* A key beside the chart makes that wrapper a row, not a column. The chart
   is given an explicit height rather than a max-height: an svg sized by its
   viewBox ratio has height:auto, which no max-height reins in, so it drew
   178px into a 151px slot and hung out of the card. height:100% plus `meet`
   scales it to the space it has and keeps the ratio. */
#report .secgrid > .secmap.legendleft > div:not(.cardhead){
  flex-direction:row;align-items:center;gap:20px}
/* The key reads from the top whatever the chart beside it does. */
#report .secgrid > .secmap.legendleft > div:not(.cardhead) > .legend{
  align-self:flex-start}
/* The module's gutter rather than the tighter 20px, so the key is clearly
   beside the chart and not part of its left axis. */
#report #market .secgrid > .secmap.legendleft > div:not(.cardhead){gap:34px}
/* align-self:stretch, not height:100%: the row's height comes from its own
   content, so a percentage height resolved against nothing and the chart
   collapsed to 46px. Stretching forces the cross size to the row instead,
   and `meet` keeps the drawing's ratio inside it. */
#report .secgrid > .secmap.legendleft > div:not(.cardhead) > svg{
  flex:1 1 0;align-self:stretch;height:auto;min-height:0;min-width:0}
#report .legendleft > div > svg{min-height:0}
/* A folded row (see planning.js). The opening card gives up its second row
   so the folded map can take it, and the folded map spans rows 4 and 5 so
   the card that came beside it stacks under the chart in the right column
   rather than being pushed below the map. */
#report .secgrid.hasfold{grid-template-rows:auto var(--sp-m) min-content auto}
#report .secgrid.hasfold > .secmap{grid-row:3;align-self:start}
/* A folded card clears the card above it by the medium step, not the small
   one: it is a new module under the opening one, not the next row of it. */
#report .secgrid.hasfold > .foldl{grid-column:1;grid-row:4;
  align-self:start;margin:var(--sp-m) 0 0}
/* The whole right column as one stack — see planning.js. It spans both of
   the left column's rows, so its own boxes follow each other at the scale's
   small step whatever the opening card and the map do beside them. */
#report .secgrid.hasfold > .rstack{grid-column:2;grid-row:3/span 2;
  display:flex;flex-direction:column;gap:var(--sp-s);min-width:0;
  align-self:start}
#report .secgrid.hasfold > .rstack > .stats,
#report .secgrid.hasfold > .rstack > .card{margin:0}

/* Sources in the right column, and the opening card starting on that same
   row — so its title is level with the sources text. */
#report .secgrid.srcright > .src{grid-column:2;grid-row:2;margin:var(--sp-m) 0 0}
#report .secgrid.srcright > .secmap{grid-row:2/span 3;align-self:start;
  margin-top:22px}
#report .secgrid.srcright > .stats{grid-row:3;margin-top:var(--sp-m)}
#report .secgrid.srcright > .secchart{grid-row:4}
/* No card surface — but only when the card really holds a map. The class is
   applied to whichever card opens a section, and in Economy that is a line
   chart, which needs its white card like any other. */
/* Not scoped to .statmod: the gallery page draws the same card outside the
   report's module wrapper and must get the same bare treatment. */
#report .secmap:has(.mapwrap){background:none;border-color:transparent;
  box-shadow:none;padding:0}
/* A map in a row of its own has no card above its title to hold it off the
   card before it — the grid gap alone put the title right against it. The
   section-opening map does not need this: the requirement above it already
   leaves the space. */
/* :has(.mapwrap) here too — the rule above that zeroes the padding carries
   it, and :has() adds its argument's specificity, so without it this loses
   and the padding never applies. */
/* Optical: the map sits 8px below the scale step so its frame aligns
   with the card beside it. Not part of the rhythm scale. */
#report .mappair > .secmap:has(.mapwrap){padding-top:calc(var(--sp-m) + 8px)}
/* In the map row the two columns start together: the stack's gap above is
   doubled from the grid's 16px, and the map card takes the same as padding
   so its title and map begin on the line the box beside it starts on. */
#report .maprow > .secmap:has(.mapwrap){padding-top:var(--sp-m)}
#report .maprow > .cardstack{margin-top:var(--sp-m)}
/* The "?" belongs beside the title it explains, not pinned to the far edge
   of a card that no longer has an edge. */
/* Only a real map: on a map the title floats on the page, so the "?" belongs
   beside it rather than out at an edge the card no longer has. A chart card
   still has its edge, and the "?" stays in the top-right corner. */
#report .secmap:has(.mapwrap) .cardhead{justify-content:flex-start}
#report .secmap:has(.mapwrap) .cardhead h3{flex:0 1 auto}
/* The "?" and the download sit as a pair beside the title, not spread across
   the head, and both carry the card's white so they read as controls on the
   page's grey ground. */
#report .secmap:has(.mapwrap) .cardhead{gap:5px}
/* Half again the size of the ones on a chart card: on a map there is no card
   edge to hold them, so they carry more weight. The "?" keeps its place on
   the title's x-height midline, which at 27px means half the extra height
   comes off the top margin. */
#report .secmap:has(.mapwrap) .cardhead .capq{
  margin-left:6px;margin-top:-.5px;width:27px;height:27px;font-size:14px}
#report .secmap:has(.mapwrap) .cardhead .mapdl{
  margin-left:0;margin-top:-.5px;width:27px;height:27px}
#report .secmap:has(.mapwrap) .cardhead .mapdl svg{width:16px;height:16px}
/* Half the width — at full width the key was a longer line than the title. */
#report .secmap .scale{max-width:50%}
/* A pair of maps sharing a row: no bleed, so their keys can use the width.
   The two columns are not equal, though, and two keys of different lengths
   under two maps of the same size read as a mistake — so the wider column's
   key is held to the narrower one's width. The factor is the columns' own
   ratio, 0.85/1.15, so it tracks any change to the grid. */
#report .mappair .secmap .scale{max-width:none}
#report .mappair > .secmap:first-child .scale{max-width:calc(100% * .85 / 1.15)}
/* A map that has the wide column to itself takes the same key width as the
   pair above it, so every banded ramp on the page is the same length. */
#report .statmod .g2.maprow > .secmap .scale{max-width:calc(100% * .85 / 1.15)}
/* They bleed outwards instead of both to the left — the left map past the
   left margin, the right map past the right — which gets each of them to
   about the size of the map that opens section 1 without the two meeting in
   the middle. A percentage, not a fixed px, so the bleed shrinks with the
   column instead of running off the page on a narrower window. */
@media (min-width:1024px){
  /* The bleed is the page gutter, not a percentage of the column: a fixed
     26% looked right at 1400 and hung 82px off the left edge at 1200,
     because the module stops narrowing at its 1180px cap while the window
     keeps shrinking. This measures what is actually outside the module —
     half the space beyond 1180, plus the 32px padding on #report-main —
     and leaves a 24px margin against the page edge at every width. */
  #report .mappair{--bleed:calc(max(0px,(100vw - 1180px)/2) + 32px)}
  #report .mappair > .secmap:first-child .mapwrap{
    width:calc(100% + var(--bleed));margin-left:calc(-1 * var(--bleed))}
  #report .mappair > .secmap:last-child .mapwrap{
    width:calc(100% + var(--bleed))}
}
/* The stepped key: a label over one swatch per value, rather than a ramp
   between two end labels. */
#report .scale.steps{display:block;max-width:none;margin:0 0 18px}
#report .scale.steps:has(.spread){display:flex;align-items:flex-start}
#report .steps-lab{display:block;font:600 11px/1 var(--display);
  letter-spacing:.1em;text-transform:uppercase;color:var(--dim);margin:0 0 9px}
#report .steps-row{display:flex;gap:10px}
/* A scale rather than a list of categories: the bands share the width. */
#report .steps-row.spread{gap:4px;flex:1}
#report .steps-row.spread .step{width:auto;flex:1;min-width:34px}
/* packed (see fitKeys): the bands sit left, each the width of the longest
   label, rather than stretching across the card. */
#report .steps-row.spread.packed{justify-content:flex-start;flex:none}
#report .steps-row.spread.packed .step{flex:none}
#report .steps-row.spread.packed .step span{white-space:nowrap}
/* A range is a longer label than a single value, so it is set smaller and
   allowed to wrap rather than being clipped by its band. */
#report .steps-row.spread .step span{text-align:left;font-size:11px;
  line-height:1.3;white-space:normal;overflow-wrap:anywhere}
#report .step{display:flex;flex-direction:column;gap:6px;width:56px;
  cursor:pointer;transition:opacity .15s}
/* The steps not being hovered take the same grey the map areas take, so the
   key shows the same state the map is in rather than just fading.
   #e3e7ea matches the DIM constant in planning.js; !important because the
   swatch colour is an inline style set when the key is built. */
#report .scale.steps.focused .step:not(.on) i{background:#e3e7ea!important}
#report .scale.steps.focused .step:not(.on) span{opacity:.5}
#report .step:focus-visible{outline:2px solid var(--mint);outline-offset:3px;
  border-radius:4px}
#report .secmap svg path{transition:fill .15s}
/* A hairline inside the swatch. The palest class in a ramp is lighter than
   the page it sits on, so without it the first band reads as an empty gap —
   the map's own colours are left alone, because the key has to be the same
   colours the map paints. */
#report .step i{display:block;height:9px;border-radius:5px;
  box-shadow:inset 0 0 0 1px rgba(5,28,35,.14)}
#report .step span{font:500 12px var(--mono);color:var(--mute);text-align:center}
/* The map runs past its column on the left. Only on the left: the gutter to
   the right is 34px and the methodology column starts straight after it.
   Only above 1024px — narrower than that the bleed reaches past the page
   edge (measured: 10px off-screen at 900) and the map gets clipped. */
@media (min-width:1024px){
  /* The bleed is only for a map that has a column to itself. Two maps side
     by side both bleeding left would put the right one over the left. */
  #report .secgrid .secmap .mapwrap,
  #report .maprow > .secmap .mapwrap{width:118%;margin-left:calc(-18% - 20px)}
}
/* The map itself, two thirds bigger. The cap it overrides is an inline
   style on the <svg> (max-height:400px), so this needs !important. */
#report .secmap .mapwrap svg{max-height:667px!important}

/* A later row carrying a map takes the module's own proportions, so a map
   below the fold is the size of the one that opens the section, and its
   cap is lifted the same way. */
#report .statmod .g2.maprow,
/* Same proportions as the module above, so a card in the left column is the
   width of the one that opens the section. A plain two-card row that follows
   the module grid gets it too — its cards sit directly under the map and the
   chart, so anything else reads as a misalignment. */
#report .statmod .g2.ratio,
#report .statmod-in > .secgrid + .g2{
  grid-template-columns:minmax(0,1.15fr) minmax(0,0.85fr);
  /* the module's gutter, not the grid's 16px — with the same ratio but a
     different gap the columns come out 10px wider than the module's */
  column-gap:34px}
/* The map row keeps its cards at their own heights; a ratio row's two cards
   match, with the shorter one's chart centred in the space it gains. */
#report .statmod .g2.maprow{align-items:start}
#report .statmod .g2.ratio{align-items:stretch}
#report .statmod .g2.ratio > .card > svg{margin-block:auto}
/* The key beside the ring instead of under it: the ring is round, so a key
   under it leaves a band of empty card either side of the legend. */
#report .legendleft > div{display:flex;align-items:center;gap:20px}
/* Capped: the ring scales with the column, and at this width it grew tall
   enough to leave a band of empty card above and below it. */
#report .legendleft svg{flex:1;min-width:0;max-height:200px}
/* The entries not being hovered take the same grey the slices take.
   #e3e7ea matches the DIM constant in planning.js. */
#report .legend span{cursor:pointer;transition:opacity .15s}
#report .legend-on .legend span:not(.on) i{background:#e3e7ea!important}
#report .legend-on .legend span:not(.on){opacity:.65}
#report .legend span:focus-visible{outline:2px solid var(--mint);
  outline-offset:3px;border-radius:4px}
#report .legendleft .legend{order:-1;flex:0 0 auto;flex-direction:column;
  align-items:flex-start;gap:10px;margin:0;max-width:44%}
/* Key across the top, ring filling what is left of the card and centred in
   it. flex:1 with min-height:0 lets the svg shrink AND grow; `meet` keeps
   the ring round whatever shape the space it is given turns out to be. */
#report .secgrid > .secmap.donutfill > div:not(.cardhead){
  flex:1 1 0;min-height:0;display:flex;flex-direction:column}
#report .secgrid > .secmap.donutfill > div:not(.cardhead) > svg{
  flex:1 1 0;min-height:0;max-height:none}
/* Two entries read as one line, not a stack: the second belongs beside the
   first, since together they are the whole of the figure. */
#report .legendleft .legend.legend-row{flex-direction:row;flex-wrap:wrap;
  align-items:center;gap:14px;max-width:none}
/* A half ring has no bottom half to balance its top, so left in the flow it
   hangs below the key with a band of empty card above it. Lifted by the head
   (one line of h3, 25px, plus its 15px margin) it starts level with the title
   and ends level with the key. */
/* Gallery: the key stacked flush left with the chart beside it, so the ring
   gets the rest of the card's width instead of sitting under the key. */
/* The key's last line sits on the chart's own bottom, which keyToBaseline in
   charts-gallery.js finds from the drawn shape, not from the svg box. */
#report .card .lleft{display:flex;align-items:flex-end;gap:24px}
/* The work-status row is a seventh shorter than the ring's own proportions
   would make it: the cards take the height and the ring scales to fit. */
#report #demography .g2.ratio > .card{height:354px}
#report .card .lleft{min-height:0}
#report #demography .g2.ratio .lleft > svg{height:100%;width:auto;flex:0 1 auto}
/* rule 3: a card with room to spare centres the key on the ring */
#report .card .lleft.ringmid{align-items:center}
#report .card .lleft > .legend{order:-1;flex:0 0 auto;flex-direction:column;
  align-items:flex-start;gap:9px;margin:0}
#report .card .lleft > svg{flex:1;min-width:0}
/* The vertical version of the same pair: the key above the chart, one line
   per series, and the ring on the card's full width under it. */
#report .card .ltop{display:flex;flex-direction:column}
#report .card .ltop > .legend{flex-direction:row;flex-wrap:wrap;
  align-items:center;gap:8px 18px;margin:0 0 14px}
#report .card .ltop > svg{width:100%}
#report .halfring > div:not(.cardhead) > svg{margin-top:-40px}
/* The key ends where the arc ends rather than sitting centred against it. */
#report .halfring > div:not(.cardhead) > .legend{align-self:flex-end}
#report .maprow > .card .mapwrap svg{max-height:667px!important}
/* Cards stacked inside one grid cell. */
#report .cardstack{display:flex;flex-direction:column;gap:var(--sp-s);min-width:0}

/* Mid widths keep the two-column shape and only even up the ratio. The
   other chart rows go back to auto-fit; they have no placement to keep. */
@media (max-width:1023px){
  #report .secgrid{grid-template-columns:minmax(0,1fr) minmax(0,1fr)}
  #report .secmap .scale{max-width:none}
  #report .statmod .g2{grid-template-columns:repeat(auto-fit,minmax(330px,1fr))}
  #report .statmod .g2.maprow,
  #report .statmod .g2.ratio,
  #report .statmod-in > .secgrid + .g2{
    grid-template-columns:minmax(0,1fr) minmax(0,1fr)}
  #report .statmod .g3{grid-template-columns:repeat(auto-fit,minmax(255px,1fr))}
}

/* The last breaking point: one column, in DOM order — requirement,
   methodology, sources, map, chart, stats. A leftover `grid-column:2` on a
   grid that no longer has two tracks leaves a hole, so every placement is
   released rather than merely overridden. */
@media (max-width:767px){
  #report .secgrid{grid-template-columns:minmax(0,1fr);gap:0}
  /* Released, not merely overridden: a leftover `grid-column:2` on a grid
     with one track leaves a hole. */
  #report .secgrid > .req,
  #report .secgrid > .reqside,
  #report .secgrid > .src,
  #report .secgrid > .secmap,
  #report .secgrid > .secchart,
  #report .secgrid > .stats{grid-column:auto;grid-row:auto}
  #report .secgrid > .reqside{margin-top:26px}
  #report .secgrid .stats{
    grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:14px}
}
#report .g3{grid-template-columns:repeat(auto-fit,minmax(255px,1fr))}
#report .card{background:var(--surface);border:1px solid var(--line-soft);
  border-radius:var(--r);padding:19px 20px 15px;min-width:0;
  display:flex;flex-direction:column;box-shadow:0 1px 2px rgba(5,28,35,.05)}
#report .card.wide{grid-column:1/-1}
#report .card h3{font-size:15px;font-weight:600;letter-spacing:-.01em}
#report .card .cap{color:var(--mute);font-size:13.5px;margin:5px 0 15px;line-height:1.55;
  max-width:65ch}
#report .card svg{display:block;width:100%;height:auto;overflow:visible}
#report .mapwrap svg{overflow:hidden}
#report .axis{font:500 12px var(--mono);fill:var(--mute)}
#report .vlab{font:600 12px var(--mono);fill:var(--dim)}
/* Gallery: row labels read as quiet type, not as headings. */
#report svg.barsh .vlab{font-weight:400}
/* Row rules in an svg chart, matching the criteria table's. */
#report .rowrule{stroke:var(--ink);stroke-opacity:.12;stroke-width:1}
/* The rules run edge to edge inside the card: the svg bleeds into the card's
   20px side padding and the bars are inset back by the same amount in
   viewBox units (see fitBars in charts-core.js). */
#report .card > svg.barsh,#report .card > svg.barsv-bleed{
  margin:0 -20px -15px;width:calc(100% + 40px);max-width:none}
.rowrule{stroke:var(--ink,#051c23);stroke-opacity:.12;stroke-width:1}
/* Column titles and the rules between categories. The rule is the same 35%
   ink as the map's keylines, so the two read as one family. */
#report .colhead{font:600 10.5px var(--mono);fill:var(--mute);
  letter-spacing:.09em;text-transform:uppercase}
/* ── criteria table ───────────────────────────────────────────────────
   Four columns: the criteria, the band, its count, and the population. HTML,
   not svg, so the type is the size it says it is at every card width.
   The rows' rules run to the card's edges — the negative margin is the
   card's own 20px padding — so they read as a table, not as a stack of
   boxes inset from it. */
#report .critab{display:flex;flex-direction:column}
/* The card ends where the table ends: no band of white under the last row's
   tint. :has, so only the card that actually holds this table loses its
   bottom padding. */
#report .card:has(> div > .critab){padding-bottom:0}

#report .critab-head,#report .critab-row{
  display:grid;
  /* The criteria track does not shrink: at minmax(0,150px) it collapsed on a
     narrow card and "Minority, income and English isolation" went to four
     lines, which is what made the rows different heights. */
  /* 146, not 150: the tint starts at this track's edge, so narrowing it by
     4 moves the grey left. The criteria text does not move with it — its
     right padding drops by the same 4 — and the gaps either side of the tint
     come out equal at 16. */
  grid-template-columns:146px minmax(36px,1fr) 44px 52px;
  /* No column gap: the spacing is padding on the cells instead, so the tint
     behind the band and its count is one continuous strip rather than two
     with a stripe of card showing between them. */
  column-gap:0;align-items:center;margin:0 -20px;padding:0 20px}
/* The gap under the head is padding on its CELLS, not on the row: a cell's
   background covers its own padding, so the tint reaches the keyline while
   the text still sits clear of it. On the row it left the tint stopping
   short and the heads sitting above the rule at different heights. */
#report .critab-head{
  letter-spacing:.06em;text-transform:uppercase;align-items:end}
#report .critab-head .c1,#report .critab-head .c4{padding-bottom:9px}
/* The head has three cells in a four-column grid, so auto-placement put
   "Pop." in the count's column. Both are placed explicitly: the band's title
   spans the band and its count — it names both — and Pop. sits over its own
   column, which also stops "Block groups" wrapping into two lines. */
/* No nowrap: on a narrow card the spanned columns are shorter than "BLOCK
   GROUPS" and it ran straight over POP. It wraps instead; the head row is
   the one place where that costs nothing. */
/* The same 9px above the title as below it. The head cells are bottom
   aligned, so the top padding grows the tint upward and leaves the text
   where it is. */
#report .critab-head .c2{text-align:left;grid-column:2/4;line-height:1.2;
  padding:9px 12px}
#report .critab-head .c4{grid-column:4}
/* One height for every row, whether its label takes one line or two. */
#report .critab-row{height:54px;border-top:1px solid rgba(5,28,35,.12)}
/* padding-right, not a narrower track: the text is right-aligned, so this
   moves the whole column 10px left and widens the gap to the bands without
   changing where the bands start. */
#report .critab .c1{text-align:right;font:400 13px var(--body);
  color:var(--ink);line-height:1.3;padding-right:16px}
/* Stretched, so the tint is the full height of the row: grid items are only
   as tall as their content when the row centres them, which made the band a
   floating strip around the bar instead of a column. The cells centre their
   own content instead. */
#report .critab .c2,#report .critab .c3{align-self:stretch;
  display:flex;align-items:center}
#report .critab .c2{padding-left:12px;padding-right:8px}
#report .critab .c3{padding-right:12px;justify-content:flex-end}
#report .critab .c4{padding-left:16px}
/* The block-groups column reads as one field: the band and the count it
   belongs to, tinted away from the card. The heading sits above the field
   rather than inside it, so it takes no tint. */
#report .critab-row .c2,#report .critab-row .c3{background:rgba(5,28,35,.04)}
/* The criteria and the population either side take a lighter tint of the
   same ink, so the three fields read as columns without the middle one
   losing its emphasis. Body rows only — the headings sit above the fields. */
#report .critab-row .c1,#report .critab-row .c4{background:rgba(5,28,35,.02)}
/* Gallery: the label column carries no tint — only the measured columns do. */
#report .critab-row .c1{background:none}
/* Gallery criteria table:
   — a wider gap between the criteria and the band it labels;
   — no fourth track when the rows carry no Pop. cell, so the tint runs to
     the card's edge instead of stopping 52px short of it;
   — and the card ends where the table does (the report's rule looks for
     `> div > .critab`; here the table is the card's own child). */
#report .critab .c1{padding-right:44px}
/* Column titles set as the data table's are: mono, 12px, quiet, wider
   tracking. */
#report .critab-head .c1,
#report .critab-head .c2,
#report .critab-head .c4{font:600 12px var(--mono);letter-spacing:.09em;
  color:var(--mute)}
#report .critab:not(:has(.c4)) .critab-head,
#report .critab:not(:has(.c4)) .critab-row{
  grid-template-columns:210px minmax(36px,1fr) 44px}
#report .card:has(> .critab){padding-bottom:0}
/* The row's own right padding is what kept the tint 20px short of the card:
   it is dropped and the count carries the inset itself. */
#report .critab-head,#report .critab-row{padding-right:0}
#report .critab .c3{padding-right:20px}
/* The figures column of a data table reads as one field, like the band in
   the criteria table: head and cells share the tint. */
/* The tint is the field, not the heading: the head stays on the card and the
   band starts under it, reaching left of the figures the way the block-groups
   band does — the extra width is padding, so the figures do not move. */
/* The figures column is the second one — the last in a two-column table, the
   middle one where there are three. */
#report .card > table td:nth-child(2),
#report .card > div > table td:nth-child(2){background:rgba(5,28,35,.04);
  padding-left:12px}
#report .card > table th:nth-child(2),
#report .card > div > table th:nth-child(2){padding-left:12px}
/* and the column after it clears the tint by the same 12px its own column
   leaves inside it */
#report .card > table td:nth-child(3),#report .card > table th:nth-child(3),
#report .card > div > table td:nth-child(3),
#report .card > div > table th:nth-child(3){padding-left:12px}
/* and, as in the criteria table, the card ends where the table ends, so the
   tint runs to the bottom edge instead of stopping above a white band. The
   corners are kept by clipping the card, which has nothing overflowing it. */
#report .card:has(> table),#report .card:has(> div > table){
  padding-bottom:0;overflow:hidden}
/* Gallery cards are plain white fields: no border, no shadow. */
#report .card{border-color:transparent;box-shadow:none}
/* Maps as the report's Environmental Justice map is set: fixed-width key
   tiles rather than tiles stretched across the card, and the drawing running
   out past the column on both sides. */
#report .gal .secmap .step i{width:56px;flex:none}
#report .gal .secmap .steps-row.spread{flex:none;gap:10px}
#report .gal .secmap .steps-row.spread .step{flex:none;width:56px}
#report .gal .secmap .mapwrap{width:calc(100% + 96px);margin:0 -48px}
#report .critab .c2 i{display:block;height:10px;border-radius:5px;flex:none}
#report .critab .c3,#report .critab .c4{
  font:500 11.5px var(--mono);color:var(--mute);white-space:nowrap}
/* Left-aligned, so the header sits directly over the first digit of the
   numbers rather than over their ragged right edge. */
#report .critab .c4{text-align:left}
/* The three column titles are one set, in the body face rather than two of
   them in the numbers' mono. Set 1.5px under the criteria they head: the
   titles are uppercase, and uppercase at the same point size reads a size
   larger than the mixed-case rows beneath it. */
#report .critab-head .c1,
#report .critab-head .c2,
#report .critab-head .c4{font:600 11.5px var(--body);color:var(--ink)}
#report .critab-head .c4{text-align:left}
/* Isolating a series on hover swaps colours on the shapes; without this it
   snapped. A short cross-dissolve on fill, stroke and opacity covers every
   chart at once — bars, rings, lines and the gradient areas, which fade
   their opacity instead because a gradient fill cannot be tweened. */
#report .card svg path,#report .card svg rect,#report .card svg circle{
  transition:fill .22s ease,stroke .22s ease,opacity .22s ease}
/* ── charts arrive as you scroll ──────────────────────────────────────
   The observer in planning.js adds .in on first crossing; everything else
   is here. transform-box:fill-box so an svg rect scales from its own edge
   rather than from the svg's origin. */
@media (prefers-reduced-motion:no-preference){
  #report rect.hbar,#report rect.vbar,#report .critab .c2 i{
    transform-box:fill-box;
    transition:transform .55s cubic-bezier(.22,1,.36,1),
               fill .22s ease,stroke .22s ease,opacity .22s ease}
  #report rect.hbar{transform-origin:left center;transform:scaleX(0)}
  #report rect.hbar-l{transform-origin:right center}
  #report rect.vbar{transform-origin:center bottom;transform:scaleY(0)}
  #report .critab .c2 i{transform-origin:left center;transform:scaleX(0);
    transition-duration:.5s}
  #report .card.in rect.hbar,#report .card.in rect.vbar,
  #report .critab.in .c2 i{transform:none}
  /* Row by row rather than all at once. */
  #report .card rect.hbar:nth-of-type(2){transition-delay:.05s}
  #report .card rect.hbar:nth-of-type(3){transition-delay:.1s}
  #report .card rect.hbar:nth-of-type(4){transition-delay:.15s}
  #report .card rect.hbar:nth-of-type(5){transition-delay:.2s}
  #report .card rect.hbar:nth-of-type(n+6){transition-delay:.25s}
  #report .critab-row:nth-child(3) .c2 i{transition-delay:.05s}
  #report .critab-row:nth-child(4) .c2 i{transition-delay:.1s}
  #report .critab-row:nth-child(5) .c2 i{transition-delay:.15s}
  #report .critab-row:nth-child(n+6) .c2 i{transition-delay:.2s}
  /* The fill under the line arrives just behind it, and the markers last —
     the shape is read before the points on it. A gradient fill cannot be
     tweened, so this is opacity, which is also what the key's isolation
     uses on these paths (as an inline style, which outranks this). */
  #report .card svg path[fill^="url("]{opacity:0;
    transition:opacity .6s ease .2s}
  #report .card.in svg path[fill^="url("]{opacity:1}
  #report .card svg circle{opacity:0;transition:opacity .4s ease .55s}
  #report .card.in svg circle{opacity:1}
  /* The line draws itself; pathLength:1 makes the dash maths unit-length. */
  #report .linepath{stroke-dasharray:1;stroke-dashoffset:1;
    transition:stroke-dashoffset 1.1s ease}
  #report .card.in .linepath{stroke-dashoffset:0}
  /* Donut slices and the maps just arrive. */
  #report .card svg path.bar{opacity:0;
    transition:opacity .5s ease,fill .22s ease}
  #report .card.in svg path.bar{opacity:1}
  #report .mapwrap{opacity:0;transform:translateY(6px);
    transition:opacity .6s ease,transform .6s cubic-bezier(.22,1,.36,1)}
  #report .mapwrap.in{opacity:1;transform:none}
  /* The stat boxes lift in together with the chart beside them. */
  #report .stats .stat{opacity:0;transform:translateY(8px);
    transition:opacity .45s ease,transform .45s cubic-bezier(.22,1,.36,1)}
  #report .stats.in .stat{opacity:1;transform:none}
  #report .stats.in .stat:nth-child(2){transition-delay:.06s}
  #report .stats.in .stat:nth-child(3){transition-delay:.12s}
  #report .stats.in .stat:nth-child(4){transition-delay:.18s}
}
#report .gl{stroke:var(--line-soft);stroke-width:1}
#report .bar{transition:opacity .15s}
#report .bar:hover{opacity:.75}
/* A key that sits above its chart carries the gap below it instead. */
#report .legend.legend-top{margin:0 0 var(--sp-s)}
#report .legend{display:flex;flex-wrap:wrap;gap:14px;margin-top:13px;
  font:500 12px var(--mono);color:var(--mute)}
#report .legend i{width:10px;height:10px;border-radius:3px;display:inline-block;
  margin-right:6px;vertical-align:-1px}
/* Gallery: round key swatches, no keyline — a dot reads as a series marker
   where a rounded square reads as a chip. */
#report .legend i,#report .chips i{border-radius:50%;border:0;
  box-shadow:none}
#report .mapwrap{position:relative}
/* The dividers between areas match the city outline: same ink, same 35% —
   stroke-opacity rather than opacity, because opacity would fade the fill
   with it and wash the whole map out. */
#report .mapwrap svg path{stroke:var(--ink);stroke-opacity:.35;
  stroke-width:.5;transition:.12s}
#report .mapwrap svg path:hover{stroke:var(--ink);stroke-opacity:1;
  stroke-width:1.1}
#report .mapwrap svg path.outline{fill:none;stroke:var(--ink);stroke-width:1.2;
  opacity:.35;pointer-events:none}
/* No fill here: it is set as an attribute so the stepped key can dim the
   base along with the areas. A fill in CSS would override that attribute. */
#report .mapwrap svg path.mapbase{stroke:none;pointer-events:none}
#report .scale{display:flex;align-items:center;gap:9px;margin:0 0 12px;
  font:500 12px var(--mono);color:var(--mute)}
#report .ramp{flex:1;height:8px;border-radius:5px;min-width:80px}
/* The banded key: one segment per class the map actually paints, each of
   them a control that isolates its class. */
#report .ramp.banded{display:flex;gap:2px;height:10px;background:none}
#report .ramp.banded i{flex:1;height:100%;border-radius:2px;cursor:pointer;
  transition:background .15s}
#report .ramp.banded i:first-child{border-radius:5px 2px 2px 5px}
#report .ramp.banded i:last-child{border-radius:2px 5px 5px 2px}
#report .ramp.banded i:focus-visible{outline:2px solid var(--mint);
  outline-offset:3px}
/* The bands not being hovered take the same grey the map takes.
   #e3e7ea matches the DIM constant in planning.js. */
#report .ramp.banded.focused i:not(.on){background:#e3e7ea!important}
#report .tip{position:absolute;pointer-events:none;background:#051c23;
  border:1px solid #0b3a5e;border-radius:8px;padding:7px 10px;
  font:500 12px var(--mono);color:#ffffff;opacity:0;transition:opacity .12s;
  z-index:9;white-space:nowrap;box-shadow:0 6px 20px rgba(5,28,35,.28)}
#report table{width:100%;border-collapse:collapse;font-size:13.5px;
  font-variant-numeric:tabular-nums}
#report th{text-align:left;font:600 12px var(--mono);letter-spacing:.09em;
  text-transform:uppercase;color:var(--mute);padding:0 12px 9px 0;
  border-bottom:1px solid var(--line-soft)}
#report td{padding:9px 12px 9px 0;border-bottom:1px solid var(--line-soft);color:var(--dim)}
#report td:first-child{color:var(--ink);font-weight:500}
#report td.n{font-family:var(--mono);color:var(--accent)}
#report tr:last-child td{border-bottom:none}
/* The row rules run to the card's edges rather than stopping at its padding,
   so a table reads as a table and not as a stack of rows inset from the box.
   The negative margin is the card's own 20px side padding, given back to the
   first and last cells so the text still lines up with the title above it.
   .critab-look sets its own version of this below. */
#report .card > table,#report .card > div > table{
  margin:0 -20px;width:calc(100% + 40px)}
#report .card > table th:first-child,#report .card > table td:first-child,
#report .card > div > table th:first-child,
#report .card > div > table td:first-child{padding-left:20px}
#report .card > table th:last-child,#report .card > table td:last-child,
#report .card > div > table th:last-child,
#report .card > div > table td:last-child{padding-right:20px}
/* ── a table presented like the criteria table ────────────────────────
   Same heads, same full-bleed row rules, same quiet numbers. The rules run
   to the card's edges — the negative margin is the card's own 20px padding —
   so it reads as a table rather than a stack of rows inset from it. */
#report .critab-look table{margin:0 -20px;width:calc(100% + 40px)}
#report .critab-look th{font:600 11.5px var(--body);letter-spacing:.06em;
  text-transform:uppercase;color:var(--ink);border-bottom:0;
  padding:0 12px 9px 0}
#report .critab-look thead tr{border-bottom:1px solid rgba(5,28,35,.12)}
#report .critab-look td{border-bottom:0;border-top:1px solid rgba(5,28,35,.12);
  height:54px;padding:0 12px 0 0}
#report .critab-look th:first-child,
#report .critab-look td:first-child{padding-left:20px}
#report .critab-look th:last-child,
#report .critab-look td:last-child{padding-right:20px}
#report .critab-look td.n{font:500 11.5px var(--mono);color:var(--mute)}
#report .critab-look td:first-child{font:400 13px var(--body);color:var(--ink)}

#report .src{font:500 12px/1.7 var(--mono);color:var(--mute);margin:var(--sp-s) 0 0;
  padding-top:15px;border-top:1px solid var(--line-soft)}
/* At the foot of the module, under its own keyline, across the full width —
   the grid placements above no longer apply because it has left the grid. */
#report .statmod-in > .src.srcfoot{margin:var(--sp-m) 0 0;padding-top:15px}
#report .src b{color:var(--dim);font-weight:600;text-transform:uppercase;
  letter-spacing:.09em;font-size:12px}
#report .note{color:var(--mute);font-size:13.5px;line-height:1.65;margin:10px 0 0;
  max-width:65ch;font-family:var(--body)}

/* The requirement and the methodology note are the two halves of the same
   question — what the Commonwealth asks for, and what this was built from.
   They are the first row of .secgrid (above); these rules are what they look
   like, wherever they sit. */
/* No panel and no keyline — the label alone carries the block. The 18px
   left padding went with the keyline: without a rule to sit against it read
   as an unexplained indent, so the text starts on the section's own left
   edge, in line with the standfirst above it.
   Scoped so #validation's .req keeps its keyline on the dark band. */
#report .secgrid .req{border-left:0;background:none;border-radius:0;padding:0}
#report .reqside{min-width:0;padding-top:0}
#report .reqside .note{margin-top:0}
/* The methodology note is a list: markers drawn as dots rather than
   list-style discs, which sit too large and too high against 13.5px text. */
#report ul.note{list-style:none;padding:0}
#report ul.note li{position:relative;padding-left:15px;margin:0 0 9px}
#report ul.note li:last-child{margin-bottom:0}
#report ul.note li::before{content:"";position:absolute;left:2px;top:.62em;
  width:4px;height:4px;border-radius:50%;background:currentColor;opacity:.55}
/* Set like the requirement's own label so the two column titles match. */
#report .sidehead{font:600 13.5px/1 var(--display);letter-spacing:.075em;
  text-transform:uppercase;color:var(--mint);display:block;margin:0 0 14px}
#report footer{padding:var(--sp-m) 0 var(--sp-l);color:var(--mute);font-size:13.5px;line-height:1.7}
#report footer strong{color:var(--dim)}
#report .hero{padding:var(--sp-m) 0 var(--sp-m)}
#report section.an{padding:var(--sp-m) 0 var(--sp-s)}
#report .anhead{gap:13px}
#report .pcard .req { color: #939cad; font-size: 13.5px; margin: 0 0 8px; }
#report .pcard .have { font-size: 13.5px; margin: 0; }
#report .pcard .have b { color: #63cfff; font-weight: 600; }
#report .valblock {
  margin-top: 26px; background: #161b24; border: 1px solid #232a36;
  border-left: 3px solid #63cfff; border-radius: 10px; padding: 16px 18px;
}
#report .valblock h3 { font-size: 17px; margin: 0 0 6px; }
#report .valtab { border-collapse: collapse; margin: 10px 0; font-size: 13.5px;
  font-variant-numeric: tabular-nums; }
#report .valtab th { text-align: left; color: #939cad; font-weight: 500;
  padding: 2px 18px 4px 0; }
#report .valtab td { padding: 4px 18px 4px 0; border-top: 1px solid #232a36; }
#report .valchart { display: flex; align-items: flex-end; gap: 3px;
  height: 90px; margin-top: 14px; }
#report .vb { flex: 1; display: flex; align-items: flex-end; gap: 1px;
  position: relative; height: 100%; }
#report .vb i { display: block; width: 50%; border-radius: 2px 2px 0 0; }
#report .vb .s { background: #63cfff; }
#report .vb .c { background: #939cad; }
#report .vb b { position: absolute; bottom: -16px; left: 0; right: 0;
  text-align: center; font-size: 12px; color: var(--text-muted); font-weight: 400; }
#report .ssw, #report .csw { display: inline-block; width: 10px; height: 10px;
  border-radius: 2px; vertical-align: -1px; }
#report .ssw { background: #63cfff; }
#report .csw { background: #939cad; }
#report .whocard {
  background: #10151d; border: 1px solid #232a36; border-radius: 8px;
  padding: 12px 14px; margin: 12px 0 4px; font-size: 13.5px; color: var(--on-dark-body);
}
#report .whocard b.hl { color: #e69f00; }

/* site header + report shell (merged page) */
#sitehead{position:sticky;top:0;z-index:60;height:var(--sitehead-h);
  background:rgba(5,28,35,.94);backdrop-filter:blur(12px);
  border-bottom:1px solid #093046}
#sitehead .shin{display:flex;align-items:center;gap:10px;height:var(--sitehead-h);
  max-width:1400px;margin:0 auto;padding:0 16px}
#sitehead .sbrand{display:flex;align-items:center;gap:9px;white-space:nowrap;
  flex:none;font-family:var(--display);font-weight:700;
  font-size:15px;letter-spacing:-.02em;color:#fff}
#sitehead .sbrand span{color:var(--brand-accent);font-weight:400}
#sitehead .sdot{width:9px;height:9px;border-radius:50%;background:#9edfff;
  box-shadow:0 0 12px rgba(158,223,255,.65)}
#snav{margin-left:auto;display:flex;gap:2px;overflow-x:auto;scrollbar-width:none}
#snav::-webkit-scrollbar{display:none}
#snav a{font:500 12px/1 var(--mono);color:var(--on-dark-muted);
  text-decoration:none;padding:6px 6px;border-radius:6px;white-space:nowrap;
  transition:.15s}
#snav a:hover{color:#9edfff;background:#082b38}
#snav a.on{color:#051c23;background:#9edfff}
#report{background:var(--ground);color:var(--text);font:15px/1.65 var(--body)}
#report .rintro{padding:var(--sp-l) 0 var(--sp-m);background:#051c23;color:#fff;
  position:relative;overflow:hidden}
#report .rintro::after{content:"";position:absolute;inset:0;pointer-events:none;
  background:radial-gradient(760px 340px at 82% -8%,rgba(158,223,255,.13),transparent 68%),
             radial-gradient(520px 280px at 8% 108%,rgba(158,223,255,.07),transparent 70%)}
#report .rin{position:relative;z-index:1;max-width:1180px;margin:0 auto;padding:0 24px}
#report .rintro h1{color:#fff;font-family:var(--display);font-weight:400;
  font-size:32px;line-height:1.15;max-width:30ch;font-weight:400;
  letter-spacing:-.02em;margin:0;text-wrap:balance}
#report .rintro h1 em{font-style:normal;color:#9edfff}
#report .rlede{color:var(--brand-accent);font-size:17px;max-width:65ch;margin:20px 0 0}
#report .rstats{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));
  gap:1px;margin-top:42px;background:#093046;border:1px solid #093046;
  border-radius:14px;overflow:hidden}
#report .rstats div{background:var(--overlay-ground);padding:17px 18px}
#report .rstats b{color:#fff}
#report .rstats b{display:block;font-family:var(--display);
  font-size:32px;font-weight:700;line-height:1.1;letter-spacing:-.03em;color:#fff}
#report .rstats span{display:block;font:500 12px/1.4 "JetBrains Mono",monospace;
  color:var(--on-dark-muted);text-transform:uppercase;letter-spacing:.07em;margin-top:7px}
#report .rwrap{max-width:1180px;margin:0 auto;padding:0 24px}
#report .an{border-bottom:1px solid #d5dbdf}
#report .an:last-of-type{border-bottom:none}
/* White, edge to edge, and no rule: the footer is its own band at the foot of
   the page rather than a note ruled off from the last section. The band is a
   pseudo-element so the text keeps the 1200px column. */
#report .rfoot{padding:var(--sp-m) 0 var(--sp-m);color:#6a828e;font-size:13.5px;
  line-height:1.7;border-top:0;position:relative}
#report .rfoot::before{content:"";position:absolute;inset:0;left:50%;
  width:100vw;margin-left:-50vw;background:#fff;z-index:0}
#report .rfoot > p{position:relative;z-index:1}
#report .rfoot strong{color:#051c23}
/* Three columns, one note each: they are three separate statements, not one
   argument, and at 65ch in a single column the last one sat a screen below
   the first. Below 900px they stack in the order they are written. */
#report .rfoot{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));
  gap:var(--sp-m)}
#report .rfoot p{max-width:none;margin:0}
@media (max-width:899px){
  #report .rfoot{grid-template-columns:minmax(0,1fr);gap:14px}
  #report .rfoot p{max-width:65ch}
}
/* Full bleed: the block spans the viewport and its own inner container
   holds the text to the report's measure. No radius and no side margin —
   a rounded corner or a gutter is what would give away that it is a card
   sitting on the page rather than a band the page ends with. */
#report #validation{background:#051c23;color:#fff;border-radius:0;
  padding:64px 0 72px;margin:0;border-bottom:none;width:100%}
#report #validation .valinner{max-width:1180px;margin:0 auto;padding:0 24px}
#report #validation h2{color:#fff}
#report #validation .ansub{color:var(--brand-accent)}
#report #validation .annum{font-size:17px;background:#9edfff;color:#051c23}
#report #validation .valblock{margin:0;background:transparent;border:none;
  padding:0}
#report #validation .valblock h3{color:#fff}
#report #validation .req,#report #validation .have{color:var(--brand-accent)}
#report #validation .req b,#report #validation .have b{color:#9edfff}
#report #validation .valtab th{color:var(--on-dark-muted);border-color:#0b3a5e}
#report #validation .valtab td{color:var(--on-dark-body);border-color:#0b3a5e}
#report #validation .whocard{background:var(--overlay-ground);border-color:#0b3a5e;
  color:var(--on-dark-body)}
@media(max-width:720px){
  #snav{display:none}
  #report .rintro{padding:44px 0 34px}
}

/* ── collapsible panel sections (v9) ───────────────────────────────────── */
/* the keylines run the full width of the panel, under its 16px padding */
.psec {
  border-top: 1px solid #e2e6ea;
  margin: 0 -16px; padding: 0 16px;
}
/* an open section sits on the same grey as the chat log. The body's bottom
   margin escapes the box (it collapses out through ::details-content), so it
   becomes padding here — otherwise the grey stops short of the keyline. */
.psec[open] { background: #f5f6f8; padding-bottom: 14px; }
/* People opens and closes on a height ramp rather than snapping. Chrome needs
   interpolate-size to animate to `auto`, and allow-discrete to keep the slot
   visible for the length of the close. Other engines just snap, as before. */
@supports (interpolate-size: allow-keywords) {
  :root { interpolate-size: allow-keywords; }
  #people-sec { transition: background .25s ease, padding-bottom .25s ease; }
  #people-sec::details-content {
    block-size: 0; overflow: hidden; opacity: 0;
    transition: block-size .3s cubic-bezier(.22,.61,.36,1), opacity .22s ease,
                content-visibility .3s allow-discrete;
  }
  #people-sec[open]::details-content { block-size: auto; opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  #people-sec, #people-sec::details-content { transition: none; }
}
.psec[open] > .ctrl-group { margin-bottom: 0; }
.psec summary {
  cursor: pointer; list-style: none; display: flex; align-items: center;
  gap: 6px; padding: 8px 2px; font-weight: 600; font-size: 13.5px;
  color: var(--brand-deep); user-select: none;
}
.psec summary::-webkit-details-marker { display: none; }
.psec summary::after { margin-left: auto; }
.psec summary .chk-label { font-weight: 600; margin: 0; }
.psec > .ctrl-group { margin-top: 0; padding-top: 2px; }

/* ── grouped activity key (v9) ─────────────────────────────────────────── */
.key-cat { list-style: none; margin: 1px 0; }
.key-cat-head {
  display: flex; align-items: center; gap: 7px;
  padding: 0; border-radius: 4px;
}
.key-cat-head:hover { background: rgba(128, 128, 128, 0.12); }
/* The row's name is a real button, so it can be reached and operated from
   the keyboard; [+] and the chevron are its siblings rather than its
   children, because a button may not contain a button. */
.key-main {
  flex: 1 1 auto; min-width: 0; display: flex; align-items: center; gap: 7px;
  background: none; border: 0; padding: 2px 0; margin: 0; border-radius: 4px;
  font: inherit; color: inherit; text-align: left; cursor: pointer;
}
.key-tick { margin-left: auto; font-size: 12px; }
.key-kids { list-style: none; margin: 1px 0 3px 14px; padding: 0; }
.key-kids li {
  display: flex; align-items: center; gap: 7px; padding: 1px 0;
  font-size: 12px; opacity: 0.92;
}
#map-key summary.map-key-title {
  cursor: pointer; list-style: none; user-select: none;
  display: flex; align-items: center;
}
#map-key summary.map-key-title::-webkit-details-marker { display: none; }
#map-key summary.map-key-title::after {
  content: "▾"; margin-left: 8px; font-size: 12px; opacity: 0.6;
}
#map-key details:not([open]) summary.map-key-title::after { content: "▸"; }
#map-key .key-kids { max-height: 180px; overflow-y: auto; }
#map-key-list { max-height: 46vh; overflow-y: auto; }


/* activity filter selection */
.key-cat-head.sel, .key-kids li.sel {
  background: rgba(158, 223, 255, 0.22); border-radius: 4px;
  outline: 1px solid rgba(158, 223, 255, 0.55);
}
/* panel list: flat rows. The tick alone carries the selected state — no
   band, and no chip behind the ✓/+ control. The map key keeps both. */
#activity-legend .key-cat-head.sel, #activity-legend .key-kids li.sel {
  background: none; outline: 0;
}
/* the ✓/+ control is gone from the panel list (the map key keeps it) */
#activity-legend .key-add { display: none; }
/* second level indented from People, third level again from its category */
#activity-legend .key-cat { padding-left: 18px; }
/* the category counts share one right edge, so they read as a column */
#activity-legend .key-main .key-count { margin-left: auto; padding-left: 8px; }
/* selected-of-total counts, beside People and beside each category */
.key-count {
  font-size: 11px; font-weight: 600; color: var(--brand, #064d90);
  font-variant-numeric: tabular-nums; margin-left: 6px; flex: none;
  /* the rows centre their items, which drops an 11px figure below a 13.5px
     label; this puts the two back on one baseline */
  position: relative; top: -2px;
}
.key-kids li:hover { background: rgba(128, 128, 128, 0.12); border-radius: 4px; }
.key-kids .key-main { font-size: 12px; }
.key-clear { list-style: none; margin-bottom: 4px; }
.key-clear-btn {
  display: block; width: 100%; cursor: pointer; font-weight: 600;
  padding: 3px 6px; border-radius: 5px; border: 0; text-align: left;
  font-family: inherit; font-size: inherit; color: inherit;
  background: rgba(255, 179, 92, 0.18); outline: 1px solid rgba(255, 179, 92, 0.5);
}
.key-clear-btn:hover { background: rgba(255, 179, 92, 0.3); }
.key-tick {
  padding: 0 4px; cursor: pointer; background: none; border: 0;
  font: inherit; color: inherit; line-height: 1;
}

/* [+] add-to-selection control on key rows */
.key-add {
  margin-left: auto; width: 16px; height: 16px; line-height: 15px;
  text-align: center; border-radius: 4px; font-weight: 700; font-size: 12px;
  background: rgba(128, 128, 128, 0.18); cursor: pointer; flex: none;
  opacity: 0.55; border: 0; padding: 0; color: inherit;
  font-family: inherit;
}
.key-cat-head:hover .key-add, .key-kids li:hover .key-add { opacity: 1; }
.key-add:hover { background: rgba(158, 223, 255, 0.45); }
.sel .key-add { opacity: 1; background: rgba(158, 223, 255, 0.4); }
/* the chevron follows the [+]; stop it stretching to the far edge */
.key-tick { margin-left: 4px; }

/* the same chevron as the section headers (assets/Group 3.svg): the "▾"/"▸"
   characters the JS writes are hidden, and aria-expanded drives the rotation.
   Closed points down, open points up — matching .psec > summary::after. */
.key-tick { font-size: 0; display: flex; align-items: center; }
.key-tick::after {
  content: ""; width: 12px; height: 12px; flex: none;
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 109 63' fill='none' stroke='%2362737d' stroke-width='16' stroke-linecap='round'%3E%3Cpath d='M3.5 3.5L54.5 59.5'/%3E%3Cpath d='M54.5 59.5L105.5 3.5'/%3E%3C/svg%3E")
    center / 11px 7px no-repeat;
  transition: transform .15s;
}
.key-tick[aria-expanded="true"]::after { transform: rotate(180deg); }

/* building hover card */
#maptip.bldg { pointer-events: auto; width: 286px; max-width: 286px; }
.rc-sub-inline { color: #8fb0c9; font-weight: 400; font-size: 12px; }
.rc-resident {
  background: rgba(158, 223, 255, 0.12); color: #9edfff;
  border: 1px solid rgba(158, 223, 255, 0.26);
  border-radius: 5px; padding: 2px 7px; font-size: 12px;
  cursor: pointer; font-family: inherit;
}
.rc-resident:hover { background: rgba(158, 223, 255, 0.3); color: #fff; }

/* development-pipeline card bits */
.fut-chip {
  display: inline-block;
  padding: 0 7px;
  border: 1px solid;
  border-radius: 9px;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: .03em;
  text-transform: uppercase;
}
.fut-link { color: #9edfff; text-decoration: underline; }
#future-legend { list-style: none; margin: 2px 0 4px 22px; padding: 0; }
#future-legend .legend-row { font-size: 12px; }

/* park hover label — glanceable, non-interactive */
#maptip.park { pointer-events: none; width: auto; max-width: 300px; }
#maptip.park .rc-head b { font-size: 13.5px; }

/* pack 02: the report gets a real container — sections were running the
   full monitor width — and anything wide scrolls inside itself */
#report-main { max-width: 1200px; margin-inline: auto; padding-inline: 32px; }
/* wide things scroll inside themselves — the validation table lives outside
   #report-main, so target every report table */
#report table { display: block; overflow-x: auto; max-width: 100%; }
/* …except a card table, which is deliberately wider than its content box so
   its row rules reach the card's edges. max-width:100% above resolves
   against that content box and was clamping the width:calc() back. */
#report .card > table,#report .card > div > table{max-width:none}
/* Gallery: the row rules run the card's full width. `display:block` (the
   mobile scroll shim) makes the table shrink-wrap its cells, so width:100%
   never reaches the edges — these tables fit, so they stay real tables. */
#report .card > table,#report .card > div > table{display:table;
  /* the rows share out a stretched card, so the tinted column reaches the
     card's bottom edge instead of stopping above a white band */
  height:100%}

/* pack 02: the header persists for the whole page now, so every anchor
   target needs to land below it */
#top, #validation, #gis, #demography, #economy, #history, #projection,
#households, #environment, #service, #market, #housing {
  scroll-margin-top: 64px;
}

/* pack 02: the brand routes back to the simulation */
#sitehead a.sbrand { color: var(--on-dark); text-decoration: none; border-radius: 7px; }
#sitehead a.sbrand:hover span { color: var(--brand-accent); }
#sitehead a.sbrand:focus-visible {
  outline: 2px solid var(--brand-accent); outline-offset: 3px;
}
/* Validation is not one of the ten numbered analyses — separate it */
#snav a[data-id="validation"] {
  margin-right: 12px; padding-right: 14px;
  border-right: 1px solid rgba(158,223,255,.22); border-radius: 7px 0 0 7px;
}
/* 12. the nav scrolls horizontally when narrow — show it */
#snav { scrollbar-width: thin; scrollbar-color: rgba(158,223,255,.35) transparent; }
#snav::-webkit-scrollbar { display: block; height: 4px; }
#snav::-webkit-scrollbar-thumb { background: rgba(158,223,255,.35); border-radius: 2px; }

/* pack 03: the click affordance, said out loud (copy pending Emma) */
.click-hint { color: var(--text-muted); font-size: 12px; margin: 6px 0 2px; }

/* pack 03b: the footer shares the container edge with the SOURCES lines */
#report .rfoot { max-width: 1200px; margin-inline: auto; padding-inline: 32px; }
/* pack 03b: three-tier panel title */
.brand-sub { font-weight: 600; font-size: 12px; color: var(--text-body); margin: 1px 0 2px; }

/* ── Ask the map: the chat assistant (internal tool) ──────────────────── */
#chat-log {
  max-height: 260px; overflow-y: auto; margin-bottom: 8px;
  scrollbar-width: thin; scrollbar-color: #c3ccd3 transparent;
}
.chat-msg {
  padding: 7px 9px; border-radius: 8px; margin-bottom: 6px;
  font-size: 12px; line-height: 1.5; white-space: pre-wrap;
}
.chat-user { background: var(--brand-soft); color: var(--text); }
.chat-assistant { background: #f2f4f6; color: var(--text-body); }
.chat-pending { color: var(--text-muted); }
.chat-error { background: #fdecef; color: #8c1230; }
/* the measurement trail: which tool produced the figure, so an answer is
   auditable rather than merely confident */
.chat-tool {
  font: 500 12px/1.5 var(--mono); color: var(--text-muted);
  padding: 1px 0 3px 9px; border-left: 2px solid var(--brand-soft);
  margin: -2px 0 6px 2px;
}
.chat-egs { display: flex; flex-direction: column; gap: 4px; margin-bottom: 8px; }
.chat-eg {
  text-align: left; padding: 5px 8px; font-size: 12px; cursor: pointer;
  border: 1px solid var(--border); border-radius: 7px;
  background: #fff; color: var(--brand);
}
.chat-eg:hover { background: var(--brand-soft); }
#chat-form { display: flex; gap: 6px; }
/* an author display rule outranks the UA's [hidden] rule, so the
   form stayed visible with hidden=true until this was added */
#chat-form[hidden], #chat-key-wrap[hidden] { display: none; }
#chat-input { flex: 1; min-width: 0; padding: 6px 9px; font-size: 12px;
  border: 1px solid #ccc; border-radius: 7px; }
#chat-form button, #chat-key-save {
  padding: 6px 11px; font-size: 12px; cursor: pointer;
  border: 1px solid var(--brand); border-radius: 7px;
  background: var(--brand); color: #fff;
}
#chat-key { width: 100%; box-sizing: border-box; padding: 6px 9px;
  font-size: 12px; border: 1px solid #ccc; border-radius: 7px;
  margin-bottom: 6px; }

/* ── wave 1: keyboard and small screens ─────────────────────────────── */
/* The first focusable element on the page. 33 controls sit between the
   top of the document and the analyses. */
#skip-link {
  position: absolute; left: 8px; top: -60px; z-index: 100;
  background: var(--surface); color: var(--brand);
  border: 1px solid var(--brand); border-radius: 8px;
  padding: 9px 14px; font: 600 13.5px/1 var(--body); text-decoration: none;
}
#skip-link:focus { top: 8px; }

/* The size belongs on the base rule, not only inside the media query: the
   note is hidden above 640px but still computes a font-size, and leaving it
   unset put an off-scale 16px into pack 02's six-step type scale. */
#small-screen-note { display: none; font-size: 13.5px; }
@media (max-width: 640px) {
  /* A phone used to get the map covered by a 332px panel, no navigation and
     no explanation. Say so instead, and leave the analyses — which read
     perfectly well on a phone — reachable below. */
  #small-screen-note {
    display: block; margin: 0; padding: 14px 18px;
    background: var(--brand-soft); color: var(--text);
    line-height: 1.5;
    border-bottom: 1px solid var(--border);
  }
  #map-wrap { height: 42vh; min-height: 0; }
  #panel, #maptip { display: none !important; }
}

/* Focus has to be visible: the key is 126 controls deep, and without this a
   keyboard user cannot tell which one they are on. */
#panel .key-main:focus-visible, #panel .key-add:focus-visible,
#panel .key-tick:focus-visible, #panel .key-clear-btn:focus-visible,
#map-key .key-main:focus-visible, #map-key .key-add:focus-visible,
#map-key .key-tick:focus-visible, #map-key .key-clear-btn:focus-visible {
  outline: 2px solid var(--brand-line); outline-offset: 1px; border-radius: 4px;
}
.key-title {
  font-size: 13.5px; font-weight: 600; margin: 10px 0 2px; letter-spacing: -.01em;
}
.key-hint { font-size: 12px; color: var(--mute); margin: 0 0 6px; line-height: 1.45; }

/* select all / deselect all, above the activity list. Panel only — the map
   key hides its hint and title too. */
.key-bulk { display: flex; gap: 6px; margin: 0 0 8px; }
.key-bulk-btn {
  flex: 1 1 0; min-width: 0; cursor: pointer;
  font-family: inherit; font-size: 12px; font-weight: 600; color: inherit;
  padding: 4px 6px; border-radius: 6px; border: 1px solid #dfe5e9;
  background: #fff;
  transition: background .15s, border-color .15s;
}
.key-bulk-btn:hover { background: var(--brand-soft, #cdeeff); border-color: var(--brand-soft, #cdeeff); }
.key-bulk-btn:focus-visible { outline: 2px solid var(--brand-line); outline-offset: 1px; }
.key-bulk-btn[disabled] { opacity: .4; cursor: default; background: #fff; border-color: #dfe5e9; }
#map-key .key-bulk { display: none; }
#map-key .key-title, #map-key .key-hint { display: none; }

/* ── wave 3: masthead logo, back-to-top, linked sources ──────────────── */
#sitehead .sbrand .slogo { height: 19px; width: auto; display: block; }
#sitehead .sbrand:focus-visible {
  outline: 2px solid var(--brand-accent); outline-offset: 3px; border-radius: 4px;
}

/* The wordmark used to be the only way back to the top of the page; it now
   links to cambium.ai, so this ships with it rather than after it. */
#backtotop {
  position: fixed; right: 18px; bottom: 18px; z-index: 70;
  display: flex; align-items: center; gap: 6px;
  padding: 9px 14px; border-radius: 999px;
  background: var(--brand); color: #fff; text-decoration: none;
  font: 600 13.5px/1 var(--body);
  box-shadow: 0 3px 14px rgba(5, 28, 35, 0.28);
}
/* display:flex above outranks the UA's [hidden] rule, which would leave the
   control on screen permanently — say it explicitly. */
#backtotop[hidden] { display: none; }
#backtotop:hover { background: #053a6d; }
#backtotop:focus-visible { outline: 2px solid var(--brand-line); outline-offset: 2px; }

/* ── feedback ─────────────────────────────────────────────────────────
   A pill at the foot of the window, to the RIGHT of Back to top — which
   moves left to make room for it — and the form it opens. */
#backtotop { right: 122px; }
#fbbtn {
  position: fixed; right: 18px; bottom: 18px; z-index: 70;
  padding: 9px 14px; border: 0; border-radius: 999px;
  background: #fff; color: var(--dim); cursor: pointer;
  font: 600 13.5px/1 var(--body);
  box-shadow: 0 3px 14px rgba(5, 28, 35, 0.18);
}
#fbbtn:hover { color: var(--ink); box-shadow: 0 4px 18px rgba(5,28,35,.24); }
#fbbtn[aria-expanded="true"] { color: var(--ink); }
#fbbtn:focus-visible { outline: 2px solid var(--brand-line); outline-offset: 2px; }
#fbpop {
  position: fixed; right: 18px; bottom: 62px; z-index: 71;
  width: 288px; padding: 14px; border-radius: 14px;
  background: #fff; box-shadow: 0 14px 40px rgba(5, 28, 35, 0.22);
  display: flex; flex-direction: column; gap: 6px;
}
#fbpop[hidden] { display: none; }
#fbpop .fblab {
  font: 600 11px var(--mono); letter-spacing: .08em; text-transform: uppercase;
  color: var(--mute);
}
#fbpop select, #fbpop textarea {
  width: 100%; box-sizing: border-box; border: 1px solid var(--line-soft);
  border-radius: 8px; padding: 8px 10px; color: var(--ink);
  font: 400 13px var(--body); background: #fff; resize: vertical;
}
#fbpop select:focus-visible, #fbpop textarea:focus-visible {
  outline: 2px solid var(--brand-line); outline-offset: 0;
}
#fbpop .fbrow { display: flex; align-items: center; justify-content: space-between;
  gap: 10px; margin-top: 4px; }
#fbpop .fbnote { font: 500 11.5px var(--mono); color: var(--mute); }
#fbpop .fbnote.done { color: var(--brand); }
#fbsend {
  border: 0; border-radius: 999px; padding: 8px 16px; cursor: pointer;
  background: var(--brand); color: #fff; font: 600 12.5px var(--body);
}
#fbsend:hover { background: #053a6d; }
#fbsend:focus-visible { outline: 2px solid var(--brand-line); outline-offset: 2px; }
@media (max-width: 640px) {
  /* Back to top keeps the corner; Feedback sits above it rather than on it. */
  #backtotop { right: 12px; bottom: 12px; }
  #fbbtn { right: 12px; bottom: 56px; }
  #fbpop { right: 12px; bottom: 100px; width: calc(100vw - 24px); }
}

/* E11 — sources are deep links to the dataset, dotted so they read as
   citations rather than navigation. Scoped to #report, matching what
   merge_report_into_sim.py step 3 does to the template's own rules. */
#report .src a.srclink {
  color: inherit; text-decoration: underline dotted;
  text-underline-offset: 2px; text-decoration-thickness: 1px;
}
#report .src a.srclink:hover { color: var(--ink); text-decoration-style: solid; }
#report .src a.srclink:focus-visible {
  outline: 2px solid var(--brand-line); outline-offset: 2px; border-radius: 2px;
}

/* E12 — copy a link to one analysis */
#report .anhead { position: relative; }
/* ── Share and save ───────────────────────────────────────────────────
   One surface. .sharebg is the white pill sitting behind the button; opening
   grows its height and relaxes its radius, so the menu reads as the pill
   expanding rather than as a second panel appearing next to it. The button
   and the list ride on top of it. */
/* z-index on the menu itself, not just on its parts: the pill and the list
   share one stacking context that sits above everything in the section, so
   the open menu always overlays the content under it. */
#report .sharemenu { margin-left: auto; flex: none; align-self: center;
  position: relative; z-index: 40; }
#report .sharebg {
  position: absolute; top: 0; right: 0; left: 0; height: 100%;
  background: var(--surface); border: 1px solid var(--line-soft);
  /* 18px, not 999px: on the 35px-tall pill the browser clamps it to 17.5
     and it reads as a capsule, and when the pill grows the corner stays the
     same size — so the bottom half slides down and the straight sides get
     longer, instead of the shape rounding off into a different one. */
  border-radius: 18px; z-index: 0;
  transition: height .26s cubic-bezier(.4,0,.2,1), box-shadow .26s ease;
}
/* The keyline stays soft throughout — darkening it to --mute on hover and
   on open read as a black outline against the white pill. Depth comes from
   the shadow instead. */
#report .sharemenu.open .sharebg { box-shadow: 0 10px 26px rgba(5,28,35,.14); }
#report .sharemenu:hover .sharebg { box-shadow: 0 2px 8px rgba(5,28,35,.07); }
#report .sharebtn {
  position: relative; z-index: 1;
  display: inline-flex; align-items: center; gap: 8px;
  background: none; border: 0; color: var(--dim); cursor: pointer;
  font: 500 12.5px var(--mono); padding: 10px 18px; white-space: nowrap;
}
/* Flex centring lines the icon up with the whole line box; the x-height of
   the label sits a shade lower, so the icon is nudged down to match it. */
#report .sharebtn svg { width: 17px; height: 17px; fill: currentColor;
  flex: none; transform: translateY(1.5px); }
#report .sharebtn:hover, #report .sharemenu.open .sharebtn { color: var(--ink); }
#report .sharebtn:focus-visible {
  outline: 2px solid var(--brand-line); outline-offset: 2px;
  border-radius: 999px;
}
/* In flow under the button so the pill can wrap both, but pulled out of the
   header's height with position:absolute — the head must not grow when the
   menu opens. */
#report .sharepop {
  position: absolute; top: 100%; left: 0; right: 0; z-index: 1;
  padding: 2px 7px 10px; display: flex; flex-direction: column;
  opacity: 0; transform: translateY(-4px);
  transition: opacity .16s ease, transform .2s ease;
  pointer-events: none;
}
#report .sharemenu.open .sharepop { opacity: 1; transform: none;
  pointer-events: auto; transition-delay: .06s; }
#report .shareitem {
  text-align: left; background: none; border: 0; border-radius: 7px;
  color: var(--dim); cursor: pointer; font: 500 12px var(--mono);
  padding: 8px 12px;
}
#report .shareitem:hover:not(:disabled) { background: #eef4fb; color: var(--ink); }
#report .shareitem:disabled { color: var(--mute); opacity: .45;
  cursor: not-allowed; }
#report .shareitem:focus-visible {
  outline: 2px solid var(--brand-line); outline-offset: -2px;
}
#report .shareitem.done { color: var(--brand); }
@media (prefers-reduced-motion: reduce) {
  #report .sharebg, #report .sharepop { transition: none; }
}

@media (max-width: 640px) {
  /* At 375px the copy control took a whole column out of the section head's
     flex row, squeezing "GIS, mapping and spatial analysis" into four
     cramped lines beside it. .anhead is already positioned, so lift the
     button out of the flow and give the title its full width back. */
  #report .anhead { padding-top: 26px; }
  #report .sharemenu { position: absolute; top: 0; right: 0; margin-left: 0; }
}

/* ── wave 5: empty states and first-use glosses ──────────────────────── */
/* E4: a panel with nothing to draw used to render as a bare caption,
   indistinguishable from one still loading. */
#report .empty {
  border: 1px dashed var(--line-soft); border-radius: var(--r);
  padding: 20px; color: var(--mute); font-size: 13.5px;
  text-align: center; line-height: 1.55;
}

/* E7: a gloss on first use of the nine terms a planner outside this office
   would read straight past. The tooltip is drawn with ::after, which
   assistive tech does not read, so .sr-only carries the same words into the
   accessibility tree. Shown on focus as well as hover — a title attribute
   would never reach a keyboard user. */
#report .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;
}
#report .gloss {
  border-bottom: 1px dotted var(--mute); text-decoration: none;
  cursor: help;
}
/* The tooltip is positioned against the PARAGRAPH, not the word. Anchored to
   the word it hung off the right edge whenever the term fell near the end of
   a line — 886px of scrollWidth in a 768px viewport. Against the paragraph it
   cannot leave the content column at any width. */
#report .ansub, #report .req p, #report .card .cap, #report .note,
#report .rlede, #report .rfoot p { position: relative; }
#report .gloss:focus-visible {
  outline: 2px solid var(--brand-line); outline-offset: 2px; border-radius: 2px;
}
/* display:none, not visibility:hidden — a hidden absolutely-positioned box
   still occupies layout, and these tooltips pushed the document's
   scrollWidth to 613px at a 375px viewport, giving the whole page a
   horizontal scrollbar at rest. */
#report .gloss::after {
  content: attr(data-gloss); display: none; position: absolute; left: 0;
  top: calc(100% + 7px); z-index: 30; width: max-content;
  max-width: min(290px, calc(100vw - 32px));
  background: var(--brand-deep); color: #ffffff;
  font: 400 12px/1.45 var(--body); padding: 8px 10px; border-radius: 7px;
  box-shadow: 0 4px 16px rgba(5, 28, 35, .32);
  pointer-events: none;
}
#report .gloss:hover::after,
#report .gloss:focus-visible::after { display: block; }


/* ── the city in motion: play on top, orientation over the map ───────── */
/* The play row is the first control in the panel now, so it reads as the
   thing to touch rather than one row among ten. */
#panel > .play-row {
  margin: 2px 0 14px; padding-bottom: 13px;
  border-bottom: 1px solid var(--line-soft, #e4e9ec);
}
#panel > .play-row #playpause { font-weight: 600; padding: 7px 15px; }

/* The one action in this pane, so it takes the same shape as the one action
   in Ask Cambium: a solid brand pill. Was a grey outlined chip in the mono
   face, which read as a debug control. */
/* The pair spans the same width as the sliders above them, split evenly —
   two pills sized to their own words left a ragged right edge against a
   column of full-width bars. No wrapping: they share the row or nothing. */
.time-ctas { display: flex; gap: 8px; }
/* Not a 50/50 split: "Jump to now" takes only what its words need and the
   longer label gets the rest, so neither pill is padded out to a size its
   text does not want. Together they still span the sliders above them. */
.time-ctas > button { flex: none; white-space: nowrap; }
.time-ctas > #jump-now { padding-inline: 14px; }
.time-ctas > #jump-8am { flex: 1 1 auto; min-width: 0; }
#jump-now, #jump-8am {
  margin-top: 7px; padding: 9px 17px; border-radius: 999px;
  border: 0; background: var(--brand); color: #fff; cursor: pointer;
  font: 600 12.5px/1.5 var(--body);
  transition: background .15s, transform .12s;
}
#jump-now:hover, #jump-8am:hover { background: var(--brand-line, #008cff); }
#jump-now:active, #jump-8am:active { transform: scale(.97); }
@media (prefers-reduced-motion: reduce) {
  #jump-now:active, #jump-8am:active { transform: none; }
}
#jump-now:focus-visible,
#jump-8am:focus-visible { outline: 2px solid var(--brand-line); outline-offset: 2px; }

/* A5: shown only while trains in motion is zero, which is true by
   definition rather than a guess at when the MBTA stops. */
#quiet-note {
  margin: 2px 0 8px; padding: 6px 9px; border-radius: 6px;
  background: rgba(255, 179, 92, 0.16);
  border: 0;
  color: #7a4a12; font-size: 12px; line-height: 1.45; font-weight: 400;
}
#quiet-note b { display: block; font-weight: 600; }
#quiet-note span { display: block; margin-top: 5px; }
#quiet-note[hidden] { display: none; }

/* A7. pointer-events:none on the wrapper so it never eats a drag; the
   button turns them back on for itself. */
/* one card: the stamp, the sentence and the button share a single black
   panel at 50% rather than each carrying their own ground */
#orient {
  /* centred in the map that is actually visible — the strip to the RIGHT of
     the panel. The panel's edge moves with the layout, so `left` and the
     width cap are written by showOrientation() and kept up to date on
     resize; these are the fallback for the moment before that runs. */
  position: absolute; left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  z-index: 6; box-sizing: border-box; width: min(480px, calc(100% - 48px));
  text-align: center; pointer-events: none;
  background: rgba(0, 0, 0, 0.5); color: #fff;
  padding: 22px 26px 24px; border-radius: 14px;
  box-shadow: 0 6px 28px rgba(0, 0, 0, 0.45);
}
#orient[hidden] { display: none; }
/* the card wears the map's own stamp as its title, in the display face */
#orient-when {
  margin: 0 0 10px; color: #fff;
  font-family: var(--display, inherit);
  font-size: 22px; font-weight: 400; line-height: 1.2; letter-spacing: -.01em;
  display: flex; flex-wrap: wrap; align-items: baseline; justify-content: center;
  gap: 0 10px;
}
#orient-when #orient-when-text { opacity: .82; font-variant-numeric: tabular-nums; }
/* leaving: the whole card sinks towards the foot of the map and fades */
#orient {
  transition: opacity .42s ease, transform .42s cubic-bezier(.4, 0, .7, 1);
}
#orient.leaving {
  opacity: 0; transform: translate(-50%, calc(-50% + 46px)); pointer-events: none;
}
#orient p {
  margin: 0 auto 18px; max-width: 34ch;
  font-size: 17px; line-height: 1.5; text-wrap: balance;
}
#orient-dismiss {
  pointer-events: auto; cursor: pointer;
  background: var(--brand-accent, #9edfff); color: #051c23;
  border: 0; border-radius: 999px; padding: 9px 18px;
  font: 600 13.5px/1 var(--body);
}
#orient-dismiss:hover { background: #c4ebff; }
#orient-dismiss:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
@media (max-width: 640px) { #orient { display: none; } }

/* ── FN9: card captions become a hoverable question mark ─────────────── */
#report .cardhead {
  display: flex; align-items: flex-start; gap: 9px;
  position: relative; margin-bottom: 15px;
}
#report .cardhead h3 { flex: 1; min-width: 0; }
/* The dot sits on the title's x-height midline, not on the top of its box:
   the head aligns to flex-start, so the 4px the two centres were apart is
   taken off here. */
#report .capq {
  flex: none; width: 18px; height: 18px; padding: 0; border-radius: 50%;
  border: 1px solid var(--line-soft); background: none; color: var(--mute);
  font: 600 12px/1 var(--body); cursor: help; margin-top: 4px;
}
/* The map's own download, beside its "?": same size and weight, so the pair
   reads as one control group. */
#report .mapdl{
  flex:none;width:18px;height:18px;padding:0;margin-top:4px;margin-left:6px;
  display:flex;align-items:center;justify-content:center;
  border-radius:50%;border:1px solid var(--line-soft);background:none;
  color:var(--mute);cursor:pointer}
#report .mapdl svg{width:11px;height:11px;fill:currentColor}
#report .mapdl,#report .secmap:has(.mapwrap) .capq{background:#fff}
#report .mapdl:hover{color:var(--ink);border-color:var(--mute)}
#report .mapdl:focus-visible{outline:2px solid var(--brand-line);
  outline-offset:2px}
#report .capq:hover { color: var(--ink); border-color: var(--mute); }
#report .capq:focus-visible {
  outline: 2px solid var(--brand-line); outline-offset: 2px;
}
/* anchored to the card head's RIGHT edge, so a long caption opens inwards
   and can never push the page sideways the way the first gloss tooltip did */
#report .capq::after {
  content: attr(data-cap); display: none; position: absolute; right: 0;
  top: calc(100% + 7px); z-index: 30; width: max-content;
  max-width: min(320px, calc(100vw - 32px)); text-align: left;
  background: var(--brand-deep); color: #ffffff;
  font: 400 12px/1.45 var(--body); padding: 8px 10px; border-radius: 7px;
  box-shadow: 0 4px 16px rgba(5, 28, 35, .32); pointer-events: none;
}
#report .capq:hover::after,
#report .capq:focus-visible::after { display: block; }

/* The theme switch sits in the top row with Play — the previous home, a
   collapsed Display section, was somewhere nobody looked. */
#theme-toggle {
  margin-left: auto; flex: none; padding: 5px 8px; border-radius: 6px;
  border: 1px solid #ccc; background: #f5f5f5; color: #333;
  font: 500 12px var(--mono); cursor: pointer;
}
#theme-toggle:hover { background: #eee; }
#theme-toggle:focus-visible {
  outline: 2px solid var(--brand-line); outline-offset: 2px;
}

/* ══ main menu + three pages (2026-09-15) ═════════════════════════════════
   Appended at the end on purpose: the report block in the middle of this
   file is GENERATED by merge_report_into_sim.py, so anything that has to
   outrank it lives after it rather than being edited into it.

   Menu style is taken from cambium.ai: a floating dark pill inset from the
   top, 20px radius, wordmark left, 13px/500 links at 85% white right. */

/* The ten numbered analysis links are still built by planning.js (generated)
   into #snav. We don't edit that file; we hide its nav. */
#snav { display: none !important; }

#sitehead {
  position: fixed;
  top: 10px; left: 50%; transform: translateX(-50%);
  width: max-content; max-width: calc(100vw - 20px);
  height: 56px; border-radius: 20px;
  /* Black glass rather than the deep-ink navy it started as. */
  background: rgba(0, 0, 0, 0.60);
  -webkit-backdrop-filter: blur(14px) saturate(1.3);
  backdrop-filter: blur(14px) saturate(1.3);
  border: 1px solid rgba(255, 255, 255, 0.08);
  box-shadow: 0 8px 28px rgba(0, 0, 0, 0.32);
  z-index: 70;
}
#sitehead .shin {
  height: 56px; max-width: none; margin: 0;
  padding: 0 12px; gap: 22px;
}
#sitehead .sbrand { font-size: 14px; gap: 8px; }
#sitehead .sbrand .slogo { height: 20px; width: auto; }
#sitehead .sbrand span {
  font-family: var(--body); font-size: 13px; font-weight: 500;
  color: rgba(255, 255, 255, 0.85); letter-spacing: 0;
}

#mainnav { display: flex; align-items: center; gap: 2px; margin-left: 0; }
#mainnav a {
  font: 500 13px/1 var(--body);
  color: rgba(255, 255, 255, 0.85);
  text-decoration: none; white-space: nowrap;
  padding: 8px 11px; border-radius: 999px;
  transition: color .15s, background-color .15s;
}
#mainnav a:hover { color: #fff; background: rgba(255, 255, 255, 0.10); }
#mainnav a.on    { color: #051c23; background: #9edfff; }
#mainnav a:focus-visible {
  outline: 2px solid var(--brand-line); outline-offset: 2px;
}

/* The pill floats over the page, so the map takes the whole viewport and the
   two reading pages open their own top gap instead. */
body[data-view="map"] #map-wrap { height: 100vh; height: 100dvh; }
body[data-view="report"] #report,
body[data-view="validation"] #report { padding-top: 78px; }
/* the report's own sticky sub-header has to clear the pill */
body #report header.top { top: 76px; }
/* deep links land below the pill rather than under it */
#report section.an, #report [id] { scroll-margin-top: 92px; }

@media (max-width: 640px) {
  #sitehead { left: 10px; right: 10px; transform: none; width: auto; }
  #sitehead .shin { gap: 10px; padding: 0 10px; }
  #mainnav a { padding: 8px 8px; }
}

/* ── map controls, top right (2026-09-15) ─────────────────────────────────
   Playback and the theme switch were the panel's first row; they now sit in
   the top-right corner alongside MapLibre's zoom/compass group, which is
   39px wide with a 10px margin — hence the 58px inset that parks this band
   immediately to its left. */
#mapctl {
  position: absolute; top: 10px; right: 58px; z-index: 6;
  display: flex; align-items: flex-start; gap: 8px;
}
#mapctl .play-row, #mapctl .mapctl-card {
  margin: 0; padding: 5px 10px;
  display: flex; align-items: center; gap: 8px; flex-wrap: nowrap;
  background: #fff; border-radius: 10px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.22);
  border-bottom: 0;
}
#mapctl .play-row input[type="range"] { flex: none; width: 104px; min-width: 0; }
#mapctl .inline-label { margin-left: 0; white-space: nowrap; font-size: 13.5px; }
#mapctl #playpause { font-weight: 600; padding: 6px 13px; }
#mapctl #theme-toggle { margin-left: 0; }

/* Narrow windows: the centred menu pill and this band run out of room long
   before the map does, so drop the band under the pill and let it shrink. */
@media (max-width: 900px) {
  #mapctl { top: 76px; right: 10px; }
}
@media (max-width: 560px) {
  #mapctl { flex-wrap: wrap; justify-content: flex-end; max-width: calc(100vw - 20px); }
  #mapctl .play-row input[type="range"] { width: 72px; }
}

/* ── dock + drawer (2026-09-15) ═══════════════════════════════════════════
   A four-item dock replaces the tall panel. Choosing an item slides the dock
   left and fades it back; #panel (the drawer) comes in from the right over
   it, leaving the dock's icon rail visible. The rail closes it. */

#dock {
  position: absolute; top: 12px; left: 12px; z-index: 7;
  width: 208px; padding: 6px;
  background: #fff; border-radius: 10px;
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.2);
  display: flex; flex-direction: column; gap: 2px;
  transition: transform .34s cubic-bezier(.22,.61,.36,1), opacity .28s ease;
}
#dock .dock-item {
  display: flex; align-items: center; gap: 10px;
  width: 100%; padding: 9px 10px;
  background: none; border: 0; border-radius: 8px;
  font: 500 13.5px/1.2 var(--body); color: var(--text, #051c23);
  text-align: left; cursor: pointer;
  transition: background-color .15s, color .15s;
}
#dock .dock-item svg { width: 19px; height: 19px; flex: none; opacity: .75; }
#dock .dock-item:hover { background: #eef2f4; }
#dock .dock-item.on { background: #051c23; color: #fff; }
#dock .dock-item.on svg { opacity: 1; }
#dock .dock-item:focus-visible { outline: 2px solid var(--brand-line); outline-offset: 1px; }

/* Open: the dock steps left and dims to a rail; only its icons stay lit. */
body.dock-open #dock { transform: translateX(-14px); opacity: .5; }
body.dock-open #dock .dock-item span { opacity: 0; transition: opacity .2s; }
body.dock-open #dock .dock-item.on { opacity: 1; }

/* #panel is the drawer. Closed it is inert and offset right; open it slides
   back and covers all of the dock but the rail. */
#panel {
  left: 78px; top: 12px; width: 320px;
  opacity: 0; transform: translateX(26px);
  pointer-events: none; z-index: 6;
  transition: transform .34s cubic-bezier(.22,.61,.36,1), opacity .26s ease;
}
body.dock-open #panel {
  opacity: 1; transform: translateX(0); pointer-events: auto;
}
#panel-collapse { display: none; }

@media (prefers-reduced-motion: reduce) {
  #dock, #panel { transition: opacity .15s ease; }
  body.dock-open #dock { transform: none; }
  #panel { transform: none; }
}

@media (max-width: 560px) {
  #dock { width: 176px; }
  #panel { left: 64px; width: calc(100vw - 88px); }
}

/* Both boxes sit on the vertical centre of the map rather than the top.
   The slide is on the same transform as the centring, so X and Y have to be
   written together — a second `transform` would replace the first. */
#dock { top: 50%; transform: translate(0, -50%); }
body.dock-open #dock { transform: translate(-14px, -50%); }

#panel { top: 50%; transform: translate(26px, -50%); max-height: calc(100% - 24px); }
body.dock-open #panel { transform: translate(0, -50%); }

@media (prefers-reduced-motion: reduce) {
  body.dock-open #dock { transform: translate(0, -50%); }
  #panel, body.dock-open #panel { transform: translate(0, -50%); }
}

/* ── info sheet (2026-09-15) ══════════════════════════════════════════════
   Behind the map's info icon: "How this is modelled" and "Sources" expanded,
   plus the basemap attribution the icon used to toggle. */
#infosheet { position: fixed; inset: 0; z-index: 90; }
#infosheet[hidden] { display: none; }
#infosheet-back {
  position: absolute; inset: 0;
  background: rgba(5, 20, 26, 0.55);
  -webkit-backdrop-filter: blur(2px); backdrop-filter: blur(2px);
  animation: infofade .18s ease;
}
#infosheet-card {
  position: absolute; top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  /* one modal size across the app — shared with the account and consent
     modals, which declare --eik-modal-* in account.css */
  width: var(--eik-modal-w, min(860px, calc(100vw - 32px)));
  height: var(--eik-modal-h, min(86vh, 820px)); overflow-y: auto;
  background: #fff; color: var(--text, #051c23);
  border-radius: 14px; padding: 26px 28px 24px;
  box-shadow: 0 18px 60px rgba(0, 0, 0, 0.45);
  font-size: 13.5px; line-height: 1.55;
  animation: inforise .22s cubic-bezier(.22,.61,.36,1);
  scrollbar-width: thin; scrollbar-color: #c3ccd3 transparent;
}
@keyframes infofade { from { opacity: 0 } }
@keyframes inforise { from { opacity: 0; transform: translate(-50%, -46%) } }
#infosheet-card h2 {
  margin: 0 0 14px; font-family: var(--display, inherit);
  font-size: 21px; font-weight: 600; letter-spacing: -.01em;
}
#infosheet-close {
  position: absolute; top: 14px; right: 14px;
  width: 30px; height: 30px; padding: 0;
  border: 1px solid #d8dfe3; border-radius: 8px;
  background: #f5f7f8; color: #33464d;
  font-size: 18px; line-height: 1; cursor: pointer;
}
#infosheet-close:hover { background: #eceff1; }
/* the two sections read as prose here, not as collapsible rows */
#infosheet-body details.psec { margin: 0 0 6px; }
#infosheet-body details.psec > summary {
  list-style: none; cursor: default; pointer-events: none;
  font-size: 15px; font-weight: 600; margin-bottom: 4px;
}
#infosheet-body details.psec > summary::-webkit-details-marker { display: none }
#infosheet-body details.psec > summary::after { content: none !important }
#infosheet-attrib { margin-top: 18px; padding-top: 14px; border-top: 1px solid #e4e9ec }
#infosheet-attrib h3 { margin: 0 0 4px; font-size: 13px; font-weight: 600 }
#infosheet-attrib p { margin: 0; color: #4a5d64 }
#infosheet-attrib a { color: var(--brand-line, #008cff) }

@media (prefers-reduced-motion: reduce) {
  #infosheet-back, #infosheet-card { animation: none }
}

/* ── map action stack (2026-09-15) ════════════════════════════════════════
   Play/pause, speed and light/dark as white circles above MapLibre's
   zoom/compass group, with the whole right-hand column centred on the map.
   MapLibre pins that container to top:0 — overriding top/transform is what
   moves the group and our circles together as one block. */
.maplibregl-ctrl-top-right {
  top: 50%; transform: translateY(-50%); height: auto;
}

#mapstack {
  pointer-events: auto;                 /* the container itself has none */
  display: flex; flex-direction: column; align-items: center; gap: 8px;
  /* the bottom margin is the gap down to MapLibre's zoom group */
  margin: 0 10px 24px 0;
}
/* Theme is not a playback control, so it stands alone between the playback
   pair and the zoom group — the same gap above it as below it. */
#mapstack #ms-theme { margin-top: 16px; }   /* 16 + the 8px gap = 24 */
#mapstack button {
  position: relative;
  width: 38px; height: 38px; padding: 0;
  display: flex; align-items: center; justify-content: center;
  border: 0; border-radius: 50%;
  background: #fff; color: #17323b;
  box-shadow: 0 1px 3px rgba(0, 0, 0, .18), 0 0 0 1px rgba(0, 0, 0, .06);
  cursor: pointer;
  transition: background-color .15s, color .15s, transform .12s;
}
#mapstack button:hover { background: #f1f5f6; }
#mapstack button:active { transform: scale(.94); }
#mapstack button:focus-visible {
  outline: 2px solid var(--brand-line, #008cff); outline-offset: 2px;
}
#mapstack svg { width: 19px; height: 19px; }
#mapstack .ms-badge {
  /* A circle hanging off the icon's bottom-left corner. The button is 38px
     and the icon 19px, so that corner is at (9.5, 28.5); the circle's centre
     sits down and out from it. translate(-50%, -50%) centres it on the
     left/top values. */
  position: absolute; left: 0; top: 37px; right: auto; bottom: auto;
  transform: translate(-50%, -50%);
  /* big enough for the longest reading, "300×" */
  width: 29px; height: 29px; padding: 0;
  display: flex; align-items: center; justify-content: center;
  background: #051c23; color: #fff;
  border-radius: 50%; border: 0;
  box-sizing: border-box;
  font: 600 9px/1 var(--mono, ui-monospace, monospace);
  letter-spacing: -.03em; text-align: center;
}

@media (prefers-reduced-motion: reduce) {
  #mapstack button { transition: none }
}

/* ── attribution icon (2026-09-15) ════════════════════════════════════════
   Collapsed to a single circle matching the action stack. The credit itself
   is not dropped — the icon opens the info sheet, which carries it in full
   with its links, which is what the ODbL asks for. */
.maplibregl-ctrl-attrib.maplibregl-compact {
  background: none; box-shadow: none;
  width: auto; min-height: 0; padding: 0; border-radius: 0;
  margin: 0 10px 10px 0;
}
.maplibregl-ctrl-attrib-inner { display: none !important; }
.maplibregl-ctrl-attrib.maplibregl-compact .maplibregl-ctrl-attrib-button {
  position: static; list-style: none;
  width: 38px; height: 38px; margin: 0; padding: 0;
  display: flex; align-items: center; justify-content: center;
  background: rgba(255, 255, 255, .5); background-image: none; border-radius: 50%;
  box-shadow: 0 1px 3px rgba(0, 0, 0, .18), 0 0 0 1px rgba(0, 0, 0, .06);
  color: #17323b; cursor: pointer;
  transition: background-color .15s, transform .12s;
}
.maplibregl-ctrl-attrib-button::-webkit-details-marker { display: none }
.maplibregl-ctrl-attrib.maplibregl-compact .maplibregl-ctrl-attrib-button::after {
  content: "i"; font: 600 17px/1 var(--display, Georgia, "Times New Roman", serif);
}
.maplibregl-ctrl-attrib.maplibregl-compact .maplibregl-ctrl-attrib-button:hover {
  background: rgba(255, 255, 255, .72);
}
.maplibregl-ctrl-attrib.maplibregl-compact .maplibregl-ctrl-attrib-button:active {
  transform: scale(.94);
}

/* ── map caption (2026-09-15) ═════════════════════════════════════════════
   Boston + the simulated date and time, 30px, at the foot of the map. Kept
   clear of the info circle in the bottom-right corner. */
#mapcaption {
  position: absolute; left: 50%; transform: translateX(-50%);
  bottom: 18px; z-index: 4;
  pointer-events: none; user-select: none;
  max-width: calc(100% - 120px);
  font-family: var(--display, inherit);
  /* 300 is not in the bundled faces (Bricolage 400/600/700, Geist 400-700),
     so this resolves to 400 — the lightest weight actually shipped. Adding a
     real 300 means downloading that woff2 into static/vendor/fonts. */
  font-size: 18px; font-weight: 300; line-height: 1.2; letter-spacing: -.01em;
  display: flex; flex-wrap: nowrap; align-items: baseline; justify-content: center;
  gap: 0 10px; white-space: nowrap;
  color: #fff; text-shadow: 0 1px 14px rgba(0, 0, 0, .55), 0 1px 3px rgba(0, 0, 0, .4);
}
/* while the card holds the stamp the real caption waits just below the
   frame, then slides up into place as the card sinks past it */
#mapcaption { transition: opacity .42s ease, transform .42s cubic-bezier(.2, .7, .3, 1); }
body.orienting #mapcaption { opacity: 0; transform: translateX(-50%) translateY(34px); }
#mapcaption b { font-weight: inherit; }
#mapcaption #mapcaption-when { opacity: .82; font-variant-numeric: tabular-nums; }

/* on the light basemap the white would vanish */
body[data-map-theme="light"] #mapcaption {
  color: #051c23;
  text-shadow: 0 1px 12px rgba(255, 255, 255, .85), 0 1px 2px rgba(255, 255, 255, .9);
}

@media (max-width: 700px) {
  #mapcaption { font-size: 15px; bottom: 14px; white-space: normal; text-align: center; }
}

/* Date & time is the only section in its pane, so the disclosure arrow was
   offering to collapse the pane's entire contents — it reads as a heading
   now, not an accordion. (Same treatment as the info sheet's sections.) */
#panel > details.psec[data-pane="time"] > summary,
#panel > details.psec[data-pane="ask"] > summary {
  cursor: default; pointer-events: none; list-style: none;
  padding-top: 2px;
}
#panel > details.psec[data-pane="time"] > summary::-webkit-details-marker,
#panel > details.psec[data-pane="ask"] > summary::-webkit-details-marker { display: none }
#panel > details.psec[data-pane="time"] > summary::after,
#panel > details.psec[data-pane="ask"] > summary::after { content: none !important }

/* ── dock/drawer motion, revised (2026-09-15) ═════════════════════════════
   The dock no longer dims to a rail: it leaves past the left edge entirely
   and the drawer arrives from the same side, taking its place. Both keep the
   -50% that centres them, since a second `transform` would replace the first
   rather than add to it. */
#dock {
  transition: transform .38s cubic-bezier(.4,0,.2,1), opacity .22s ease;
}
body.dock-open #dock {
  transform: translate(calc(-100% - 24px), -50%);
  opacity: 0; pointer-events: none;
}
body.dock-open #dock .dock-item span { opacity: 1; }   /* no rail to dim now */

#panel {
  left: 12px;                        /* it lands where the dock was */
  transform: translate(calc(-100% - 24px), -50%);
  transition: transform .38s cubic-bezier(.4,0,.2,1), opacity .24s ease;
}
body.dock-open #panel {
  transform: translate(0, -50%); opacity: 1; pointer-events: auto;
  /* a beat behind the dock's exit so they read as a hand-off, not a swap */
  transition-delay: .06s;
}

/* Sticky header on the drawer: the way back stays put while the pane scrolls.
   #panel is the scroll container and carries 16px of padding, so the row uses
   negative margins to span the full width and cover that padding — otherwise
   content would show through the gap above it once scrolled. */
/* The panel's own top padding has to go: a sticky child sticks to the
   scrollport's CONTENT edge, so with 16px of padding the header parked 16px
   down and scrolling content showed through the strip above it. The header
   supplies that spacing itself instead. */
#panel { padding-top: 0; }
#panel-head {
  position: sticky; top: 0; z-index: 3;
  display: flex; align-items: center; gap: 8px;
  /* auto width + negative side margins: the row bleeds to the panel's edges
     whatever its padding is, where a calc(100% + 32px) has to be kept in
     step with it by hand */
  width: auto; margin: 0 -16px 10px; padding: 14px 16px 11px;
  background: #fff; border-bottom: 1px solid var(--line-soft, #e4e9ec);
  border-radius: 10px 10px 0 0;
}
#panel-back {
  display: flex; align-items: center; gap: 5px; flex: 1; min-width: 0;
  padding: 0; background: none; border: 0;
  font: 500 12.5px/1 var(--body); color: #5a6f77;
  text-align: left; cursor: pointer; pointer-events: auto;
  transition: color .15s;
}
#panel-back svg { width: 16px; height: 16px; flex: none; }
#panel-back:hover { color: var(--text, #051c23); }
#panel-back:focus-visible { outline: 2px solid var(--brand-line); outline-offset: 1px; }

@media (prefers-reduced-motion: reduce) {
  body.dock-open #dock { transform: translate(0, -50%); }
  #panel, body.dock-open #panel { transform: translate(0, -50%); transition-delay: 0s; }
}

/* MapLibre's zoom/compass group is 29px wide against our 38px circles, which
   left the right-hand column looking like two different stacks. Match the
   width so they share a centre line, and soften the corners so a 38px-wide
   box does not read as a slab next to round buttons. */
.maplibregl-ctrl-top-right .maplibregl-ctrl-group {
  width: 38px; margin: 0 10px 0 0; border-radius: 999px; overflow: hidden;
}
.maplibregl-ctrl-top-right .maplibregl-ctrl-group button {
  width: 38px; height: 38px;
}
.maplibregl-ctrl-top-right .maplibregl-ctrl-group button .maplibregl-ctrl-icon {
  width: 38px; height: 38px; background-position: center; background-size: 18px 18px;
}

/* The sticky header names the pane, so a one-section pane must not print the
   same title again underneath it — and the header's rule would then be a
   keyline sitting above nothing. Both go. */
#panel > details.psec[data-pane="time"] > summary,
#panel > details.psec[data-pane="ask"] > summary { display: none; }
#panel-head { border-bottom: 0; padding-bottom: 6px; }
/* .psec carries a border-top for the stack of sections in Filters; the first
   visible one would draw that line straight under the header. */
#panel > details.psec[data-pane="time"],
#panel > details.psec[data-pane="ask"] { border-top: 0; }
#panel > .psec:not([hidden]):first-of-type { border-top: 0; }

/* The playback controls the map circles drive. Out of sight, still in the
   document — they carry app.js's listeners. */
#hidden-controls { display: none !important; }

/* ── Range sliders ─────────────────────────────────────────────────────────
   One treatment for every bar on the page: a soft grey rail, the travelled
   part filled with the brand ramp, and a hollow ring for the thumb so the
   fill reads through it. The fill is a background gradient driven by --pct,
   which a rAF loop in index.html keeps in step with the value (the clock
   slider moves on its own during playback, so an input listener alone would
   leave it behind). --sl-h is the rail; the input is taller than the rail so
   the thumb has somewhere to sit. */
input[type="range"] {
  --pct: 0%;
  --sl-h: 6px;
  --sl-thumb: 20px;
  -webkit-appearance: none; appearance: none;
  background: transparent;
  height: var(--sl-thumb);
  margin: 2px 0;
  cursor: pointer;
}
/* WebKit/Blink paints the rail on the track pseudo-element… */
input[type="range"]::-webkit-slider-runnable-track {
  height: var(--sl-h); border-radius: calc(var(--sl-h) / 2);
  background:
    linear-gradient(90deg, var(--brand, #064d90), var(--brand-line, #008cff))
      0 / var(--pct) 100% no-repeat,
    #eceff1;
}
input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: var(--sl-thumb); height: var(--sl-thumb); border-radius: 50%;
  background: #fff;
  border: 2px solid var(--brand-line, #008cff);
  box-shadow: 0 1px 3px rgba(5, 28, 35, 0.22);
  /* centre the thumb on the rail: WebKit aligns it to the track's top */
  margin-top: calc((var(--sl-h) - var(--sl-thumb)) / 2);
  transition: transform .12s ease, box-shadow .12s ease;
}
/* …Firefox on the track, and it needs the fill spelled out the same way. */
input[type="range"]::-moz-range-track {
  height: var(--sl-h); border-radius: calc(var(--sl-h) / 2);
  background:
    linear-gradient(90deg, var(--brand, #064d90), var(--brand-line, #008cff))
      0 / var(--pct) 100% no-repeat,
    #eceff1;
}
input[type="range"]::-moz-range-thumb {
  width: var(--sl-thumb); height: var(--sl-thumb); border-radius: 50%;
  box-sizing: border-box;
  background: #fff;
  border: 2px solid var(--brand-line, #008cff);
  box-shadow: 0 1px 3px rgba(5, 28, 35, 0.22);
}
input[type="range"]:hover::-webkit-slider-thumb { transform: scale(1.08); }
input[type="range"]:focus { outline: none; }
input[type="range"]:focus-visible::-webkit-slider-thumb {
  box-shadow: 0 0 0 3px rgba(0, 140, 255, 0.28);
}
input[type="range"]:focus-visible::-moz-range-thumb {
  box-shadow: 0 0 0 3px rgba(0, 140, 255, 0.28);
}
@media (prefers-reduced-motion: reduce) {
  input[type="range"]::-webkit-slider-thumb { transition: none; }
  input[type="range"]:hover::-webkit-slider-thumb { transform: none; }
}
/* The speed bar sits inline in the transport row, where a full-size thumb
   would push the row onto two lines. */
.play-row input[type="range"] { --sl-h: 6px; --sl-thumb: 14px; }

/* ── Slider scales ─────────────────────────────────────────────────────────
   The steps a bar can land on, drawn under it: a grey keyline per step and a
   label on the ones there is room to name. Built by index.html from a config,
   so the tick positions always match the input's own min/max. */
.slider-ctrl { margin-bottom: 20px; }
.slider-ctrl .ticks {
  position: relative; height: 20px; margin-top: 5px;
  /* the rail spans the input minus half a thumb at each end, and the ticks
     have to line up with the rail, not the box */
  margin-inline: calc(var(--sl-thumb, 18px) / 2);
}
.slider-ctrl .ticks .tick {
  position: absolute; top: 0; transform: translateX(-0.5px);
  width: 1px; height: 4px; background: #d7dde1;
}
.slider-ctrl .ticks .tick.major { height: 7px; background: #b9c3ca; }
.slider-ctrl .ticks .tlab {
  position: absolute; top: 8px; transform: translateX(-50%);
  font-size: 10px; line-height: 1.1; color: #7b878f;
  white-space: nowrap; letter-spacing: .01em;
}
/* the first and last labels would otherwise hang off the panel */
.slider-ctrl .ticks .tlab.first { transform: none; left: 0 !important; }
.slider-ctrl .ticks .tlab.last  { transform: translateX(-100%); }

/* The slider names are quiet labels, not headings — the bar and its value
   carry the weight. The value itself stays a shade stronger so it still
   reads as the answer to the label. */
.slider-ctrl label { font-weight: 400; color: #51606a; }
.slider-ctrl label output { font-weight: 500; color: var(--text, #051c23); }

/* ── API key CTA ───────────────────────────────────────────────────────────
   Top-right of the sticky header, on the "Ask Cambium" title row, and only
   on that pane — the other panes' headers keep the whole row for their name.
   The popup hangs off the header, which is the panel's own positioning
   context, so it stays put while the pane scrolls under it. */
#panel-head { position: sticky; }   /* also the popup's containing block */
#key-cta, #keypop { display: none; }
#panel[data-pane="ask"] #key-cta { display: inline-flex; }
#panel[data-pane="ask"] #keypop:not([hidden]) { display: block; }

#key-cta {
  align-items: center; gap: 5px; flex: none;
  padding: 4px 10px; border-radius: 999px;
  border: 1px solid var(--brand-line, #008cff);
  background: #fff; color: var(--brand, #064d90);
  font: 600 11.5px/1.5 var(--body); cursor: pointer; white-space: nowrap;
  transition: background .15s, color .15s;
}
#key-cta:hover { background: var(--brand-soft, #cdeeff); }
#key-cta:focus-visible { outline: 2px solid var(--brand-line); outline-offset: 2px; }
/* Once a key is saved the CTA is a status, not an invitation. */
#key-cta.set {
  border-color: var(--line-soft, #e4e9ec);
  background: #f4f7f9; color: #5a6f77; font-weight: 500;
}
#key-cta.set::before { content: "●"; font-size: 8px; color: #2e9e5b; }

#keypop {
  position: absolute; top: calc(100% - 4px); right: 12px; z-index: 6;
  width: 262px; padding: 12px; box-sizing: border-box;
  background: #fff; border: 1px solid var(--line-soft, #e4e9ec);
  border-radius: 10px; box-shadow: 0 8px 24px rgba(5, 28, 35, 0.16);
}
#keypop .note { margin: 0 0 9px; }
#keypop input {
  width: 100%; box-sizing: border-box; margin-bottom: 9px;
  padding: 7px 9px; border: 1px solid #ccd5da; border-radius: 7px;
  font-size: 12.5px;
}
#keypop input:focus-visible {
  outline: none; border-color: var(--brand-line, #008cff);
  box-shadow: 0 0 0 3px rgba(0, 140, 255, 0.18);
}
.keypop-actions { display: flex; gap: 8px; align-items: center; }
#chat-key-save {
  padding: 6px 13px; border: 0; border-radius: 7px;
  background: var(--brand, #064d90); color: #fff;
  font: 600 12.5px/1.5 var(--body); cursor: pointer;
}
#chat-key-save:hover { background: var(--brand-line, #008cff); }
#chat-key-clear {
  padding: 6px 4px; border: 0; background: none; color: #8b979e;
  font: 500 12px/1.5 var(--body); cursor: pointer; text-decoration: underline;
}
#chat-key-clear:hover { color: #b3402e; }

/* ── Ask Cambium as a conversation ─────────────────────────────────────────
   Every other pane is a list of controls that ends where its content ends. A
   conversation doesn't: the transcript is the pane, so on this pane alone the
   drawer takes the full height of the map and the column inside it gives all
   the spare room to the log. The composer stays on the bottom edge where it
   is reachable without scrolling back down. */
#panel[data-pane="ask"] {
  height: calc(100% - 24px);
  display: flex; flex-direction: column;
  overflow: hidden;              /* the log scrolls, not the drawer */
}
#panel[data-pane="ask"] > details.psec[data-pane="ask"] {
  flex: 1; min-height: 0;        /* without this the log never shrinks */
  display: flex; flex-direction: column;
  margin-bottom: 0; padding-bottom: 0;
}
/* Chrome wraps a <details>' contents in a ::details-content slot, so the
   group below is the slot's child, not the <details>' — without this the
   column stops growing at the slot and the log never gets the spare room.
   Engines that have no such slot ignore the rule and the group, which is
   then the direct flex item, grows on its own. */
#panel[data-pane="ask"] > details.psec[data-pane="ask"]::details-content {
  display: flex; flex-direction: column; flex: 1; min-height: 0;
}
#panel[data-pane="ask"] > details.psec[data-pane="ask"] > .ctrl-group {
  flex: 1; min-height: 0;
  display: flex; flex-direction: column; margin-bottom: 0;
}
#panel[data-pane="ask"] #chat-log {
  flex: 1; min-height: 0; max-height: none;
  margin-bottom: 10px; padding-right: 2px;
}
/* The first message sits at the bottom of an empty transcript rather than
   floating at the top of a tall column. */
#panel[data-pane="ask"] #chat-log { display: flex; flex-direction: column; }
#panel[data-pane="ask"] #chat-log::before { content: ""; flex: 1; }
#panel[data-pane="ask"] .chat-msg, #panel[data-pane="ask"] .chat-tool { flex: none; }

/* The starters are an empty state: once there is a conversation they are
   just three more things between the reader and the transcript. */
#panel[data-pane="ask"] .chat-egs { flex: none; }
#panel.chat-started .chat-egs { display: none; }

/* Pinned to the foot of the drawer, on its own ground, so a long transcript
   scrolls behind it rather than pushing it off. */
#panel[data-pane="ask"] #chat-form {
  flex: none; margin: 0 -16px -16px; padding: 10px 16px 16px;
  border-top: 1px solid var(--line-soft, #e4e9ec); background: #fff;
}
#panel[data-pane="ask"] #chat-input { padding: 8px 11px; font-size: 12.5px; }

/* ── Chat micro-animations ─────────────────────────────────────────────────
   Small enough to read as the message arriving rather than as an effect:
   a short rise and fade, leaning in from the side the bubble belongs to, and
   a soft settle on the bubble itself. Everything here is switched off under
   prefers-reduced-motion at the foot of this block. */
@keyframes chat-in-left {
  from { opacity: 0; transform: translate3d(-6px, 8px, 0) scale(.985); }
  to   { opacity: 1; transform: none; }
}
@keyframes chat-in-right {
  from { opacity: 0; transform: translate3d(6px, 8px, 0) scale(.985); }
  to   { opacity: 1; transform: none; }
}
.chat-msg {
  animation: chat-in-left .34s cubic-bezier(.22, .9, .3, 1) both;
  transform-origin: left bottom;
}
.chat-user { animation-name: chat-in-right; transform-origin: right bottom; }
.chat-tool { animation: chat-in-left .3s cubic-bezier(.22, .9, .3, 1) both; }
/* The welcome bubble is already on screen when the pane opens — it should
   arrive with the pane, a beat after the header. */
#panel[data-pane="ask"] .chat-welcome { animation-delay: .06s; }

/* .chat-msg is pre-wrap so plain answers keep their line breaks; this one is
   real markup, and pre-wrap would print the source's own indentation. */
.chat-welcome { white-space: normal; padding: 10px 11px; }
.chat-welcome p { margin: 0; }
.chat-welcome-sub { color: var(--text-muted); margin-top: 3px !important; }

/* The starters are part of the welcome bubble now, so they sit on its ground
   rather than on the panel's, and they come in one after another. */
.chat-welcome .chat-egs { margin: 9px 0 1px; gap: 5px; }
.chat-welcome .chat-eg {
  background: #fff; border-color: #dfe5e9;
  animation: chat-in-left .3s cubic-bezier(.22, .9, .3, 1) both;
  transition: background .15s, border-color .15s, transform .15s;
}
.chat-welcome .chat-eg:nth-child(1) { animation-delay: .20s; }
.chat-welcome .chat-eg:nth-child(2) { animation-delay: .28s; }
.chat-welcome .chat-eg:nth-child(3) { animation-delay: .36s; }
.chat-welcome .chat-eg:hover {
  background: var(--brand-soft); border-color: var(--brand-line);
  transform: translateX(2px);
}
.chat-welcome .chat-eg:active { transform: translateX(2px) scale(.99); }

/* Waiting: three dots breathing in sequence, in place of a static ellipsis. */
.chat-pending { display: flex; align-items: center; gap: 4px; padding: 11px 10px; }
.chat-pending .dot {
  width: 5px; height: 5px; border-radius: 50%;
  background: var(--text-muted, #7b878f);
  animation: chat-dot 1.15s ease-in-out infinite;
}
.chat-pending .dot:nth-child(2) { animation-delay: .16s; }
.chat-pending .dot:nth-child(3) { animation-delay: .32s; }
@keyframes chat-dot {
  0%, 70%, 100% { opacity: .3; transform: translateY(0); }
  35%           { opacity: 1;  transform: translateY(-3px); }
}

/* The composer lifts slightly when it has focus, so the eye follows it. */
#panel[data-pane="ask"] #chat-input {
  transition: border-color .15s, box-shadow .15s;
}
#panel[data-pane="ask"] #chat-input:focus {
  outline: none; border-color: var(--brand-line);
  box-shadow: 0 0 0 3px rgba(0, 140, 255, .16);
}
#chat-form button { transition: background .15s, transform .12s; }
#chat-form button:active { transform: scale(.97); }

@media (prefers-reduced-motion: reduce) {
  .chat-msg, .chat-tool, .chat-welcome .chat-eg { animation: none; }
  .chat-welcome .chat-eg:hover, .chat-welcome .chat-eg:active,
  #chat-form button:active { transform: none; }
  .chat-pending .dot { animation: none; opacity: .55; }
}

/* ── Bubble tail ───────────────────────────────────────────────────────────
   A small triangle on the top-left corner, so an assistant bubble reads as
   something said rather than a panel of text. It hangs above the bubble's
   top edge, which needs a matching top margin or it would sit on the
   message above. */
.chat-assistant { position: relative; margin-top: 8px; }
.chat-assistant::before {
  content: ""; position: absolute; top: -6px; left: 13px;
  border-left: 7px solid transparent;
  border-right: 7px solid transparent;
  border-bottom: 7px solid #f2f4f6;   /* the bubble's own ground */
}
.chat-error::before { border-bottom-color: #fdecef; }
/* The waiting dots are a placeholder for a bubble, not a remark of its own. */
.chat-pending::before { display: none; }
/* The tail belongs to the bubble it opens, not to the trail of tool calls
   that follows it. */
.chat-tool + .chat-assistant::before { display: none; }

/* A little more room inside than the text strictly needs — the bubble reads
   as a held pause rather than a tight label. */
.chat-assistant, .chat-user { padding: 11px 13px 12px; }
.chat-welcome { padding: 14px 14px 16px; }
.chat-welcome .chat-egs { margin: 12px 0 2px; }

/* ══ Chat, reformatted ══════════════════════════════════════════════════════
   Wide bubbles on a tinted ground, each with a squared-off corner and a small
   flag on the side it comes from; the starters are cards inside the welcome
   bubble under their own eyebrow; the composer is one pill. This block is the
   current word on the chat's shape — it deliberately overrides the sizes and
   colours set in the earlier chat blocks above. */
:root {
  --chat-them: #e3e7f0;    /* what the map says */
  --chat-you:  #c7ebfa;    /* what you say      */
  --chat-tail: 9px;
}

/* The transcript sits on its own ground, bled to the panel's edges, so the
   header and the composer read as chrome either side of it. */
#panel[data-pane="ask"] #chat-log {
  margin: 0 -16px; padding: 6px 16px 12px;
  background: #f5f6f8;
}

.chat-msg {
  font-size: 13px; line-height: 1.5;
  border-radius: 14px; padding: 12px 14px 13px;
  margin-bottom: 12px;
}
/* Near the full width either way: these are statements, not chat-app slivers,
   and the figures in them are long. The inset is on the flag's side only. */
.chat-assistant { background: var(--chat-them); color: var(--text); }
.chat-user      { background: var(--chat-you);  color: var(--text); }
.chat-assistant { margin-left: var(--chat-tail); border-top-left-radius: 0; }
.chat-user      { margin-right: var(--chat-tail); border-top-right-radius: 0; }

/* The flag: a right triangle whose vertical edge is flush with the bubble's
   squared corner, so the two read as one shape rather than a bubble with a
   spike stuck on it. */
.chat-assistant::before, .chat-user::before {
  content: ""; position: absolute; top: 0;
  border-bottom: 11px solid transparent;
}
.chat-assistant { position: relative; margin-top: 0; }
.chat-user      { position: relative; }
.chat-assistant::before {
  left: calc(var(--chat-tail) * -1); right: auto;
  border-left: 0; border-right: var(--chat-tail) solid var(--chat-them);
}
.chat-user::before {
  right: calc(var(--chat-tail) * -1); left: auto;
  border-right: 0; border-left: var(--chat-tail) solid var(--chat-you);
}
.chat-error { background: #fdecef; color: #8c1230; }
.chat-error::before { border-right-color: #fdecef; }

/* ── The welcome bubble ── */
.chat-welcome { padding: 14px 15px 16px; }
.chat-welcome p { font-size: 13px; line-height: 1.5; }
.chat-welcome-sub { color: #6b7a83; margin-top: 6px !important; }
.chat-egs-label {
  margin: 15px 0 8px;
  font: 700 10.5px/1.2 var(--body); letter-spacing: .085em;
  text-transform: uppercase; color: #8794a0;
}
.chat-welcome .chat-egs { margin: 0; gap: 7px; }
.chat-welcome .chat-eg {
  padding: 11px 13px; border-radius: 11px;
  border: 0; background: #fff; color: var(--text);
  font-size: 12.5px; line-height: 1.35;
}
.chat-welcome .chat-eg:hover {
  background: #fff; border: 0;
  box-shadow: 0 2px 8px rgba(5, 28, 35, .1); transform: translateX(2px);
}

/* ── The composer ── */
#panel[data-pane="ask"] #chat-form { padding: 11px 16px 16px; }
#panel[data-pane="ask"] .composer {
  /* the same span as a bubble WITH its flag: the bubbles are inset by
     --chat-tail on their own side and reach the content edge on the other,
     so the pair together covers the full content width — and so does this */
  flex: 1; min-width: 0;
  display: flex; align-items: center; gap: 6px;
  padding: 4px 4px 4px 6px;
  background: #fff; border: 1px solid #dde4e9; border-radius: 999px;
  transition: border-color .15s, box-shadow .15s;
}
#panel[data-pane="ask"] .composer:focus-within {
  border-color: var(--brand-line); box-shadow: 0 0 0 3px rgba(0, 140, 255, .14);
}
#panel[data-pane="ask"] #chat-input {
  border: 0; background: none; padding: 9px 8px; font-size: 12.5px;
}
#panel[data-pane="ask"] #chat-input:focus { outline: none; box-shadow: none; }
#panel[data-pane="ask"] #chat-form button {
  flex: none; padding: 9px 17px; border: 0; border-radius: 999px;
  background: var(--brand); color: #fff; font-weight: 600; font-size: 12.5px;
}

/* ── Waiting ── */
.chat-pending { background: var(--chat-them); padding: 15px 15px 14px; }
.chat-pending .dot { width: 7px; height: 7px; background: var(--brand-deep); }
.chat-pending::before { display: block; }   /* it is a bubble; it keeps its flag */

/* ── The drawer fills the map ──────────────────────────────────────────────
   Every pane, not just Ask: the panel takes the full height of the map with
   the same 12px inset it already keeps on the sides, so it reads as a drawer
   fixed to the edge rather than a card that changes size with whatever
   happens to be inside it. Content still scrolls when there is more of it
   than there is room. */
#panel { height: calc(100% - 24px); }

/* Date & time then has room to spare, and that room should fall between the
   controls and the actions rather than dangling under everything — the same
   shape as the composer pinned to the foot of Ask. */
#panel[data-pane="time"] { display: flex; flex-direction: column; }
#panel[data-pane="time"] > details.psec[data-pane="time"] {
  flex: 1; min-height: 0; display: flex; flex-direction: column;
  margin-bottom: 0;
}
/* Chrome's anonymous slot again — see the note on the Ask pane above. */
#panel[data-pane="time"] > details.psec[data-pane="time"]::details-content {
  display: flex; flex-direction: column; flex: 1; min-height: 0;
}
/* The spare room belongs ABOVE the sliders, not between them and the buttons:
   the banners get air under them, and the four bars sit as one group with the
   CTAs at the foot of the panel. The first slider is the one that follows the
   pips row — :first-of-type would catch #readout instead. */
/* The four sliders share whatever is left under the banners rather than
   sitting in a block at the bottom with the spare room above them: each one
   grows by the same amount and centres its own label, bar and scale inside
   its share. flex-shrink 0 so that on a short window they fall back to their
   natural heights and the pane scrolls, instead of being squashed. */
#panel[data-pane="time"] .slider-ctrl {
  flex: 1 0 auto;
  display: flex; flex-direction: column; justify-content: center;
  margin-bottom: 0;
}
#panel[data-pane="time"] .time-ctas { margin: 0; }

/* ── The readout block ─────────────────────────────────────────────────────
   The figures and the shape of the day are what the pane reports; the
   sliders are what you do to it. Putting the report on its own grey ground,
   bled to the panel's edges and sitting flush under the sticky header,
   separates the two without needing a rule or a heading. */
#readout {
  /* -10px cancels #panel-head's bottom margin so the grey starts at the
     header's keyline rather than after a white strip */
  margin: -10px -16px 16px;
  padding: 24px 16px 26px;
  background: #f5f6f8;
}
#readout > .ctrl-group:last-child { margin-bottom: 0; }
#readout label { color: var(--text); margin-bottom: 16px; }
/* The shape of the day is the point of this chart; 48px flattened the
   difference between a 6am ramp-up and the midday plateau. */
#readout #histogram { height: 56px; }

/* Big enough to read from across the desk — these are the headline of the
   pane, not a caption to the sliders. */
#stats .stat b { font-size: 26px; letter-spacing: -.03em; }
#stats { margin-bottom: 16px; }

/* The histogram's baseline was a grey rule that disappears on a grey ground. */
#readout #histogram { border-bottom-color: #d8dde2; }
.hbar { background: #d3d9de; }

/* ── Day/night in the clock's own rail ─────────────────────────────────────
   The day used to be a separate strip above the slider, which meant two bars
   saying things about the same axis. It is now the rail itself: dark where
   the chosen date is dark, light where it is light, with --daynight built in
   app.js from the real solar altitude.

   The brand ramp every other slider uses would paint over it, so progress is
   shown the other way round here — the part of the day still to come is
   veiled, and the part you have travelled through stands at full colour. */
#clock { --sl-h: 6px; }   /* the same rail as the rest, day/night and all */
/* One rule per engine, NOT a selector list: a browser that does not know one
   of the pseudo-elements throws away the whole rule, so pairing the WebKit
   and Firefox track selectors here would leave BOTH engines unstyled. */
#clock::-webkit-slider-runnable-track {
  background:
    linear-gradient(90deg,
      rgba(255, 255, 255, 0) var(--pct),
      rgba(255, 255, 255, .62) var(--pct)) 0 0 / 100% 100% no-repeat,
    var(--daynight, #eceff1);
}
#clock::-moz-range-track {
  background:
    linear-gradient(90deg,
      rgba(255, 255, 255, 0) var(--pct),
      rgba(255, 255, 255, .62) var(--pct)) 0 0 / 100% 100% no-repeat,
    var(--daynight, #eceff1);
}

/* ── The banner slot ───────────────────────────────────────────────────────
   One banner fills the width. Two become a scroll-snap pair: the track bleeds
   out to the panel's edges so the second card is cropped by the panel itself
   rather than by a box drawn inside it, and pips sit underneath. */
/* The slot is a FIXED size — three lines of banner plus a pips row — whatever
   is in it. The sliders sit directly underneath, and a banner arriving or
   leaving used to shove them up and down the panel mid-drag. Both parts hold
   their space: the track is one banner tall, and the pips row keeps its
   height whether or not there are pips in it. */
:root { --note-h: 81px; }          /* 3 lines + padding, and room to spare
                                      so the last line is not shaved */
#notes {
  display: flex; gap: 8px;
  /* +2px of slack so sub-pixel rounding can never clip a card again */
  height: calc(var(--note-h) + 2px); flex: none;
  margin: 0 -16px; padding: 2px 16px 0;
  overflow-x: auto; overflow-y: hidden;
  scroll-snap-type: x mandatory; scroll-padding-left: 16px;
  scrollbar-width: none;                 /* the pips are the affordance */
}
#notes::-webkit-scrollbar { display: none; }
#notes[hidden] { display: flex; visibility: hidden; }   /* keeps its space */
/* border-box or the two cards come out different widths: flex-basis sizes the
   CONTENT box, and these two carry different padding and borders — which also
   breaks the fixed step the pips scroll by. */
/* margin: 0, not just margin-bottom — each banner brings its own 2px TOP
   margin from when they were stacked, and inside a fixed-height track that
   2px pushed the card past the content box and overflow:hidden shaved the
   bottom of every banner. */
#notes > * { flex: 0 0 100%; box-sizing: border-box;
             scroll-snap-align: start; margin: 0; }
/* Two up, the front card keeps the FULL width it has on its own: the pair
   should not shrink just because there are two of them. The track bleeds to
   the panel's edges, so the next card starts past the content column and the
   panel itself crops it to a sliver — enough to say it is there. */
#notes.multi > * { flex-basis: 100%; }
/* The card that is NOT in front is a way to reach it. */
#notes.multi > *:not(.is-current) { cursor: pointer; }

#notes-pips {
  display: flex; justify-content: center; align-items: center; gap: 6px;
  height: 6px; margin: 15px 0 12px;
}
#notes-pips.empty { visibility: hidden; }
#notes-pips .pip {
  width: 6px; height: 6px; padding: 0; border: 0; border-radius: 50%;
  background: #c3ccd3; cursor: pointer;
  transition: background .15s, transform .15s;
}
#notes-pips .pip.on { background: var(--brand, #064d90); transform: scale(1.25); }
#notes-pips .pip:focus-visible { outline: 2px solid var(--brand-line); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) {
  #notes { scroll-behavior: auto; }
  #notes-pips .pip { transition: none; }
  #notes-pips .pip.on { transform: none; }
}

/* ── The play button, while it is playing ──────────────────────────────────
   A keyline sweeping round the inside of the circle. The map moves slowly
   enough at 1× that a still frame and a running one look alike; this is the
   one thing on screen that always says the clock is going. It is a quarter
   arc — a full ring with three of its four border colours left transparent —
   rotated, so there is no SVG and nothing to keep in step with the icon. */
/* The arc is a conic gradient masked down to a 2px ring, which is what lets
   its LENGTH animate as well as its rotation — the two together are what
   makes a spinner read as a loader rather than as a rotating tick. A border
   arc can only spin at a fixed quarter. */
@property --ms-arc {
  syntax: "<angle>"; inherits: false; initial-value: 0deg;
}
#mapstack #ms-play::after {
  content: ""; position: absolute; inset: 3px;   /* inside the circle's edge */
  border-radius: 50%; pointer-events: none;
  /* a disc with its middle cut out */
  -webkit-mask: radial-gradient(farthest-side, #0000 calc(100% - 2px), #000 0);
          mask: radial-gradient(farthest-side, #0000 calc(100% - 2px), #000 0);
}
/* A stopwatch, not a loader: the arc sweeps once round from twelve o'clock at
   an even pace and starts again, and the ring itself does not rotate. A
   conic gradient begins at the top by default, so the hand starts where a
   stopwatch's would. */
#mapstack #ms-play.running::after {
  background: conic-gradient(var(--brand-deep, #051c23) var(--ms-arc, 90deg),
                             #0000 0);
  animation: ms-sweep 3s linear infinite;
}
/* Engines without @property cannot animate an angle, and keep a static
   quarter-arc instead of a moving one. */
@keyframes ms-sweep {
  from { --ms-arc: 0deg; }
  to   { --ms-arc: 360deg; }
}
@media (prefers-reduced-motion: reduce) {
  #mapstack #ms-play.running::after { animation: none; }
}

/* ── MapLibre's zoom group ─────────────────────────────────────────────────
   The + and − are drawn small inside a 38px button, which left them looking
   timid beside our own 19px icons above them. MapLibre draws them as
   background images, so the size is set here rather than on an <svg>. */
.maplibregl-ctrl-top-right .maplibregl-ctrl-group button.maplibregl-ctrl-zoom-in
  .maplibregl-ctrl-icon,
.maplibregl-ctrl-top-right .maplibregl-ctrl-group button.maplibregl-ctrl-zoom-out
  .maplibregl-ctrl-icon {
  background-size: 26px 26px;
}

/* The compass button resets the bearing and the pitch, so a plain rotate
   arrow says what it does. It keeps MapLibre's own inline rotation, so the
   icon still leans with the current bearing. */
.maplibregl-ctrl-top-right .maplibregl-ctrl-group button.maplibregl-ctrl-compass
  .maplibregl-ctrl-icon {
  background-size: 22px 22px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23051c23' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20.5 12a8.5 8.5 0 1 1-2.5-6'/%3E%3Cpath d='M20 3.2V7h-3.8'/%3E%3C/svg%3E");
}

/* ── The speed pill ────────────────────────────────────────────────────────
   Closed, it is one of the circles. Open, a white pill grows out to the left
   carrying the speed slider, with the circle sitting on its right end so the
   two read as one shape.

   The wrap holds the column's 38px width whatever the panel is doing — the
   panel is positioned out of flow, so opening it never nudges the play and
   theme circles sideways. */
#ms-speed-wrap { position: relative; width: 38px; height: 38px; flex: none; }
#ms-speed-panel {
  position: absolute; top: 0; right: 0; height: 38px;
  width: 38px;                      /* closed: exactly the circle */
  box-sizing: border-box;
  padding: 0;
  display: flex; align-items: center;
  background: #fff; border-radius: 999px;
  box-shadow: 0 1px 3px rgba(0, 0, 0, .18), 0 0 0 1px rgba(0, 0, 0, .06);
  overflow: hidden;
  opacity: 0; pointer-events: none;
  transition: width .26s cubic-bezier(.22, .9, .3, 1),
              padding .26s cubic-bezier(.22, .9, .3, 1), opacity .16s;
}
/* The padding lives in the OPEN state only: 60px of it against a 38px width
   wins on a border-box element, so a closed panel would sit 22px wider than
   the circle it is meant to be hiding behind. The right padding is the room
   the circle sits over. */
#mapstack.speed-open #ms-speed-panel {
  /* The right padding is the room the circle sits over — and the speed badge
     hangs off the circle's bottom-LEFT corner, so the slider has to stop
     clear of the badge, not just clear of the circle. */
  width: 208px; padding: 0 58px 0 16px; opacity: 1; pointer-events: auto;
}
/* two overlapping shadows on the same pill end read as a seam */
#mapstack.speed-open #ms-speed { box-shadow: none; background: transparent; }
#ms-speed-panel input[type="range"] {
  width: 100%; margin: 0; --sl-h: 6px; --sl-thumb: 16px;
}
@media (prefers-reduced-motion: reduce) {
  #ms-speed-panel { transition: opacity .16s; }
}

/* A place in the results is a different KIND of answer from a street, so it
   is marked as one rather than left to be told apart by its name. */
#street-results li.is-area span:first-child { font-weight: 600; }
#street-results li.is-area .loc {
  color: var(--brand, #064d90); font-weight: 500;
}

/* ── Browsing places and stations ──────────────────────────────────────────
   The list under the search box, for when you do not already know the name
   you want. Two tabs, two orderings, and a measured figure on every row. */
/* The grey block at the top of the Search pane, matching #readout on Date:
   what this pane is, and the search box, on their own ground. It bleeds to
   the panel's edges and sits flush under the sticky header. */
#brand-readout {
  margin: -10px -16px 20px;
  padding: 20px 16px 22px;
  background: #f5f6f8;
}
#brand-readout #street-search-group { margin-bottom: 0; }
#brand-readout .subtitle { margin-bottom: 14px; }
#area-browser { margin-top: 0; }
.browse-bar {
  display: flex; align-items: center; gap: 8px; margin-bottom: 18px;
}
/* A segmented control rather than two separate pills: one grey track with the
   current segment lifted out of it as a white card. The track says the two
   are one choice; the shadow says which one is in force. */
.browse-tabs {
  display: flex; gap: 0; flex: 1; min-width: 0;
  padding: 3px; border-radius: 12px; background: #eaedf1;
}
.browse-tabs button {
  flex: 1 1 0; min-width: 0; padding: 7px 10px;
  border: 0; border-radius: 9px; background: none;
  font: 600 11.5px/1.35 var(--body); color: #6b7b88; cursor: pointer;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  transition: background .18s, color .18s, box-shadow .18s;
}
.browse-tabs button:hover { color: var(--text); }
.browse-tabs button.on {
  background: #fff; color: var(--text);
  box-shadow: 0 1px 3px rgba(5, 28, 35, .14), 0 0 0 1px rgba(5, 28, 35, .04);
}
/* ── Sort ──────────────────────────────────────────────────────────────── */
.sort-wrap { position: relative; flex: none; }
#sort-btn {
  display: flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; padding: 0;
  border: 0; border-radius: 12px;
  background: #fff; box-shadow: inset 0 0 0 1px var(--line-soft, #e4e9ec);
  color: #6b7b88; cursor: pointer;
  transition: background .15s, color .15s;
}
#sort-btn svg { width: 16px; height: 16px; }
#sort-btn:hover { background: #f5f6f8; color: var(--text); }
#sort-btn.on { background: var(--brand-soft, #cdeeff); color: var(--brand); }
#sort-menu {
  position: absolute; top: calc(100% + 6px); right: 0; z-index: 6;
  min-width: 132px; padding: 4px;
  background: #fff; border: 1px solid var(--line-soft, #e4e9ec);
  border-radius: 10px; box-shadow: 0 8px 24px rgba(5, 28, 35, .16);
}
#sort-menu[hidden] { display: none; }
#sort-menu button {
  display: flex; align-items: center; gap: 7px; width: 100%;
  padding: 7px 9px; border: 0; border-radius: 7px;
  background: none; color: var(--text); cursor: pointer;
  font: 500 12px/1.3 var(--body); text-align: left;
}
#sort-menu button:hover { background: #f2f6f8; }
/* a tick on the one in force, drawn rather than imported */
#sort-menu button::before {
  content: ""; width: 12px; height: 12px; flex: none;
  background: no-repeat center / 12px 12px;
}
#sort-menu button.on {
  color: var(--brand); font-weight: 600;
}
#sort-menu button.on::before {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23064d90' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 12.5l5.5 5.5L20 6.5'/%3E%3C/svg%3E");
}
.browse-tabs button:focus-visible, #sort-btn:focus-visible,
#sort-menu button:focus-visible,
#area-list li:focus-visible { outline: 2px solid var(--brand-line); outline-offset: 2px; }

#area-list {
  list-style: none; margin: 0; padding: 0;
  max-height: 320px; overflow-y: auto;
  scrollbar-width: thin; scrollbar-color: #c3ccd3 transparent;
}
#area-list li {
  /* A fixed middle column, not `auto`: each row is its own grid, so an auto
     column is sized per ROW and "Town" would start further right than
     "Boston neighbourhood". A set width gives every row the same column. */
  display: grid; grid-template-columns: 1fr 108px auto;
  align-items: baseline; gap: 8px;
  padding: 7px 8px; border-radius: 7px; cursor: pointer;
  transition: background .12s;
}
#area-list li:hover { background: #f2f6f8; }
#area-list .an { font-size: 12.5px; color: var(--text); }
/* the kind sits at the start of its own column, so "Town" and "Boston
   neighbourhood" begin on the same line down the list */
#area-list .ak {
  font-size: 10.5px; color: #8794a0; white-space: nowrap;
  justify-self: start; text-align: left;
  overflow: hidden; text-overflow: ellipsis; max-width: 108px;
}
#area-list .at {
  font: 600 11.5px/1 var(--mono, ui-monospace, monospace);
  color: var(--brand); font-variant-numeric: tabular-nums;
  min-width: 34px; text-align: right;
}

/* ── The pill, right-aligned and stacked ───────────────────────────────────
   Off the centre line and over to the right, clear of the drawer on the
   left; the destinations run down the page under the wordmark rather than
   across from it. */
#sitehead {
  left: auto; right: 12px; transform: none;
  height: auto; border-radius: 18px;
}
#sitehead .shin {
  flex-direction: column; align-items: stretch;
  /* the gap here separates the wordmark from the destinations, and only
     those two — the links space themselves with #mainnav's own gap */
  height: auto; gap: 30px; padding: 13px 14px;
}
#sitehead .sbrand { justify-content: flex-start; padding-inline: 4px; }
#mainnav { flex-direction: column; align-items: stretch; gap: 2px; }
#mainnav a { text-align: left; padding: 7px 10px; }

/* The pill is taller than the 56px the reading pages were clearing, and it
   now hangs over their top-right corner. */
body[data-view="report"] #report { padding-top: 34px; }
body #report header.top { top: 0; }
#report section.an, #report [id] { scroll-margin-top: 24px; }

@media (max-width: 640px) {
  #sitehead { left: 10px; right: 10px; width: auto; }
}

/* ══ The site menu ═════════════════════════════════════════════════════════
   This build's menu differs from transit-pulse-v2's: instead of a pill in the
   top-right with the destinations stacked under the wordmark, it runs along
   the top-LEFT at the drawer's own width and sits directly above it, so the
   menu and the drawer read as one column down the left-hand side.

   These are overrides on the rules further up, which still describe the
   right-hand version — keeping them means the two builds' stylesheets stay
   diffable against each other. */
:root { --menu-h: 66px; }

#sitehead {
  left: auto; right: 12px; width: max-content;
  border-radius: 16px;
}
#sitehead .shin {
  flex-direction: column; align-items: stretch;
  gap: 14px; padding: 12px;
}
#sitehead .sbrand { padding-inline: 2px; }
/* The destinations sit UNDER the wordmark, still as one segmented control
   rather than two separate pills. */
#mainnav { margin-left: 0; flex: none; }

/* the two destinations as one segmented control */
#mainnav {
  flex-direction: row; align-items: stretch; gap: 0;
  padding: 3px; border-radius: 999px;
  background: rgba(255, 255, 255, 0.12);
}
#mainnav a {
  flex: 1; text-align: center; white-space: nowrap;
  padding: 6px 12px; border-radius: 999px;
  font-size: 12.5px; color: rgba(255, 255, 255, 0.72);
}
#mainnav a:hover { background: rgba(255, 255, 255, 0.10); color: #fff; }
#mainnav a.on {
  background: #fff; color: var(--brand-deep, #051c23); font-weight: 600;
}



/* ── The panes as tabs, not a dock ─────────────────────────────────────────
   This build has no #dock: the four panes are a tab strip at the top of the
   always-open drawer. The rules further up still describe the dock version —
   these override them, so the two builds stay diffable. */
#dock { display: none !important; }

/* The drawer no longer slides in and out of a closed state. */
#panel, body.dock-open #panel { transform: translate(0, -50%); opacity: 1; }

/* The menu is back over on the right, so the drawer has the full height of
   the map again. Stated on #panel[data-pane] as well as #panel: the per-pane
   rules carry an attribute selector and would otherwise outweigh this. */
#panel, #panel[data-pane] {
  top: 50%;
  height: calc(100% - 24px);
}

/* The header is the sticky lid the pane scrolls under. Its ground is grey so
   the white belongs to the tab strip alone — the strip keeps the panel's own
   top corners and bleeds to the header's edges, and anything below it (the
   API key banner) floats on the grey. */
#panel-head {
  display: flex; flex-direction: column; align-items: stretch;
  padding: 0 8px; margin: 0 -16px 10px; width: auto;
  /* the same grey as the transcript below it, so the banner reads as
     floating on one continuous ground rather than on a second shade */
  background: #f5f6f8;
}
#ptabs {
  /* 16px of side padding is the panel's own content column, so the first
     pill starts and the last pill ends exactly where the pane's text and
     sliders do. space-between puts the leftover width BETWEEN the tabs
     rather than stretching them, which is what keeps both ends anchored. */
  margin: 0 -8px; padding: 16px;
  justify-content: space-between;
  background: #fff; border-radius: 10px 10px 0 0;
}
/* the grey only needs to show where something is floating on it */
#panel-head { padding-bottom: 0; }
#panel[data-pane="ask"] #panel-head { padding-bottom: 10px; }
/* The transcript has a grey ground of its own and starts right under the
   header, so the 10px of panel white between them would read as a seam.
   Only this pane loses it; the others still want the gap under their tabs. */
#panel[data-pane="ask"] #panel-head { margin-bottom: 0; }
/* .ctrl-group's own 2px of top padding was the last white line between the
   header and the transcript's grey. */
#panel[data-pane="ask"] > details.psec[data-pane="ask"] > .ctrl-group {
  padding-top: 0;
}
/* One row, all four named. The labels are short enough to sit together —
   Filters / Search / Ask / Date need 264px of the 304 available — so nothing
   has to be revealed on hover. The full names ("Search location", "Ask
   Cambium", "Date & time") stay on each tab's title and aria-label, so the
   tooltip and any screen reader still get the whole thing. */
#ptabs { display: flex; align-items: center; gap: 2px; }
#ptabs .ptab {
  /* sized to their own words rather than stretched to equal quarters — the
     stretch is what was pushing the four names apart */
  flex: none; min-width: 0;
  display: flex; align-items: center; justify-content: center; gap: 3px;
  padding: 8px 7px; border: 0; border-radius: 999px;
  background: none; color: #7c8b94; cursor: pointer;
  font: 600 11px/1 var(--body); white-space: nowrap;
  transition: background .18s, color .18s;
}
#ptabs .ptab svg { width: 18px; height: 18px; flex: none; }
#ptabs .ptab:hover { background: #f2f6f8; color: var(--text); }
#ptabs .ptab.on { color: var(--brand); background: var(--brand-soft, #cdeeff); }
#ptabs .ptab:focus-visible { outline: 2px solid var(--brand-line); outline-offset: -2px; }

/* ── The API key strip ─────────────────────────────────────────────────────
   Its own line under the tabs — sharing their row would put it on top of the
   fourth tab — and on its own light blue ground, so the ask is written out
   and the button is left as one short verb. Only the Ask pane has it, so only
   that pane's header is the taller one. */
/* sticky, not relative: the pane has to scroll UNDER this. Sticky is also a
   containing block, so the key popup still positions against it. */
#panel-head { position: sticky; top: 0; z-index: 3; }
#key-bar { display: none; }
#panel[data-pane="ask"] #key-bar {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  margin: 6px 0 0; padding: 7px 8px 7px 12px;
  background: var(--brand-soft, #cdeeff); border-radius: 999px;
}
#key-bar-copy {
  font: 500 11.5px/1.3 var(--body); color: var(--brand, #064d90);
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
#panel[data-pane="ask"] #key-cta {
  flex: none; margin: 0; padding: 5px 14px;
  background: var(--brand, #064d90); border-color: var(--brand, #064d90);
  color: #fff;
}
#panel[data-pane="ask"] #key-cta:hover {
  background: var(--brand-line, #008cff); border-color: var(--brand-line, #008cff);
}
/* the strip carries the state in words, so the button needs no dot */
#panel[data-pane="ask"] #key-cta.set::before { content: none; }

/* ── The banners carry their own way out ───────────────────────────────────
   "Jump to 8am" belongs to "no service at this hour", and "Jump to current
   time" to the scenario note: each banner states a situation and offers the
   one action that leaves it. They used to sit in a row at the foot of the
   pane, where neither said which message it answered. */
:root { --note-h: 126px; }
#quiet-note, #scenario-note {
  /* one padding for the pair — they were carrying two different insets from
     when they were stacked notes rather than a matched set of cards */
  padding: 14px 15px;
  display: flex; flex-direction: column; align-items: flex-start; gap: 8px;
  /* the clamp lives on .note-text now, so the box itself can be plain */
  -webkit-line-clamp: none; line-clamp: none;
}
#scenario-note[hidden], #quiet-note[hidden] { display: none; }
.note-text {
  flex: 1; min-height: 0; overflow: hidden;
  display: -webkit-box; -webkit-box-orient: vertical;
  /* two lines: the copy is written to fit, so nothing is cut mid-sentence */
  -webkit-line-clamp: 3; line-clamp: 3;
}
#quiet-note .note-text b, #scenario-note .note-text b { display: block; }
#quiet-note .note-text span,
#scenario-note .note-text span { display: block; margin-top: 4px; }

.note-cta {
  flex: none; margin: 0 !important;
  padding: 5px 13px !important; border-radius: 999px; border: 0;
  font: 600 11px/1.45 var(--body); cursor: pointer; white-space: nowrap;
  transition: background .15s;
}
#scenario-note .note-cta { background: var(--brand); color: #fff; }
#scenario-note .note-cta:hover { background: var(--brand-line, #008cff); }
/* the amber banner's own ink, so the button belongs to the message it sits in */
#quiet-note .note-cta { background: #7a4a12; color: #fff; }
#quiet-note .note-cta:hover { background: #96601f; }

/* The row the two buttons used to live in. :empty does not match it — the
   markup leaves whitespace between the buttons — and there is nothing left
   in it worth showing once both have moved into their banners. */
.time-ctas { display: none; }

/* ── Info icons and their tip ──────────────────────────────────────────────
   An "i" beside a layer's name, carrying the legend or the note that used to
   be behind its expand arrow. Sections with nothing else in them lose the
   arrow entirely. */
/* Every summary reserves the arrow's column, whether or not it has one, so
   the info icons line up down the panel instead of stepping left on the rows
   that can expand.

   The arrow must NOT keep its own margin-left:auto. With two auto margins in
   one flex row the free space is split BETWEEN them, which left each icon at
   a distance that tracked its label's width. Only the icon pushes now; the
   arrow simply follows it. */
.psec > summary::after {
  flex: none; width: 12px; height: 12px; margin-left: 0;
  /* assets/Group 3.svg — a chevron rather than the "▸" character, so it
     matches the weight of the info icon beside it. Its colour is baked in:
     a background image cannot inherit currentColor. */
  content: "";
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 109 63' fill='none' stroke='%2362737d' stroke-width='16' stroke-linecap='round'%3E%3Cpath d='M3.5 3.5L54.5 59.5'/%3E%3Cpath d='M54.5 59.5L105.5 3.5'/%3E%3C/svg%3E")
    center / 11px 7px no-repeat;
  transition: transform .15s;
}
/* closed points down, open points back up */
.psec[open] > summary::after { transform: rotate(180deg); }
.psec.no-expand > summary { cursor: default; }
/* the column stays, the chevron does not */
.psec.no-expand > summary::after { background-image: none; }

/* The ring is part of the icon now, so the button itself carries no border
   — otherwise there would be two circles. */
.info-i {
  margin-left: auto; flex: none;
  width: 18px; height: 18px; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border: 0; border-radius: 50%;
  background: none; color: #62737d; cursor: pointer;
  transition: color .15s, background .15s;
}
.info-i svg { width: 16px; height: 16px; display: block; }
.psec > summary > .info-i { position: relative; right: 11px; }
.info-i:hover { color: var(--brand); }
.info-i.on { color: var(--brand); background: var(--brand-soft, #cdeeff); }
.info-i:focus-visible { outline: 2px solid var(--brand-line); outline-offset: 2px; }
/* the Air passengers row: label on the left, its icon on the right */
.label-row { display: flex; align-items: center; gap: 6px; margin-bottom: 6px; }
.label-row .chk-label { margin: 0; }

.infotip {
  position: fixed; z-index: 80;
  max-height: min(60vh, 460px); overflow-y: auto;
  padding: 12px 14px; box-sizing: border-box;
  background: #fff; border-radius: 10px;
  box-shadow: 0 10px 30px rgba(5, 28, 35, .22), 0 0 0 1px rgba(5, 28, 35, .06);
  font-size: 12px; line-height: 1.5; color: var(--text);
  scrollbar-width: thin; scrollbar-color: #c3ccd3 transparent;
}
.infotip[hidden] { display: none; }
.infotip > :first-child { margin-top: 0; }
.infotip > :last-child { margin-bottom: 0; }
.infotip .note, .infotip .sub-note { margin: 0 0 8px; }

/* ── The month rail is the year ────────────────────────────────────────────
   Four seasonal bands instead of a progress fill. The blue ramp says "how
   far along" — true of a clock, not of a month, where what matters is which
   season you have landed in. The thumb marks the month.

   The stops are the MIDPOINTS between one season's last month and the next
   season's first: the slider runs 1–12, so month m sits at (m-1)/11, and
   Feb→Mar falls at 13.6%, May→Jun at 40.9%, Aug→Sep at 68.2%, Nov→Dec at
   95.5%. Winter appears at both ends because the year wraps.

   One rule per engine, never a selector list — an unknown pseudo-element
   would throw the whole thing away in both. */
/* Each band carries its OWN veil alpha, so a season change cross-fades the
   two bands that are actually involved and leaves the other two alone. A
   single veil ramped over the whole bar is what produced the white flash:
   at full strength it whited out every band at once, not just the one
   changing. Registered so the alphas can be interpolated at all. */
@property --a-w  { syntax: "<number>"; inherits: true; initial-value: 0.68; }
@property --a-sp { syntax: "<number>"; inherits: true; initial-value: 0.68; }
@property --a-su { syntax: "<number>"; inherits: true; initial-value: 0.68; }
@property --a-au { syntax: "<number>"; inherits: true; initial-value: 0.68; }

#month {
  transition: --a-w .3s ease, --a-sp .3s ease,
              --a-su .3s ease, --a-au .3s ease;
  /* full strength: this is what the season you are IN looks like */
  --winter: #6fb3e0;
  --spring: #63c168;
  --summer: #efc23f;
  --autumn: #db8f52;
  /* Hard stops: one season ends and the next begins, at the midpoints
     between their months. Only the HIGHLIGHT dissolves (see the veil
     below) — the seasons themselves are four flat bands. */
  --seasons: linear-gradient(90deg,
    var(--winter) 0 13.6%, var(--spring) 13.6% 40.9%,
    var(--summer) 40.9% 68.2%, var(--autumn) 68.2% 95.5%,
    var(--winter) 95.5% 100%);
}

/* ── Histogram tip ─────────────────────────────────────────────────────────
   The count for the hour under the pointer, above the bar it belongs to. */
.hbar-tip {
  position: fixed; z-index: 80; pointer-events: none;
  padding: 5px 9px; border-radius: 7px;
  background: var(--brand-deep, #051c23); color: #fff;
  font: 500 11px/1.35 var(--body); white-space: nowrap;
  box-shadow: 0 4px 14px rgba(5, 28, 35, .28);
}
.hbar-tip[hidden] { display: none; }
.hbar-tip b { font-weight: 700; font-variant-numeric: tabular-nums; }
.hbar-tip span {
  margin-left: 7px; padding-left: 7px;
  border-left: 1px solid rgba(255, 255, 255, .25);
  color: rgba(255, 255, 255, .7);
}
/* the bar under the pointer lifts out of the row */
#readout .hbar { transition: background .12s; }
#readout .hbar:hover { background: #aab4bc; }
#readout .hbar.active:hover { background: var(--brand-line, #008cff); }

/* The veil, one alpha per band. The bands themselves never move. */
#month::-webkit-slider-runnable-track {
  background:
    linear-gradient(90deg,
      rgba(255, 255, 255, var(--a-w))  0 13.6%,
      rgba(255, 255, 255, var(--a-sp)) 13.6% 40.9%,
      rgba(255, 255, 255, var(--a-su)) 40.9% 68.2%,
      rgba(255, 255, 255, var(--a-au)) 68.2% 95.5%,
      rgba(255, 255, 255, var(--a-w))  95.5% 100%)
      0 0 / 100% 100% no-repeat,
    var(--seasons);
}
#month::-moz-range-track {
  background:
    linear-gradient(90deg,
      rgba(255, 255, 255, var(--a-w))  0 13.6%,
      rgba(255, 255, 255, var(--a-sp)) 13.6% 40.9%,
      rgba(255, 255, 255, var(--a-su)) 40.9% 68.2%,
      rgba(255, 255, 255, var(--a-au)) 68.2% 95.5%,
      rgba(255, 255, 255, var(--a-w))  95.5% 100%)
      0 0 / 100% 100% no-repeat,
    var(--seasons);
}
@media (prefers-reduced-motion: reduce) { #month { transition: none; } }

/* ── Phone width: the pill, the notice and the map ─────────────────────────
   #sitehead is position:fixed, so it is out of flow and the full-width static
   notice was rendering behind it, cut mid-sentence. Measured at 375x812
   before this block: notice 0,0 -> 375,70; pill 179,10 -> 363,101. The
   injected PREVIEW badge is not the cause — hide it and the overlap is
   unchanged — but it does change the pill's WIDTH, which matters below.

   1. The pill collapses to one row. Stacked three deep it was 123px tall and
      a third of the screen wide, which is what put it over the notice. The
      wordmark goes: it cannot have the room here, and the notice already says
      what the page is.

   2. The PREVIEW badge comes out of the pill's width. build_preview.py
      injects it into the masthead, where as a flex child it made the pill
      273px on a preview against 168px on the canonical site — so any space
      the notice reserved would have been right in one and wrong in the other,
      and the preview would not have shown the real layout. Positioned under
      the pill instead: same width in both, still legible. The selector
      matches nothing on the canonical site, where no badge is injected.

   3. The notice reserves the pill's corner with a floated spacer rather than
      padding, so only the two lines actually beside the pill are narrowed and
      the rest use the full width — padding would have indented every line and
      made the notice needlessly tall.

   The map is deliberately left at 100dvh. `#map-wrap{height:42vh}` in the
   640px block above looks like it is being overridden by mistake, and it is
   not: it was written for the single-scrolling-page layout, where the map sat
   on top and the analyses ran underneath it. v3 replaced that with two views,
   and on the map view `#report` is display:none — there is no "below" to
   leave room for any more. Shrinking the map here just puts 371px of white
   under it. The analyses are reached by the Report destination in the pill.

   Verified at 375x812: pill 197,8 -> 365,49; no line of the notice overlaps
   it. */
@media (max-width: 640px) {
  #sitehead {
    left: auto; right: 10px; top: 8px;
    height: auto; width: auto; border-radius: 18px;
  }
  #sitehead .shin {
    position: relative;
    flex-direction: row; align-items: center;
    height: auto; gap: 4px; padding: 5px 7px;
  }
  #sitehead .sbrand { display: none; }
  /* the injected PREVIEW badge — preview only, see 2 above */
  #sitehead .shin > span { position: absolute; top: 100%; right: 0; margin-top: 5px; }

  #mainnav { flex-direction: row; align-items: center; gap: 2px; }
  #mainnav a { padding: 5px 10px; font-size: 12.5px; text-align: center; }

  #small-screen-note::before { content: ""; float: right; width: 180px; height: 44px; }
}

/* ── section rail ─────────────────────────────────────────────────────
   A pill of dots against the right edge that opens sideways on hover or
   keyboard focus. It is one element that grows, not a panel that appears:
   the labels are width-clipped, so nothing moves on the page behind it. */
#report .secnav{
  /* Same width and the same right inset as the save button above it, so the
     dots and the icon sit on one vertical line. */
  position:fixed;right:10px;top:50%;transform:translateY(-50%);z-index:40;
  box-sizing:border-box;width:38px;
  display:flex;flex-direction:column;align-items:center;gap:2px;
  padding:12px 0;border-radius:999px;
  /* Semi-transparent over the page, with a blur behind it so the labels
     stay legible against whatever scrolls under the rail. */
  background:rgba(255,255,255,.78);
  -webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);
  box-shadow:0 8px 28px rgba(5,28,35,.12);
  transition:padding .25s cubic-bezier(.22,1,.36,1),
             width .25s cubic-bezier(.22,1,.36,1);
}
/* row-reverse: the dot stays against the right edge and the name opens to
   its left, so the column of dots does not shift as the rail grows. */
#report .secnav-item{
  display:flex;flex-direction:row-reverse;align-items:center;gap:0;
  padding:5px 0;text-decoration:none;color:var(--ink);
  font:500 12.5px/1.2 var(--mono);white-space:nowrap;
}
#report .secnav-item i{
  flex:none;width:7px;height:7px;border-radius:50%;
  background:var(--ink);opacity:.4;
  transition:opacity .2s,transform .2s;
}
#report .secnav-item:hover i,#report .secnav-item:focus-visible i{opacity:1}
#report .secnav-item.on i{opacity:1;transform:scale(1.35)}
/* max-width, not display: a hidden label cannot be transitioned, and the
   rail has to grow from the dots rather than snap to its open width. */
#report .secnav-item span{
  max-width:0;opacity:0;overflow:hidden;
  text-align:right;
  transition:max-width .3s cubic-bezier(.22,1,.36,1),
             opacity .2s ease,margin-right .3s cubic-bezier(.22,1,.36,1);
}
/* .open is set in planning.js, not :hover — see the note there. */
/* Open, the rail grows leftwards to hold the names: it drops the fixed width
   and goes back to aligning its dots on the right edge. */
#report .secnav.open{
  width:auto;align-items:flex-end;padding:12px 15px 12px 18px;
  border-radius:20px;
}
#report .secnav.open .secnav-item span{
  max-width:22rem;opacity:.85;margin-right:11px;
}
#report .secnav-item:hover span,
#report .secnav-item.on span{opacity:1}
#report .secnav-item:focus-visible{
  outline:2px solid var(--brand-line);outline-offset:3px;border-radius:6px;
}
@media (prefers-reduced-motion:reduce){
  #report .secnav,#report .secnav-item span,#report .secnav-item i{
    transition:none}
}
/* No room for a rail beside the content at these widths. */
/* Save and share the whole report: the simulation's own control geometry —
   38px, circular, white, on the same 10px right inset — placed above the
   section rail by planning.js. */
#report .repsave{
  position:fixed;right:10px;z-index:41;
  width:38px;height:38px;padding:0;border:0;border-radius:50%;
  display:flex;align-items:center;justify-content:center;
  background:#fff;color:var(--dim);cursor:pointer;
  box-shadow:0 8px 28px rgba(5,28,35,.12);
  transition:color .15s,box-shadow .15s}
#report .repsave svg{width:17px;height:17px;fill:currentColor}
#report .repsave:hover{color:var(--ink);box-shadow:0 8px 28px rgba(5,28,35,.2)}
#report .repsave:focus-visible{outline:2px solid var(--brand-line);
  outline-offset:2px}
/* Its menu opens to the LEFT of the button — the button is against the
   window's right edge — and carries the same items a section head does. */
#report .repsave-pop{
  position:fixed;right:56px;z-index:41;min-width:150px;
  display:flex;flex-direction:column;padding:6px;border-radius:12px;
  background:rgba(255,255,255,.95);
  -webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);
  box-shadow:0 10px 26px rgba(5,28,35,.16);
  opacity:0;transform:translateX(6px);pointer-events:none;
  transition:opacity .16s ease,transform .2s ease}
#report .repsave-pop.open{opacity:1;transform:none;pointer-events:auto}
#report .repsave.open{color:var(--ink);box-shadow:0 8px 28px rgba(5,28,35,.2)}
@media (prefers-reduced-motion:reduce){#report .repsave-pop{transition:none}}
@media (max-width:1100px){#report .repsave-pop{display:none}}
@media (max-width:1100px){#report .repsave{display:none}}
@media (max-width:1100px){#report .secnav{display:none}}

/* ── chart tooltip ────────────────────────────────────────────────────
   One element for the whole report (see planning.js). Fixed, so it is not
   clipped by a card's overflow, and pointer-events:none so it never steals
   the hover from the shape that summoned it. */
#report .charttip{
  position:fixed;left:0;top:0;z-index:60;pointer-events:none;
  max-width:22rem;padding:7px 10px;border-radius:8px;
  background:rgba(5,28,35,.94);color:#fff;
  font:500 12px/1.35 var(--mono);white-space:nowrap;
  box-shadow:0 6px 20px rgba(5,28,35,.22);
  opacity:0;transform:translateY(3px);
  transition:opacity .12s ease,transform .12s ease;
}
#report .charttip.on{opacity:1;transform:none}
#report [data-tip]{cursor:default}
@media (prefers-reduced-motion:reduce){
  #report .charttip{transition:none}
}

/* ── phone ─────────────────────────────────────────────────────────────
   Checked at 375: two cards abreast left the bar labels rendering at 2.7px
   and squeezed the ring's chart to nothing, because a key-beside-chart row
   keeps its flex direction however narrow it gets. Below 768 every paired
   row stacks and the key goes back above its chart. */
@media (max-width:767px){
  #report .statmod .g2.ratio,
  #report .statmod .g2.maprow,
  #report .statmod .g2.mappair,
  #report .statmod-in > .secgrid + .g2{
    grid-template-columns:minmax(0,1fr)}
  #report .card .lleft{flex-direction:column;align-items:stretch;gap:14px}
  #report .card .lleft > .legend{order:-1;margin:0 !important}
  #report .card .lleft > svg{width:100%;height:auto !important;
    margin-top:0 !important;margin-bottom:0 !important}
  #report .secmap:has(.mapwrap) .mapwrap{width:100%;margin:0}
}
