/*
 * Millburn Green dashboard components.
 *
 * Every colour and radius here comes from the theme's tokens. The plugin
 * defines no palette of its own -- if the brand changes, it changes in
 * style.css and theme.json and these components follow.
 *
 * Fallback values are supplied on each var() so a component still renders
 * sensibly if it is ever placed on a site without the child theme active.
 */

.mg-component { margin-block: 28px; }
.mg-loading { color: var(--muted, #65716D); font-size: 14px; }
.mg-error {
  color: var(--flood, #B66A55);
  font-size: 14px;
  border-left: 3px solid var(--flood, #B66A55);
  padding-left: 12px;
}

/* Section heading shared by components ------------------------------------ */
.mg-head { display: flex; align-items: baseline; gap: 14px; flex-wrap: wrap; margin-bottom: 14px; }
.mg-head h3 { font-size: 30px; }
.mg-head .mg-meta { margin-left: auto; color: var(--muted, #65716D); font-size: 12px; }

/* Widget anatomy -- §11 --------------------------------------------------
   The parts every widget on the dashboard is built from, so they stay
   consistent as more are added:

     .mg-w-mark      the icon column         always present, may be empty
     .mg-w-icon      the subject badge       optional
     .mg-w-label     the widget name         14px, uppercase
     .mg-w-value     the headline figure     serif, large
     .mg-w-caption   what qualifies it       12px, title case, regular
     .mg-w-detail    supporting figures      12px, muted

   Any part may be omitted. The label sits ABOVE the value -- it names the
   widget before the number, rather than captioning it afterwards. The icon
   sits to the LEFT of all three, its top level with the label.               */
/* 270px, against the 190px this started at: the icon column plus its gap takes
   80px out of the card, and below roughly this width "71° Partly Cloudy" can
   no longer hold one line. */
/* The column count comes from renderCurrent, not from auto-fit.
   A 270px card and its gap means four need 1280px of container; an iPad has
   976, so auto-fit gave three across and a stranded fourth. gridCols() picks
   the fewest rows and then the evenest split, so four become two by two until
   there is genuinely room for a single row.
   The fallbacks are how many fit at each width, so a row rendered without the
   properties still behaves. */
.mg-metrics {
  display: grid;
  grid-template-columns: repeat(var(--c-sm, 1), minmax(0, 1fr));
  gap: 20px;
}
@media (min-width: 640px) {
  .mg-metrics { grid-template-columns: repeat(var(--c-md, 2), minmax(0, 1fr)); }
}
@media (min-width: 1024px) {
  .mg-metrics { grid-template-columns: repeat(var(--c-lg, 3), minmax(0, 1fr)); }
}
@media (min-width: 1280px) {
  .mg-metrics { grid-template-columns: repeat(var(--c-xl, 4), minmax(0, 1fr)); }
}
.mg-w {
  display: flex;
  align-items: flex-start;   /* the icon's top sits level with the label */
  gap: 16px;
  background: var(--white, #fff);
  border: 1px solid var(--border, #DDE3DE);
  border-radius: var(--radius-md, 12px);
  padding: 10px;
}
/* The header is sticky, so a jump that puts the target at the very top of the
   viewport puts it underneath the header. This is what keeps the forecast's
   own heading visible when the weather widget jumps down to it. */
#forecast { scroll-margin-top: 96px; }
html { scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

/*
 * A widget that leads somewhere.
 *
 * The whole tile is the anchor, so it has to be told to stop looking like a
 * link -- inherit the colour and drop the underline, or every figure on the
 * card turns blue and underlined. What marks it instead is the hover: the
 * border and a small lift, which is how the rest of the site says clickable.
 */
.mg-w.is-link {
  color: inherit;
  text-decoration: none;
  transition: border-color .15s ease, box-shadow .15s ease, transform .15s ease;
}
.mg-w.is-link:hover {
  border-color: var(--green-solid, #527C46);
  box-shadow: 0 3px 10px -4px rgba(38, 49, 44, .22);
  transform: translateY(-1px);
}
.mg-w.is-link:focus-visible {
  outline: 2px solid var(--green-deep, #174F48);
  outline-offset: 2px;
}
/* A pointer-driven lift is a distraction to anyone who has asked for less
   motion, and the border change alone still says the same thing. */
@media (prefers-reduced-motion: reduce) {
  .mg-w.is-link { transition: none; }
  .mg-w.is-link:hover { transform: none; }
}

/* Fixed width whether or not it holds an icon -- see the note in widget(). */
.mg-w-mark { flex: 0 0 64px; height: 64px; }
.mg-w-icon { width: 64px; height: 64px; display: block; }
.mg-w-body { min-width: 0; }

.mg-w-label {
  color: var(--muted, #65716D);
  font: 600 14px/1.2 var(--font-ui, sans-serif);
  letter-spacing: .08em;
  text-transform: uppercase;
}

/* Serif numerals.
   NOTE: this departs from §3, which specifies Montserrat 500 for the dashboard
   number. Deliberate -- worth reflecting in the style guide so the two do not
   disagree.
   Weight is 400, not 500: Newsreader is loaded at 400 only, and asking for 500
   makes the browser synthesise a fake bold, which smears a serif at 48px. */
.mg-w-value {
  display: flex;
  /* Wraps as a fallback; between 640 and 1279 it is stacked outright, below.
     The row gap is what makes either readable -- it was 0, so a caption that
     came off the figure's line sat tight under its baseline. */
  flex-wrap: wrap;
  align-items: baseline;
  gap: 2px 8px;
  margin-top: 6px;
  font: 400 48px/0.95 var(--font-editorial, Georgia, "Times New Roman", serif);
  color: var(--green-deep, #174F48);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
}

/* Half the size of the figure it belongs to. A degree or foot mark set at the
   full 48px reads as another digit; at half it reads as a unit.

   The raise is because the glyph scales with the type: at 48px the degree sits
   near the cap height of the numerals on its own, and shrinking it drops it
   with everything else. 0.42em of its OWN size puts the top back level with
   the figures. */
.mg-w-figure { white-space: nowrap; }

/*
 * ON A TABLET THE CAPTION GOES UNDER THE FIGURE:
 *
 *     62 deg                 instead of      62 deg  Mostly Cloudy
 *     Mostly Cloudy
 *
 * Side by side this row is the widest thing in the tile, so it sets the
 * tile's width -- which is why four of them would not fit across an iPad and
 * came out two by two, spending half the screen and twice the height on a row
 * that had room for one line. Stacked, the row needs only the figure's width
 * and all four fit.
 *
 * Stacked outright rather than left to wrap. Wrapping is per tile, so it
 * would have dropped "Mostly Cloudy" under its figure while "Good" stayed
 * beside its own -- four tiles in a row, disagreeing about where the second
 * line goes.
 *
 * Bounded at both ends on purpose. Below 640 the grid is one column and the
 * tile is wide enough to read across; at 1280 and up four fit without giving
 * anything up. Only between those does the trade pay.
 */
@media (min-width: 640px) and (max-width: 1279px) {
  .mg-metrics .mg-w-value {
    flex-direction: column;
    align-items: flex-start;
  }
}

/*
 * AND AT FOUR ACROSS, the detail line stacks too:
 *
 *     Feels 70 deg            instead of    Feels 70 deg | Wind 12 mph
 *     Wind 12 mph
 *
 * Four tiles across 976px leaves each one a 129px text column once the icon
 * and the padding are taken out, and "Feels 70 | Wind 12 mph" wants about
 * 175. Stacking the value row alone was not enough; this is the line that
 * would have wrapped raggedly instead.
 *
 * The separator goes with it -- a rule between two numbers side by side, and
 * nothing at all once they are one above the other.
 *
 * ONLY here. At 768 the grid is two across, each tile is 352px, and the
 * detail reads perfectly well on one line; stacking it there would add a row
 * of height to buy nothing.
 */
@media (min-width: 1024px) and (max-width: 1279px) {
  .mg-metrics .mg-w-detail {
    display: flex;
    flex-direction: column;
    gap: 2px;
  }
  .mg-metrics .mg-w-detail .mg-w-sep { display: none; }
}

.mg-w-unit {
  font-size: 50%;
  vertical-align: 0.62em;   /* of its own size, so the top sits at cap height */
  letter-spacing: 0;
}

/* Rides alongside the value ("71° Partly Cloudy"), so it needs its own font
   shorthand to escape the 48px serif it sits inside. Title case is
   how the source writes it -- no text-transform, which would mangle "NW" and
   "T-storms" into something the forecaster did not say. */
.mg-w-caption {
  font: 400 12px/1.3 var(--font-ui, sans-serif);
  color: var(--muted, #65716D);
  letter-spacing: 0;
}

.mg-w-detail {
  margin-top: 10px;
  color: var(--muted, #65716D);
  font: 400 12px/1.4 var(--font-ui, sans-serif);
}
.mg-w-detail .mg-w-v { color: var(--ink, #1D2B27); font-variant-numeric: tabular-nums; }
.mg-w-detail .mg-w-sep { margin: 0 8px; opacity: .45; }

/* Semantic accents -- §7: green is the brand colour, not every data category */
.mg-w.is-water  .mg-w-value { color: var(--water, #3295B5); }
.mg-w.is-energy .mg-w-value { color: var(--energy, #C69A45); }
.mg-w.is-flood  .mg-w-value { color: var(--flood, #B66A55); }
.mg-w.is-air    .mg-w-value { color: var(--green, #679B58); }

/* Tables ------------------------------------------------------------------ */
.mg-card { background: var(--white, #fff); border: 1px solid var(--border, #DDE3DE); border-radius: var(--radius-md, 12px); overflow: hidden; }
.mg-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }
.mg-table { border-collapse: collapse; width: 100%; font-variant-numeric: tabular-nums; }
.mg-table th, .mg-table td { padding: 11px 14px; text-align: right; white-space: nowrap; border-bottom: 1px solid var(--border, #DDE3DE); font-size: 14px; }
.mg-table th:first-child, .mg-table td:first-child { text-align: left; position: sticky; left: 0; background: var(--white, #fff); }
.mg-table thead th {
  font: 600 11px/1.2 var(--font-ui, sans-serif);
  letter-spacing: .08em; text-transform: uppercase;
  color: var(--muted, #65716D);
}
/* th AS WELL AS td. The first column of a body row is a th -- it is the row's
   name -- so dropping the rule from the td alone left a line under the last
   gauge's name with nothing under its level and flow: a rule that stopped a
   third of the way across the table. */
.mg-table tbody tr:last-child td,
.mg-table tbody tr:last-child th { border-bottom: 0; }
.mg-table tbody tr:hover td, .mg-table tbody tr:hover th { background: #F3F7F3; }
/* A row that acts like a link should look like one under the pointer. Only
   where a row HAS a link in its name cell -- the rows that do not are not
   targets and must not claim to be. See renderStations(). */
.mg-table tbody tr:has(th a) { cursor: pointer; }
/* No rule under a row link. Every name in the first column is a link, so the
   underline marked the whole column rather than distinguishing anything in it.
   Hover carries the affordance instead. */
.mg-table a { color: inherit; text-decoration: none; }
.mg-table a:hover { color: var(--green-deep, #174F48); }
.mg-na { color: #A9B2AD; font-size: 12px; letter-spacing: .02em; }
.mg-stale { color: var(--energy, #C69A45); }

/* Alerts -- §7 puts risk in terracotta ------------------------------------ */
.mg-alert {
  border: 1px solid var(--flood, #B66A55);
  border-left-width: 5px;
  background: #FBF0EC;
  border-radius: var(--radius-md, 12px);
  padding: 16px 18px;
  margin-bottom: 12px;
}
.mg-alert h4 { margin: 0 0 4px; font: 600 16px/1.3 var(--font-ui, sans-serif); color: #7A3B2C; }
.mg-alert .mg-when { font-size: 13px; color: #8A5344; }
.mg-alert details { margin-top: 8px; }
.mg-alert summary { cursor: pointer; font-size: 13px; color: #7A3B2C; }
.mg-alert pre { white-space: pre-wrap; font: inherit; font-size: 13px; margin: 8px 0 0; }

/* Map -- §14: the base map stays quiet ------------------------------------ */

/* Legend column then map, 20/80.
   1fr/4fr rather than literal 20%/80%: percentages are taken from the full
   width and would add the gap on top, overflowing the container. Fractions
   divide what is left AFTER the gap, so the split stays 20/80 whatever the
   gap is. minmax(0,...) lets the map column shrink below the intrinsic width
   of its own contents, which a grid track otherwise refuses to do. */
/* No gap: the legend and the map are one block, the legend reading as the map's
   own surface carried out to the left rather than a card parked beside it. With
   the gap gone, 1fr/4fr is now literally 20/80 of the width. */
.mg-map-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 4fr);
  gap: 0;
}
.mg-map-aside {
  background: var(--white, #fff);
  border: 1px solid var(--border, #DDE3DE);
  /* Outer corners only. The right edge runs straight into the map. */
  border-radius: var(--radius-md, 12px) 0 0 var(--radius-md, 12px);
  /* No border on the seam -- the map's own left border draws it, and two
     adjacent 1px borders would read as a 2px rule down the middle. */
  border-right: 0;
  /* 10 across, not 28. This column is a fifth of the grid, so at 1024 -- an
     iPad on its side -- 28 each side left 120px for the text beside a swatch
     and "Station not reporting" needs about 143. Every key wrapped. At 10 it
     is 156 and they fit.
     The vertical 28 stays: that is the space above and below the keys, and
     nothing was wrong with it. */
  padding: 28px 10px;
}
/*
 * FOOTNOTES, ACROSS THE FOOT OF THE BLOCK.
 *
 * A third grid item spanning both tracks, so it clears the legend and the map
 * rather than living inside either. In the legend these were the paragraphs
 * that made the column taller than the map beside it; here they are two short
 * lines at full width.
 *
 * order:3 is for the stacked layout below 860px, where the map is given
 * order:1 and the legend order:2. Without it an unordered item sorts before
 * both and the footnotes would open the block.
 */
.mg-map-foot {
  grid-column: 1 / -1;
  order: 3;
  margin-top: 12px;
}
.mg-map-foot .mg-map-note { margin: 0; }
.mg-map-foot .mg-map-note + .mg-map-note { margin-top: 6px; }

.mg-map-title {
  margin: 0;
  font-size: 26px;          /* smaller than a page h2: the column is narrow */
  line-height: 1.2;
  color: var(--green-deep, #174F48);
}
.mg-map-intro {
  margin: 10px 0 0;
  font: 400 14px/1.55 var(--font-ui, sans-serif);
  color: var(--muted, #65716D);
}

.mg-map {
  /* A stacking context of its own. Leaflet gives its panes z-index 400 and its
     controls 1000, and without this those compete directly with the page --
     the zoom buttons drew over the sticky header, which is only z-index 100.
     Contained here, the whole map sits at 0 and nothing inside it can reach
     past the header again, however Leaflet numbers its internals. */
  position: relative;
  z-index: 0;
  min-height: 400px; height: 400px; width: 100%;
  border-radius: 0 var(--radius-md, 12px) var(--radius-md, 12px) 0;
  overflow: hidden;
  border: 1px solid var(--border, #DDE3DE);
  background: #EAEEE9;
}

/* Below this the legend column is too narrow to read, so it stacks above the
   map at full width. 860px is where 20% of the container stops fitting
   "Weather station -- temperature F" on two lines. */
/* TABLETS: a quarter to the legend rather than a fifth.
   1fr/4fr is 20/80, which is right on a desktop and too mean in between. At
   1024 -- an iPad on its side -- a fifth is 195px and leaves 156 for text
   beside a swatch; a quarter is 244 and leaves 205. The map gives up 49px of
   its 781, which it does not miss, and every key stops wrapping.
   Bounded above by 1280, where 1fr/4fr already leaves 204px and the map is
   better off with the width. */
@media (min-width: 861px) and (max-width: 1279px) {
  .mg-map-layout { grid-template-columns: minmax(0, 1fr) minmax(0, 3fr); }
}

@media (max-width: 860px) {
  .mg-map-layout { grid-template-columns: 1fr; }
  /* Map first, legend under it. Stacked, the aside was a screen of text before
     you reached the thing it describes. The DOM keeps the aside first, so a
     screen reader still meets the heading before the map; only the visual
     order changes.

     The tree map joins them: it uses the same layout, so it needs the same
     reordering and the same corners, or it stacks legend-first with its
     rounding still set for a seam that is no longer there. */
  .mg-map,
  .mg-treemap,
  .mg-floodmap,
  .mg-roadmap,
  /* The ballots choropleth and the prism map use the same layout, so they
     need the same reordering and the same corners -- without it they stack
     legend-first with their rounding still set for a seam that is no longer
     there. */
  .mg-ballotmap,
  .mg-map-layout > .mg-prism { order: 1; border-radius: var(--radius-md, 12px) var(--radius-md, 12px) 0 0; }
  .mg-map-aside {
    order: 2;
    border-radius: 0 0 var(--radius-md, 12px) var(--radius-md, 12px);
    border-right: 1px solid var(--border, #DDE3DE);
    border-top: 0;
    /* The 10px gutter is on the base rule now -- it turned out to be wanted at
       every width, not only once the aside stacks. */
  }
}
/* A FINGER GOING DOWN THE PAGE SCROLLS THE PAGE.
   On a touch screen a scroll and a pan are the same gesture -- one finger
   moving -- and whoever claims it wins. Leaflet claims all of it: with drag
   and zoom both enabled it sets touch-action:none on the container, which
   tells the browser to hand over every gesture, so a swipe that began on the
   map panned the map and the page stayed where it was.
   pan-y gives vertical back to the browser. The map keeps horizontal drag and
   pinch-zoom, which is the trade: the town is wider than it is tall and the
   view is bounded anyway, so up-and-down was the direction it needed least.
   Four classes deep because Leaflet's own rule is three and its stylesheet
   loads after this one -- a tie would go to Leaflet.
   Touch only. touch-action does not apply to a mouse, so dragging with one
   still pans in every direction. */
.mg-map.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
  touch-action: pan-y pinch-zoom;
}

/*
 * 28px, down from 46. The pins carry two or three characters and were sized
 * for a label twice that; on a town with eleven weather stations and four
 * gauges the circles ran into each other at every zoom a reader actually
 * uses. Area falls to a third of what it was, which is what overlap is
 * measured in.
 *
 * 28 rather than 23 -- half -- because the floor is set by the widest label
 * the map can produce, not the usual one. "68 deg" and "2.1 ft" are 18px at
 * this size and would fit a 24px pin; a flood crest reading "10.9 ft" is
 * 24px, and a pin that fits every day except the day it matters is not
 * smaller, it is broken.
 */
.mg-pin {
  position: relative; width: 28px; height: 28px;
  display: flex; align-items: center; justify-content: center;
  font: 600 10px/1 var(--font-ui, sans-serif); color: #fff;
  border-radius: 50% 50% 50% 0; transform: rotate(-45deg);
  border: 2px solid rgba(255,255,255,.92);
  /* Tighter with the pin: the old shadow was half the new pin's height and
     read as a smudge under it rather than as lift. */
  box-shadow: 0 1px 5px rgba(0,0,0,.30); cursor: pointer;
}
.mg-pin span { transform: rotate(45deg); white-space: nowrap; }
/* The degree and the foot mark, small AND RAISED. They are a third of the
   label's width at full size and carry none of its meaning, so shrinking them
   is most of what let the pin come down without the number going with it.
   Shrinking alone dropped them, though: both glyphs sit high in their own em
   box, so at 0.72em and no lift the degree landed halfway up the digits and
   read as a character rather than a unit. vertical-align is in the mark's own
   em, as on .mg-w-unit, and puts its top back at the digits' cap height. */
.mg-pin i { font-style: normal; font-size: .72em; vertical-align: .3em; }
.mg-pin.is-gauge { border-radius: 7px 7px 7px 0; background: var(--water, #3295B5); }
.mg-pin.is-offline { background: #9AA5A0 !important; }
.leaflet-tooltip.mg-tip {
  font: 13px/1.45 var(--font-ui, sans-serif);
  padding: 11px 13px; border-radius: var(--radius-md, 12px);
  border: 1px solid var(--border, #DDE3DE); background: var(--white, #fff);
  color: var(--ink, #26312C); box-shadow: 0 4px 16px rgba(0,0,0,.16);
}
.mg-tip h4 { margin: 0 0 6px; font: 600 13px/1.3 var(--font-ui, sans-serif); }
.mg-tip table { border-collapse: collapse; }
.mg-tip td { border: 0; padding: 1px 0; font-size: 12.5px; }
.mg-tip td.k { color: var(--muted, #65716D); padding-right: 14px; text-align: left; }
.mg-tip td.v { text-align: right; font-variant-numeric: tabular-nums; }

/* Legend ------------------------------------------------------------------
   Swatch colours are classes, not inline styles. Inline cannot carry a media
   query or a dark-mode override, and hardcoded hex duplicates the tokens --
   so a brand change would have to be chased through the JavaScript. */
.mg-legend { display: flex; gap: 18px; flex-wrap: wrap; margin-top: 12px; font-size: 12px; color: var(--muted, #65716D); }
.mg-legend.is-tight { margin: 0 0 6px; }
/* In the map aside the keys read as a list, not a strip. The column is narrow
   enough that a label wraps, so the swatch aligns to the FIRST line rather
   than centring on the block and floating between the two. */
/* NO margin-top. It was separating the legend from whatever happened to be
   above it, which is spacing that belongs to the thing above -- a heading
   already carries its own, and the two collapsed into whichever was larger,
   so a group's gap depended on which element preceded it rather than on any
   rule. Where a legend does need air above it, the element before it asks for
   it: see .mg-map-more. */
.mg-legend.is-stacked { flex-direction: column; gap: 10px; align-items: flex-start; }
.mg-legend.is-stacked span { align-items: flex-start; }
.mg-legend.is-stacked i { margin-top: 2px; }
@media (max-width: 860px) {
  .mg-legend.is-stacked { flex-direction: row; gap: 10px 18px; }
  /* The ramp takes its own line. As an ordinary flex item it is sized by its
     content, so a key landed beside it and the bar was squeezed to about half
     the column. */
  .mg-legend.is-stacked .mg-key-scale { flex: 0 0 100%; }
}
.mg-legend span { display: inline-flex; align-items: center; gap: 7px; }
.mg-legend i { width: 12px; height: 12px; border-radius: 50%; display: inline-block; flex: 0 0 auto; }
.mg-legend i.sq { border-radius: 3px; }

.mg-legend i.is-weather { background: var(--green, #679B58); }
/* The two map overlays. A line, not a dot: both are drawn as lines on the map
   and a round swatch would describe neither. */
.mg-legend i.is-river {
  background: none; border-radius: 0; height: 0;
  border-top: 3px solid #2E86AB; align-self: center;
}
.mg-legend i.is-township {
  background: none; border-radius: 0; height: 0;
  border-top: 2px dashed var(--green-deep, #174F48); align-self: center;
}
.mg-legend i.is-offline { background: #9AA5A0; }

/* The temperature scale, as a ramp rather than one swatch. A weather pin takes
   whichever band its reading falls in -- seven of them -- so a single green dot
   said the amber pins were something other than weather stations.
   Band colours come from TEMP_STOPS in the JS; only the frame is here. */
.mg-key-scale { display: block; }
.mg-key-text { display: block; }
.mg-ramp {
  display: flex;
  height: 8px;
  margin-top: 7px;
  border-radius: 3px;
  overflow: hidden;
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .12);
}
.mg-ramp i { display: block; width: auto; height: 100%; border-radius: 0; }
.mg-ramp-ticks {
  display: flex;
  margin-top: 3px;
  font-size: 9.5px;
  font-variant-numeric: tabular-nums;
  opacity: .8;
}
/* Each tick carries its band's flex weight and pushes its number to the right
   edge, which is the boundary the number actually marks. */
.mg-ramp-ticks span { text-align: right; white-space: nowrap; }
.mg-legend i.is-gauge   { background: var(--water, #3295B5); }
.mg-legend i.is-hi      { background: var(--flood, #B66A55); }
.mg-legend i.is-avg     { background: var(--green, #679B58); }
.mg-legend i.is-lo      { background: var(--water, #3295B5); }
.mg-legend i.is-normal  { background: var(--muted, #65716D); }

/* Legend keys on the charts are drawn SVG, not styled boxes -- see lineKey().
   Nothing to colour or dash here; the swatch only has to sit on the text. */
.mg-key-line { flex: 0 0 auto; display: block; }
/* Pushes itself and everything after it to the far end, so a dual-axis legend
   sits over the axis each series is drawn against.
   Dropped once the legend has to wrap: there the slack lands mid-line and
   splits the pair it exists to keep together -- Solar pushed right, Wind
   orphaned on the line below. Four keys in order read better than that. */
.mg-legend .to-right { margin-left: auto; }

/* NARROW: one column per axis, each over the axis it belongs to.
   On a wide screen .to-right does this by pushing the gigawatt pair to the
   far end. Once the legend has to wrap that stops working -- the slack lands
   mid-line and splits the pair, Solar pushed right and Wind orphaned below --
   so it used to be dropped and all four ran in order. That read as four
   unrelated keys when it is two pairs, one per axis.
   Column-major grid instead: items 1 and 2 fill the left column, 3 and 4 the
   right, and space-between puts each column over its own axis. */
/* NARROW: the legend goes under the chart.
   The same reasoning as the map aside -- stacked, a key is a paragraph of text
   standing between the reader and the thing it describes, and it means nothing
   until the thing has been seen. Order only; the DOM keeps the legend first so
   a screen reader still meets it before the chart. */
@media (max-width: 700px) {
  .mg-panel.is-chart .mg-panel-body { display: flex; flex-direction: column; }
  .mg-panel.is-chart .mg-chart  { order: 1; }
  .mg-panel.is-chart .mg-legend { order: 2; margin: 12px 0 0; }
  .mg-panel.is-chart .mg-note   { order: 3; }
  /* The daily table is appended after everything in the markup, and with no
     order of its own it defaulted to 0 -- ahead of the chart. On a phone
     "Daily readings, 31 days - view data details" was therefore the first
     thing in the card, offering a way into numbers the reader had not been
     shown yet. Last, which is where the DOM already puts it. */
  .mg-panel.is-chart .mg-daily  { order: 4; }
}

@media (max-width: 700px) {
  .mg-legend.is-axes {
    display: grid;
    grid-auto-flow: column;
    grid-template-rows: auto auto;
    justify-content: space-between;
    /* 16, not 6. Turned on their sides the samples are vertical strokes, and
       at the old spacing the one above ran into the one below and read as a
       single long line through both keys. */
    row-gap: 16px;
    column-gap: 12px;
  }
  /* margin-left:auto inside a grid cell pushes the item, not the column. The
     grid is doing that job now. */
  .mg-legend.is-axes .to-right { margin-left: 0; }
  /* Right edges aligned with the right axis, not ragged against it -- and the
     sample after the label rather than before it, so the two swatches sit at
     the right edge of the column, nearest the axis they belong to. The left
     pair already sits that way against the left axis; this makes the two
     halves mirror each other instead of both pointing the same way.
     row-reverse, not reordered markup: the DOM stays sample-then-label in
     both columns, which is the order a screen reader wants. */
  .mg-legend.is-axes > span:nth-child(n+3) {
    justify-self: end;
    flex-direction: row-reverse;
  }
  /* The swatch on its side. A rotated element keeps its unrotated box, so a
     30x10 sample still claims 30px of width while drawing 10 -- the negative
     margins hand the other 20 back, or the label would sit a thumb away. */
  .mg-legend.is-axes .mg-key-line {
    /* scaleX shortens the line along its own length, 30px to 20px, and leaves
       the stroke at its full 2px. Setting width:20px instead would scale the
       whole drawing and thin the stroke to 1.3 with it, which is how a key
       stops matching the line it is a key for. */
    transform: rotate(90deg) scaleX(.667);
    margin: 0 -10px;
  }
}

/* Basemap fills, not our data: these are the Protomaps "light" flavour's own
   values. Re-check them if that flavour changes or the library is upgraded, or
   the key quietly stops matching the map.
     park_b  #9CD3B4  parks, reservations, playgrounds -- the saturated green
     water   #80DEEA
   NOT wood_a #D0DED0, which is the pale wash over the whole wooded suburb. It
   covers far more of the map than the parks do, so picking the commonest green
   picks that one -- and it reads as grey. Frequency is not the same as
   meaning. The hairline makes a light fill visible at 12px on a white card. */
.mg-legend i.is-park      { background: #9CD3B4; box-shadow: inset 0 0 0 1px rgba(0,0,0,.14); }
.mg-legend i.is-waterbody { background: #80DEEA; box-shadow: inset 0 0 0 1px rgba(0,0,0,.14); }

/* The chart panel's body is tighter than the table panels': the SVG carries
   its own margins, so the usual padding would double up on three sides. */
.mg-panel.is-chart .mg-panel-body { padding: 14px 16px 6px; }
/* THE SAME TRAP AS web/station.html. `hidden` is styled only by the browser's
   own stylesheet, so any author rule setting display beats it -- and the rule
   right below sets display on every chart, which would leave an svg on the
   page after the code had hidden it.

   Scoped to this stylesheet's own components rather than set globally: it is
   loaded into WordPress pages whose markup is not ours, and a page-wide
   !important on [hidden] is a bigger hammer than the bug. */
.mg-component [hidden] { display: none !important; }

.mg-chart { display: block; width: 100%; }

/* Footnote ---------------------------------------------------------------- */
.mg-note { margin-top: 10px; font-size: 12.5px; color: var(--muted, #65716D); }
/* Links in a note, on .mg-panel-link's reasoning: a note is a small muted
   paragraph, and at 12.5px colour alone is not enough to mark a link as one.
   Underlined from the start rather than on hover.

   These are the first links to appear in a note. The panel title links that
   deliberately carry no underline are styled by their own class and are not
   touched by this. */
.mg-note a {
  color: var(--green-deep, #174F48);
  font-weight: 600;
  text-decoration: underline;
  text-underline-offset: 2px;
}
.mg-note a:hover { text-decoration-thickness: 2px; }

/* Year slider, in a chart panel's header ---------------------------------- */
/*
 * A slider without progress: the two ends are named, the thumb marks a place
 * between them, and the year it is on rides underneath it.
 *
 * Left to itself the browser fills the track to the left of the thumb. That
 * fill says "this much of something" -- a volume, a level, a progress bar --
 * when what is meant is a position on an axis of years, where left is not
 * less than right. So every part of the control is drawn here. accent-color
 * is gone with the fill: asking for it is what produces one.
 */
/*
 * The bar over a chart: what the lines mean on the left, what sets them on
 * the right.
 *
 * The sliders had a row to themselves above the legend, which stacked two
 * rows of furniture over the chart and read top-heavy. The keys are one line
 * of text and the sliders are taller than that, so setting them side by side
 * costs no height at all -- the row was always going to be the sliders' tall.
 *
 * Not in the panel header: that carries a title as long as the panel itself.
 */
.mg-chartbar {
  display: flex; flex-wrap: wrap;
  align-items: flex-start;
  gap: 10px 30px;
  margin-bottom: 6px;
}
/* Level with the slider captions beside it. The readout used to sit under the
   thumb, so 4px was enough; now it sits above the track and takes the whole
   row down with it, leaving the legend stranded at the top of the bar. */
.mg-chartbar .mg-legend.is-tight { margin: calc(var(--readout, 26px) + 4px) 0 0; }
/* Beside the keys, NOT pushed to the far end. At 1360px an auto margin left
   about six hundred pixels of nothing between the legend and the controls,
   and the bar stopped reading as one thing. Whitespace is better spent at the
   end of the row than down the middle of it. */
.mg-controls {
  display: flex; flex-wrap: wrap; gap: 8px 30px;
  align-items: flex-start;
}
/* How far the slider track sits below the top of its row: the readout's own
   height (13px at 1.3 plus 2px of padding each side) and 5px of air under it.
   Named once because the caption, both end labels, the track, the legend
   beside them and the figure at the end of the row all have to agree on it,
   and six hand-kept 26s drift apart.

   On the bar rather than on .mg-year, because the legend and the figure are
   not inside a slider; .mg-year keeps its own copy for the sliders that are
   used outside a chart bar. */
.mg-chartbar { --readout: 26px; }
.mg-year {
  display: flex; align-items: flex-start; gap: 9px;
  --readout: 26px;
}
/* Which slider this is. Two unlabelled sliders are a puzzle. */
.mg-year-caption {
  font: 600 11px/1.3 var(--font-ui, sans-serif);
  letter-spacing: .04em; text-transform: uppercase; color: var(--muted, #65716D);
  margin-top: calc(var(--readout) + 6px); white-space: nowrap;
}
/* The range, named where it starts and stops. This is what the fill was
   standing in for -- two numbers say more than a bar that looks 60% full. */
.mg-year-end {
  font: 500 12px/1 var(--font-ui, sans-serif);
  font-variant-numeric: tabular-nums;
  color: var(--muted, #65716D);
  margin-top: calc(var(--readout) + 6px);   /* level with the track below the readout */
  white-space: nowrap;
}
/* Holds the readout and the track it travels over. Its height is the pair's,
   so the legend below is not walked on. */
.mg-year-scale { position: relative; display: block; height: 47px; --f: 0; }
/* Six years do not need the width that sixty-one do. Short enough to give the
   row back some length, long enough that six stops are still six
   distinguishable places rather than one thumb's worth of travel. */
.mg-year.is-short .mg-year-slider { width: 88px; }

.mg-year-slider {
  -webkit-appearance: none;
  appearance: none;
  width: 150px;
  height: 20px;             /* the pointer target, not the track */
  display: block;
  margin: var(--readout) 0 0;   /* the readout sits in this space, above */
  background: none;
  cursor: pointer;
}
.mg-year-slider::-webkit-slider-runnable-track {
  height: 6px; border-radius: 3px; background: var(--border, #DDE3DE);
}
.mg-year-slider::-moz-range-track {
  height: 6px; border-radius: 3px; background: var(--border, #DDE3DE);
}
/* Firefox paints its fill through a pseudo-element of its own, so dropping
   accent-color is not enough to be rid of it. */
.mg-year-slider::-moz-range-progress { height: 6px; background: transparent; }
/* An upright bar rather than a dot: it reads as "this place on the line",
   where a circle reads as a bead that has been pushed along it. */
.mg-year-slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 8px; height: 20px; border-radius: 4px;
  background: var(--green-deep, #174F48);
  margin-top: -7px;         /* (track 6 - thumb 20) / 2, to straddle the line */
}
.mg-year-slider::-moz-range-thumb {
  width: 8px; height: 20px; border-radius: 4px; box-sizing: border-box;
  border: 0;
  background: var(--green-deep, #174F48);
}
/* Removing the UA appearance takes the focus ring with it, and this is worth
   driving from the keyboard -- arrow keys step a year at a time. */
.mg-year-slider:focus { outline: none; }
.mg-year-slider:focus-visible { outline: 2px solid var(--green-deep, #174F48); outline-offset: 4px; border-radius: 4px; }

/*
 * The year, ABOVE the thumb and moving with it.
 *
 * Above, because below is where the finger is. On a phone the thumb is under
 * a fingertip for the whole of a drag, and a readout beneath it was covered
 * by the hand doing the dragging -- so the one number that answers "which
 * year am I on" was readable only after letting go.
 *
 * Centred on the thumb's travel, which runs from half a thumb in to half a
 * thumb from the end -- 4px and 8px being the thumb's half-width and width.
 * A plain percentage would drift off the thumb at both extremes, and the ends
 * are exactly where a reader checks it against the labels.
 */
.mg-year-value {
  position: absolute;
  top: 0;
  left: calc(4px + (100% - 8px) * var(--f));
  transform: translateX(-50%);
  padding: 2px 7px;
  border-radius: 5px;
  background: var(--green-deep, #174F48);
  color: #fff;
  font: 600 13px/1.3 var(--font-ui, sans-serif);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  pointer-events: none;     /* never in the way of a drag that runs past it */
}
/* The reference readout says "1931-1960", not "1931", so at the ends of the
   track it reaches past them. It sits a row ABOVE the end labels, so it passes
   over them rather than into them, and the panel's own padding absorbs the
   rest. */
.mg-ref-label { font-variant-numeric: tabular-nums; }

/*
 * The figure at the end of a control row: how far off the whole year is.
 *
 * Pushed to the far end with an auto margin, which is the one place on this
 * bar that whitespace belongs -- the sliders read as a pair and this reads as
 * their answer, so a gap between them is doing work that a gap down the
 * middle of the legend was not.
 *
 * Aligned to the tracks, not to the top of the row: the captions and the end
 * labels are already a readout's height down, and this is the same kind of
 * thing they are.
 */
/*
 * BOTTOM-ALIGNED WITH THE SLIDER LABELS, by arithmetic rather than by eye.
 *
 * An end label's text ends at (--readout + 6px of margin + its own 12px line),
 * so --readout + 18px down the row. This block is 36px tall -- the two lines
 * below have exact pixel line-heights so that it is, and so that changing a
 * font size cannot quietly break the alignment -- and putting its bottom on
 * that same line means starting it 18px above where the labels start.
 */
.mg-anomaly {
  display: flex; flex-direction: column; gap: 0;
  margin-left: auto;
  /* 16px value line + 14px sub = 30px tall, and its bottom belongs on the
     same line as the end labels', which is --readout + 18px down the row. */
  margin-top: calc(var(--readout, 26px) - 12px);
  text-align: right; white-space: nowrap;
}
/* Tabular, so the number does not shuffle sideways as the slider moves it
   through 9.9 to 10.0 -- the figure is being watched while it changes.
   Exact pixel line-heights, not ratios: see the alignment note above.

   12px rather than the 19 it started at. At 19 a single figure was a headline;
   the temperature one now carries two of them and a slash, and a headline that
   long stops being a headline and starts being a line of text competing with
   the chart it belongs to. Small and bold and coloured is enough to find. */
.mg-anomaly-value {
  font: 700 12px/16px var(--font-ui, sans-serif);
  font-variant-numeric: tabular-nums;
  color: var(--ink, #26312C);
}
/* Warm and cool: the chart's own two series colours, so the figure agrees
   with the bars under it. */
.mg-anomaly.is-up .mg-anomaly-value   { color: #B5561E; }
.mg-anomaly.is-down .mg-anomaly-value { color: #2A6F97; }
/*
 * RAIN READS THE OTHER WAY ROUND. Up and down are neutral facts about
 * temperature -- a warm year is orange and a cold one blue, and neither
 * colour is a verdict. Rain is not neutral: a surplus is the good outcome and
 * a deficit is a drought, and red-and-green is what everyone already reads
 * that as. Temperature keeps orange and blue, so the page is not claiming a
 * warm year is a good one.
 *
 * --green-solid is the project's white-on-green token, 4.85:1 as text here.
 * The red is #C62828, 5.62:1, picked over the nearer #C0392B and #B3261E
 * because it is the furthest from the warmer orange above -- a deficit has to
 * read as a different colour, not as a darker shade of "warm".
 */
.mg-anomaly.is-rain.is-up .mg-anomaly-value   { color: var(--green-solid, #527C46); }
.mg-anomaly.is-rain.is-down .mg-anomaly-value { color: #C62828; }
.mg-anomaly-sub {
  font: 500 11px/14px var(--font-ui, sans-serif);
  font-variant-numeric: tabular-nums; color: var(--muted, #65716D);
}
/* Only ever seen on a phone -- see the media query below and anomalyFor. */
.mg-anomaly-lead { display: none; }

/*
 * ONE LINE, RANGED LEFT, ON A PHONE.
 *
 * Two right-aligned lines is a desktop arrangement: it works because the
 * readout sits at the end of a row of controls with the chart's end labels to
 * line its baseline up against. Narrow, that row wraps, the readout gets a
 * line of its own, and a right-aligned two-line block under a left-aligned
 * slider is the only thing on the card ranged the other way -- with its widest
 * line, the one naming the reference period, hard against the right edge.
 *
 * Flat, left, and with the word "Annual" in front, it reads as the sentence it
 * always was: Annual +0.8F / +0.5C vs 1991-2020.
 */
@media (max-width: 640px) {
  .mg-anomaly {
    flex-direction: row;
    align-items: baseline;
    gap: 6px;
    margin-left: 0;
    /* Not the calc the wide layout uses -- that one drops a two-line block
       onto the end labels' baseline, and there are no end labels beside a
       wrapped row to line up with. 25px is the gap that reads as "this
       belongs under the sliders" rather than "this is the next thing". */
    margin-top: 25px;
    text-align: left;
    /* It is one line of three parts now, and on a 320px screen the reference
       period is the part that should wrap rather than overflow. */
    white-space: normal;
    flex-wrap: wrap;
  }
  /* THE SAME GUTTER AS THE PANEL BODY. .mg-panel.is-chart drops its own side
     padding under 640 so the chart can use the full width, which is right for
     a chart and wrong for the row of sliders above it -- they were left
     hard against the card's edge while the title and the note below kept
     their margin. The padding goes back on the controls alone. */
  .mg-controls { padding: 0 16px; }
  .mg-anomaly-lead {
    display: inline;
    font: 500 11px/14px var(--font-ui, sans-serif);
    color: var(--muted, #65716D);
  }
}

/* The anomaly strip under the monthly temperature chart. Short on purpose --
   it answers one question and the chart above it is the one with the detail. */
.mg-chart.is-anomaly { margin-top: 2px; }

/* Every station's chart, stacked ----------------------------------------- */
/* One column, whatever the width. A 31-day chart needs its width more than the
   page needs two of them side by side -- at half a desktop the day labels
   start dropping out, and a chart you cannot read the dates on is a shape. */
.mg-charts { display: grid; gap: 20px; }

/* Dashboard ground -- §12 ------------------------------------------------- */
body.mg-dashboard { background: var(--dashboard-bg, #F2F4F0); }

@media (max-width: 640px) {
  /* The chart runs edge to edge; only the legend keeps the gutter.
     The SVG already carries its own margins -- 42px on the left for the axis
     labels -- so the body's padding was a second inset on top of that, and on
     a phone the two together took a sixth of the width before the plot began. */
  .mg-panel.is-chart .mg-panel-body { padding-left: 0; padding-right: 0; }
  .mg-panel.is-chart .mg-legend { padding-inline: 16px; }
  /* The note needs its gutter back for the same reason, and never had one --
     the rule above took the body's padding away and only the legend was given
     it back, so a footnote ran to both edges of the phone. */
  .mg-panel.is-chart .mg-note { padding-inline: 10px; }

  /* Two up, not one. Four full-width cards is four screens of scrolling before
     the map, and each one is mostly empty to the right of its number.
     The count now comes from --c-sm, which is 1 -- overridden here, because
     this is about the phone being cramped rather than about the cards not
     fitting, and it is the one place a row is deliberately tighter than the
     arithmetic would choose. */
  .mg-metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }

  /* The icon comes down with the card. At 56px it took more than a third of a
     half-width card, and the text had nowhere to go. */
  .mg-w { gap: 10px; }
  .mg-w-mark { flex-basis: 40px; height: 40px; }
  .mg-w-icon { width: 40px; height: 40px; }

  /* Caption under the figure rather than beside it. Half a phone's width has
     no room for "71 degrees" and "Partly Cloudy" on one line, and wrapping
     them left the caption hanging off the numeral's baseline. */
  .mg-w-value {
    font-size: 34px;
    flex-direction: column;
    align-items: flex-start;
    gap: 2px 0;
  }
  .mg-w-label { font-size: 11px; letter-spacing: .06em; }
  .mg-w-detail { margin-top: 8px; }
  .mg-w-detail .mg-w-sep { margin: 0 5px; }

  .mg-head h3 { font-size: 27px; }
}

/* Panels -- the three widgets below the map ------------------------------- */

/* Three across on a wide screen, stacking as the room runs out. 340px is where
   the forecast's five columns stop fitting; the two tables are wider than that
   and scroll inside their card, which is what .mg-scroll is for. */
.mg-panels {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
  gap: 20px;
  margin-block: 28px;
}
/* The shortcodes emit .mg-component wrappers; they are plumbing, not layout,
   so they pass their grid cell straight through to the panel. */
.mg-panels .mg-component { margin-block: 0; display: flex; }
.mg-panels .mg-component > .mg-panel { flex: 1; }

.mg-panel {
  display: flex;
  flex-direction: column;
  background: var(--white, #fff);
  border: 1px solid var(--border, #DDE3DE);
  border-radius: var(--radius-md, 12px);
  overflow: hidden;
}
.mg-panel-head {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 16px 22px;
  border-bottom: 1px solid var(--border, #DDE3DE);
}
.mg-panel-icon { width: 40px; height: 40px; flex: 0 0 40px; display: block; }
/* A heading that happens to be a link. No colour change, no underline: the
   panel title is a title first, and marking it up as a link should not turn
   it into one visually. The underline on hover is the whole affordance. */
/* The icon, pointing where the title points.
   display:contents so the anchor draws no box of its own: .mg-panel-icon is a
   flex item with `flex: 0 0 40px`, and an anchor around it would become the
   flex item instead and take that sizing away from the image. This way the
   image keeps every rule it already had and the link is simply wrapped round
   what is painted. */
.mg-panel-iconlink { display: contents; cursor: pointer; }
.mg-panel-titlelink { color: inherit; text-decoration: none; }
.mg-panel-titlelink:hover { text-decoration: underline; text-underline-offset: 3px; }
.mg-panel-titlelink:focus-visible { outline: 2px solid var(--green-deep, #174F48); outline-offset: 3px; }
.mg-panel-title {
  margin: 0;
  font: 400 21px/1.2 var(--font-editorial, Georgia, serif);
  color: var(--green-deep, #174F48);
}
/* Title and timestamp stack beside the icon, so the stamp reads as belonging
   to the panel it names rather than floating at the far end of the header. */
.mg-panel-heading { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
/* Pushed to the far end of the header, and allowed to drop onto its own line
   on a narrow screen rather than squeezing the title into two words. */
.mg-panel-control { margin-left: auto; display: flex; align-items: center; gap: 10px; }
/* A pointer to where the heading leads, on the heading's own line.
   Quiet: it is a signpost, not the panel's subject. It sits in the control
   slot, which already takes the far end of the header, so it needs no float
   and cannot fall out of the flow the way one would. */
.mg-panel-hint {
  font: 500 12px/1.3 var(--font-ui, sans-serif);
  color: var(--muted, #65716D);
  text-decoration: none; white-space: nowrap;
}
.mg-panel-hint span { opacity: .7; }
/*
 * a.mg-panel-hint, NOT .mg-panel-hint. A hint is either one link -- "Historical
 * view" -- or a span holding two, "Humidity | Wind". Underlining the container
 * on hover is right for the first and wrong for the second: it underlined both
 * links and the bar between them at once, which reads as one link to one place,
 * or as two charts that are really the same chart. The pair underlines whichever
 * link the pointer is actually on, which is what the charts themselves do.
 */
a.mg-panel-hint:hover { color: var(--green-deep, #174F48); text-decoration: underline; }
.mg-panel-hint:focus-visible { outline: 2px solid var(--green-deep, #174F48); outline-offset: 2px; border-radius: 3px; }
/* Two hints in one slot. The span is the container, so the colour and size
   rules above still describe the pair; the links inside carry their own hover
   and their own focus ring. */
.mg-panel-hint a { color: inherit; text-decoration: none; }
.mg-panel-hint a:hover { color: var(--green-deep, #174F48); text-decoration: underline; }
.mg-panel-hint a:focus-visible {
  outline: 2px solid var(--green-deep, #174F48); outline-offset: 2px; border-radius: 3px;
}
.mg-hint-sep { margin: 0 7px; opacity: .45; }
@media (max-width: 560px) {
  /* TWO RULES LESS. The panel already has an outline, and inside it the header
     and the daily table were each drawing another. On a wide card those read
     as structure; at phone width the panel is barely wider than one line of
     text, and three stacked horizontals in 300px read as a table of contents
     for something with one entry. The spacing does the dividing. */
  .mg-panel-head { border-bottom: 0; }
  /* .mg-daily's own border-top is declared further down this file at the same
     specificity, so it has to be turned off from below it, not here. */

  /* The old fallback, for a header with nothing in the control slot. */
  .mg-panel-head { flex-wrap: wrap; }
  .mg-panel-control { margin-left: 0; width: 100%; }

  /*
   * "HUMIDITY | WIND" JOINS THE "LAST 31 DAYS" LINE, RANGED RIGHT.
   *
   * The control slot is a sibling of the heading, not of the meta inside it,
   * so flex could only ever drop it onto a line of its own -- a third row for
   * two words, under a line that had the whole width to itself. What the eye
   * wants is the span on the left and the way out on the right, sharing the
   * row under the title.
   *
   * display:contents on the heading lifts the title and the meta out to be
   * grid items of the header itself, which is the only way those two and the
   * control can be placed on one grid without moving anything in the markup.
   *
   * :has() so this touches ONLY the headers that have a control -- today that
   * is the chart panels and nothing else. Where it is not supported the rule
   * is dropped whole and the header keeps the wrapping flex layout above,
   * which is what it does now.
   *
   * column-gap:0 with the 12px carried as margins instead: a header with no
   * icon would otherwise pay for an empty first column.
   */
  .mg-panel-head:has(> .mg-panel-control) {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    column-gap: 0;
    row-gap: 3px;
    align-items: center;
  }
  .mg-panel-head:has(> .mg-panel-control) .mg-panel-heading { display: contents; }
  /* The icon keeps the full height of both rows beside it, as it has beside
     the flex column. */
  .mg-panel-head:has(> .mg-panel-control) .mg-panel-icon {
    grid-area: 1 / 1 / 3 / 2;
    margin-right: 12px;
  }
  .mg-panel-head:has(> .mg-panel-control) .mg-panel-title { grid-area: 1 / 2 / 2 / 4; }
  .mg-panel-head:has(> .mg-panel-control) .mg-panel-meta  { grid-area: 2 / 2 / 3 / 3; }
  .mg-panel-head:has(> .mg-panel-control) > .mg-panel-control {
    grid-area: 2 / 3 / 3 / 4;
    /* RIGHT IS FOR THE LINKS ONLY. justify-self on the control, not
       text-align on the row -- the span keeps its place at the left. */
    justify-self: end;
    margin-left: 12px;
    width: auto;
  }
}
.mg-panel-meta {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  font: 400 11px/1.3 var(--font-ui, sans-serif);
  color: var(--muted, #65716D);
}
/* Nothing to show there leaves no gap behind it. */
.mg-panel-meta:empty { display: none; }
/* The way out of a panel, on the meta line beside the span it belongs to.
   Underlined from the start: it sits in an 11px muted row where colour alone
   would be the only thing separating a link from the text next to it, and at
   that size colour alone is not enough to notice. */
.mg-panel-link {
  color: var(--green-deep, #174F48);
  font-weight: 600;
  text-decoration: underline;
  text-underline-offset: 2px;
}
.mg-panel-link:hover { text-decoration-thickness: 2px; }
.mg-live {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 9px;
  border-radius: var(--radius-pill, 999px);
  background: #EAF3E8;
  color: #3F6B36;
  font-weight: 600;
}
.mg-live::before {
  content: "";
  width: 6px; height: 6px;
  border-radius: 50%;
  background: currentColor;
}
/* A reading that has stopped arriving is not "Live". Saying so over a stale
   figure is worse than saying nothing at all. */
.mg-live.is-stale { background: #F7EFDC; color: #8A6A22; }

/* 10px at the sides, not 22. The air table wants 312px and a 390px phone left
   it exactly 312 -- equal enough that sub-pixel rounding still produced a
   scrollbar. The narrower gutter gives it 24px of room instead of none. */
.mg-panel-body { padding: 18px 10px; flex: 1; }
.mg-panel-body .mg-note { margin: 12px 0 0; }
/* Tighter than a full-width table: these sit in a third of the page and the
   air panel carries nine columns, so every pixel of padding costs a column. */
.mg-panel-body .mg-table th,
.mg-panel-body .mg-table td { padding: 10px 9px; font-size: 13px; }

/* -- the election district map ----------------------------------------- */
/* Taller than the dashboard's map: this page is the map, so it gets the
   height rather than sharing a column with widgets. */
/* The leaf map, same geometry as the district map. */
.mg-leafmap {
  /* A stacking context of its own, the same as .mg-map and for the same
     reason: Leaflet gives its panes z-index 400 and its controls 1000, and
     loose in the page those beat the sticky header's 100 -- the map drew over
     the header on the way past it. Contained here the whole map sits at 0,
     however Leaflet numbers its internals.

     .mg-map had this from the start; these two were written later and did not
     copy it, which is the whole bug. */
  position: relative;
  z-index: 0;
  height: clamp(420px, 68vh, 760px);
  border-radius: var(--radius-md, 12px);
  border: 1px solid var(--border, #DDE3DE);
  overflow: hidden;
}
.mg-leafmap.is-compact { height: 400px; }
/* The key sits under the map here rather than over it: six zones plus the
   dashed case is more than a corner can hold without covering streets. The
   20px off the map is on the label now -- it heads the block, so the gap
   belongs above it and the keys tuck in under it. */
.mg-legend-label {
  margin: 20px 0 8px;
  font: 600 14px/1.2 var(--font-ui, sans-serif);
  letter-spacing: 0.08em;
  /* Uppercased in CSS, not in the markup: it is still the word "Legend" to a
     screen reader, which reads capitals letter by letter in some voices. */
  text-transform: uppercase;
  color: var(--muted, #65716D);
}
.mg-leaf-key { margin-top: 0; }
/* The zone's name written across its streets, shaped like the site's buttons.
   It is a divIcon marker rather than a tooltip -- it has a tooltip of its own
   now, listing the collection dates, and a tooltip cannot carry one. The
   wrapper is stripped bare and the pill inside carries the whole appearance;
   its colours vary by zone, so they are set inline.

   No pointer-events:none any more, on purpose. The label used to have it so
   it could not eat a hover meant for the street beneath; now the hover IS the
   point. It covers 75x32 of the map in exchange for ten dates. */
.mg-leaf-labelicon { background: none; border: 0; }
.mg-leaf-pill {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 75px;
  padding: 9px 0;
  border: 1px solid;            /* colour inline, from the zone */
  /* A tag rather than a pill. Not --radius-sm: that is 6px, and at 75px wide
     and 32 tall a 6px corner still reads as a rounded chip. */
  border-radius: 4px;
  font: 600 13px/1 var(--font-ui, sans-serif);
  letter-spacing: 0.02em;
  white-space: nowrap;
  /* NO backdrop-filter. A 2px blur was tried here to settle the streets running
     under the pill and it smeared them into coloured fog instead -- the label
     read as a printing fault. The fill is opaque instead (see onWhite): the
     same colour a 10% tint has on a button elsewhere on the site, but it does
     not let the streets through the words. */
  box-shadow: 0 1px 2px rgba(38, 49, 44, .12);
}

/* Matched to the gauges map when both sit on the maps page. */
.mg-edmap.is-compact { height: 400px; }
/* A heading for a map that is one of several. Not .mg-map-title -- that one
   lives in the gauges map's narrow legend column and is sized for it. */
.mg-map-heading {
  margin: 0 0 12px;
  font: 400 26px/1.2 var(--font-editorial, Georgia, serif);
  color: var(--green-deep, #174F48);
}
/* Same manners as .mg-panel-titlelink: a heading that happens to be a link
   still reads as a heading, and only says so under the pointer. */
/*
 * THE TITLE IS A LINK, AND HAS TO LOOK LIKE ONE.
 *
 * It is one of the two ways into the full map -- the other is the button at
 * the foot of the legend -- and a reader who never hovers would not find it.
 * Not the body link colour either: at 26px in the green the heading already
 * uses, a second colour would make the title read as a different kind of
 * object from the headings beside it.
 *
 * A dotted rule under it instead, which says "this goes somewhere" without
 * restyling the heading, and firms up to a solid one on hover.
 */
.mg-map-headinglink {
  color: inherit;
  text-decoration: underline dotted;
  text-decoration-thickness: 1px;
  text-underline-offset: 4px;
  text-decoration-color: var(--border, #DDE3DE);
}
.mg-map-headinglink:hover {
  text-decoration: underline solid;
  text-decoration-thickness: 1px;
  text-underline-offset: 4px;
  text-decoration-color: currentColor;
}
/* The map's own header row, doing what .mg-panel-head does for a panel: the
   heading at one end, the signpost pushed to the other. Baseline rather than
   centre -- a 26px serif heading and a 12px label centred against each other
   sit on two different lines to the eye, and these are one line. */
.mg-map-head {
  display: flex;
  align-items: baseline;
  gap: 12px;
  /* 10, matching .mg-map-aside-head. These two head styles sit side by side
     in the gallery grid -- Election Districts beside Street Trees -- and 12
     against 10 put two titles at visibly different heights above their maps
     in the same row. */
  margin-bottom: 10px;
}
.mg-map-head .mg-map-heading { margin-bottom: 0; }
.mg-map-head .mg-panel-hint { margin-left: auto; }
/* Onto its own line rather than squeezing the title, as the panel header does
   at the same width. */
@media (max-width: 560px) {
  .mg-map-head { flex-wrap: wrap; }
  .mg-map-head .mg-panel-hint { margin-left: 0; }
}
/* SHIFT IS HELD: no rule needed. The container borrows Leaflet's own
   .leaflet-crosshair, which already covers the interactive children -- see
   addShiftHint(). A rule of ours here lost a specificity tie to .leaflet-grab,
   because WordPress enqueues this file before leaflet.css. */

/* The reset control, matched to Leaflet's own zoom bar above it rather than to
   this site's buttons: it shares the stack, and a control that looks like a
   stranger there reads as something bolted on afterwards. */
/* NOTHING ABOUT SIZE OR CHROME. The button lives inside Leaflet's own zoom
   bar now, so width, height, border, radius and the divider between buttons
   all come from .leaflet-bar a and cannot drift from the +/- above it. Only
   the centring, which Leaflet does with line-height for a glyph and cannot do
   for an svg. */
/*
 * TWO CLASSES, AND THE REASON IS LOAD ORDER.
 *
 * WordPress enqueues this file BEFORE leaflet.css. `a.mg-resetview` and
 * `.leaflet-bar a` are both (0,1,1), so the tie goes to whichever loads last
 * -- Leaflet -- and its `display:block` won. That is why the icon sat in the
 * top-left corner of the button instead of the middle: a block-level svg is
 * not centred by `text-align`, and nothing was centring it down the box at
 * all. Leaflet declares `display:block` twice, so overriding it takes
 * specificity rather than another declaration.
 *
 * `.leaflet-bar a.mg-resetview` is (0,2,1) and wins whatever the order. It
 * also has to restate the colour, which lost the same tie to Leaflet's
 * `color: black`.
 *
 * Flex rather than fixed offsets because the box is 26px on a mouse and 30px
 * under .leaflet-touch, and centring is the same instruction for both.
 */
.leaflet-bar a.mg-resetview {
  display: flex; align-items: center; justify-content: center;
  color: var(--ink, #26312C);
}
/* 18, matching the font-size Leaflet gives the +/- above. An outline icon
   with a hole in the middle needs every pixel of that to weigh the same as a
   solid glyph; 15 looked like a mistake. */
.leaflet-bar a.mg-resetview svg { width: 18px; height: 18px; display: block; }
/* The ring belongs to keyboard focus, not to the click that just used the
   button. Leaflet's own +/- show the browser default here too, but this is the
   one that gets pressed and then looked at. */
.leaflet-bar a.mg-resetview:focus:not(:focus-visible) { outline: none; }

/* The line that tells them. Quiet -- it is an aside about how to use the map,
   not part of what the map says. */
.mg-map-kbd {
  margin: 14px 0 0;
  font: 400 11.5px/1.5 var(--font-ui, sans-serif);
  color: var(--muted, #65716D);
}
.mg-map-kbd kbd {
  font: 600 10.5px/1 var(--font-ui, sans-serif);
  padding: 3px 5px;
  border: 1px solid var(--border, #DDE3DE);
  border-bottom-width: 2px;
  border-radius: 4px;
  background: var(--white, #fff);
  color: var(--ink, #26312C);
}
/* NO SHIFT KEY ON A PHONE. Telling a reader to hold one is worse than saying
   nothing, and the pinch they already know about does the same job. */
@media (pointer: coarse) {
  .mg-map-kbd { display: none; }
}

/* The flood map. Same stacking context as every other Leaflet host here. */
.mg-floodmap {
  position: relative;
  z-index: 0;
  height: clamp(420px, 70vh, 800px);
  border-radius: 0 var(--radius-md, 12px) var(--radius-md, 12px) 0;
  border: 1px solid var(--border, #DDE3DE);
  overflow: hidden;
}
.mg-floodmap.is-compact { height: auto; min-height: 400px; }

/* Each key is a swatch, a name, and a line saying what the name means. The
   odds are the whole point: "1% annual chance" is a number people reliably
   misread, and the legend is the only place with room to unpack it. */
/*
 * A river's name, set along its own channel.
 *
 * SVG text, not a Leaflet tooltip: a tooltip is a white card with a border
 * and a shadow parked at one point, and five of those down a river read as
 * five notes stuck on the map. This is a <textPath> that bends with the
 * water -- see renderFlood.
 *
 * paint-order, not text-shadow. A halo on SVG text has to be a stroke, and a
 * stroke paints OVER the glyph by default and eats it from the outside at
 * 3px; paint-order puts the stroke down first and the fill on top of it, so
 * the white is only ever the outline. That is the standard way a map gets a
 * name to stay legible crossing a blue line and a yellow fill.
 */
.mg-river-labels text {
  font: 600 11px/1 var(--font-ui, sans-serif);
  letter-spacing: .01em;
  fill: #14547A;
  stroke: #fff;
  stroke-width: 3px;
  stroke-linejoin: round;
  paint-order: stroke fill;
  pointer-events: none;
}

.mg-flood-keys { gap: 14px; }
.mg-flood-key { display: flex; align-items: flex-start; gap: 8px; }
.mg-flood-key .sq { margin-top: 3px; border-radius: 3px; flex: 0 0 auto; }
/* A rule, not a block: the river is drawn as a line and its key says so.
   `.mg-legend i` -- class plus element -- outranks a bare class, so this has
   to name the element too or the swatch comes out as the legend's 12px
   circle. */
.mg-legend i.mg-flood-line {
  width: 11px; height: 3px; border-radius: 2px;
  margin-top: 7px; flex: 0 0 auto; display: inline-block;
}
.mg-flood-text { display: flex; flex-direction: column; gap: 1px; }
.mg-flood-text b {
  font: 600 12.5px/1.35 var(--font-ui, sans-serif);
  color: var(--ink, #26312C);
}
.mg-flood-text em {
  font-style: normal;
  font: 400 11.5px/1.4 var(--font-ui, sans-serif);
  color: var(--muted, #65716D);
}

/*
 * The tree map. Same stacking-context rule as .mg-map and .mg-edmap, and for
 * the same reason -- Leaflet's panes are z-index 400 and its controls 1000,
 * and loose in the page those beat the sticky header's 100.
 */
.mg-treemap {
  position: relative;
  z-index: 0;
  height: clamp(420px, 70vh, 800px);
  /* Outer corners only: the left edge runs into the aside, the same seam
     .mg-map has. */
  border-radius: 0 var(--radius-md, 12px) var(--radius-md, 12px) 0;
  border: 1px solid var(--border, #DDE3DE);
  overflow: hidden;
}
/* On the gallery the aside carries a title, three switches, ten species and a
   link, which comes to more than 400px. A map with a FIXED 400 in a grid row
   that tall leaves white space under it, so here the height is a floor and
   the map stretches to whatever the legend beside it needs. */
.mg-treemap.is-compact { height: auto; min-height: 400px; }

/* NO WIDTH OF ITS OWN. This was 1.35fr for a while on the assumption that a
   species list needs more room than "Station not reporting" -- but measured,
   the ten names all fit on one line at 252px, and gt_map's fifth is 272 at
   full width. Two legends of different widths on one page is a worse cost
   than a name that might one day wrap, so the tree map takes the same column
   as the map above it. */

/*
 * TWO MAPS TO A ROW, because Millburn is very nearly square.
 *
 * The township's bounding box is 5.6km by 4.9km -- 1.15:1. A full-width map
 * card is about 1.46:1, so fitBounds lands the town height-first and leaves a
 * fifth of the width as empty basemap down each side. Two half-width cards
 * are 0.85:1 each, which the town fills, and the page gets shorter at the
 * same time: a gallery of five maps was five screens of scrolling.
 *
 * Only the ones that are a map and a heading. Explore the Map and the tree
 * map carry a legend column, and halving the row would leave that column
 * about 140px wide -- too narrow for "Northern Red Oak" beside a number.
 */
/* Clear of the page intro above it. The cards carry margin-block:0 inside the
   grid -- the gap does that spacing now -- which left the grid itself with
   nothing above it either.
   Compounded with .wp-block-group, the class the block editor puts on the
   wrapper, so it outranks whatever block margin WordPress emits for that
   group without an !important. */
.wp-block-group.mg-map-pair { margin-top: 30px; }
.mg-map-pair {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  /* ROW GAP DOUBLE THE COLUMN GAP. The two are not the same job: across, 22px
     is the seam between two cards a reader takes as a pair; down, it is the
     break between one pair and the next. Equal, the grid reads as one field
     of cards rather than as three rows of two. */
  gap: 44px 22px;
  /* STRETCH, not start. Two cards side by side that end at different heights
     read as one of them having gone wrong. Whichever legend is taller sets
     the row, and the shorter card's map takes up the slack rather than
     leaving a step between them. */
  align-items: stretch;
}
/* The gap is the spacing now. .mg-component carries 28px block margins for
   when it is one thing stacked under another, and inside the grid those add
   56px to the row on top of a gap that was already doing the job. */
.mg-map-pair > .mg-component {
  margin-block: 0;
  /* The card stretches to its taller sibling, but the map layout INSIDE it
     was still only as tall as its own legend -- so the shorter card had the
     right outline and a band of page showing under its map. A column flex
     with the layout growing hands the spare height to the map. */
  display: flex;
  flex-direction: column;
}
.mg-map-pair > .mg-component > .mg-map-layout { flex: 1 1 auto; }
/* AN ODD ONE OUT TAKES THE WHOLE ROW. Five maps do not divide into pairs, and
   a lone half-width card with a blank beside it reads as a map that failed to
   load. Spanning is a deliberate-looking end to the grid. */
.mg-map-pair > .mg-component:last-child:nth-child(odd) { grid-column: 1 / -1; }

/*
 * A LEGEND MAP IN HALF A ROW PUTS ITS LEGEND UNDER THE MAP.
 *
 * This was a 250px legend column beside the pane, which is what the layout
 * does at full width. Measured in half a row: the card is 589px, so 250 of it
 * went to the legend and the map got 339 -- and Millburn is wider than it is
 * tall, so a 339px pane showed the town at a third of the size of the card it
 * sat in. Half the width was spent on a legend that fits comfortably across
 * the foot.
 *
 * So half a row takes the same reflow a phone does: map first at the card's
 * full width, legend under it. The DOM order is unchanged -- aside first --
 * so a screen reader still meets the heading before the map, as it does at
 * every other width.
 *
 * And the pane gets the town's own ratio back. 5.6km by 4.9km is 1.15:1, and
 * fitBounds fills whichever side runs out first, so a pane cut to that shape
 * is the only one that wastes nothing. It was suppressed here because the
 * legend beside it set the height; nothing is beside it now.
 */
.mg-map-pair .mg-map-layout { grid-template-columns: minmax(0, 1fr); }
.mg-map-pair .mg-map,
.mg-map-pair .mg-treemap,
.mg-map-pair .mg-floodmap,
.mg-map-pair .mg-roadmap {
  order: 1;
  height: auto;
  min-height: 320px;
  aspect-ratio: 1.15;
  width: auto;
  border-radius: var(--radius-md, 12px) var(--radius-md, 12px) 0 0;
}
/*
 * AND THE TWO PANES THAT DRAW THEIR OWN SEAM.
 *
 * .mg-prism and .mg-ballotmap are the map cell of .mg-map-layout, so they
 * round their RIGHT corners and square their left ones against the legend
 * column standing beside them. In the gallery nothing stands beside them --
 * the aside is display:contents and its white box is underneath -- so the
 * early-voting card came out square at the top left, round at the top right,
 * and round again at the bottom right where the legend box begins. Three
 * different corners on one card, and the only map on /maps/ whose top edge
 * did not match its neighbours'.
 *
 * Radius only, and a rule of its own rather than four more selectors on the
 * one above: that one also carries height, aspect-ratio and width, and these
 * two panes take their height from the canvas or the Leaflet map inside them.
 */
.mg-map-pair .mg-map-layout > .mg-prism,
.mg-map-pair .mg-ballotmap {
  border-radius: var(--radius-md, 12px) var(--radius-md, 12px) 0 0;
}
/*
 * THE SLACK GOES TO THE BOTTOM, not between the map and its legend.
 *
 * Two cards in a row stretch to the taller of them, and the extra height was
 * landing between the grid's two rows -- so the Explore card had a hundred
 * pixels of page showing between the foot of its map and the top of its
 * legend, which reads as a legend that has come loose.
 *
 * auto for the map, 1fr for the legend: the map keeps the height its ratio
 * gives it, the white box takes everything left over, and the empty space
 * ends up inside the legend box at the bottom where it looks deliberate.
 *
 * Rows are assigned in ORDER order, not DOM order: the title block, then the
 * pane, then the legend. THREE rows, not two -- splitting the aside in half
 * added one, and `auto 1fr` then handed the slack to the pane's row instead
 * of the legend's, which put the gap straight back between map and legend.
 */
.mg-map-pair .mg-map-layout { grid-template-rows: auto auto 1fr; }
/*
 * SIDE BY SIDE, THE TITLE GOES ABOVE THE MAP.
 *
 * Six cards in the gallery were putting their titles in two different places:
 * the district and leaf maps above the pane, the other four below it inside
 * the legend box. Name, map, key is the order that reads, and it is the one
 * the two odd cards already had.
 *
 * display: contents dissolves the aside so its two halves become grid items
 * in their own right and can be ordered around the pane. The white box moves
 * with the half that needs it -- the legend -- since an aside that is no
 * longer a box cannot draw one.
 *
 * ONLY HERE. At full width the aside is a real column down the left and its
 * two halves belong together in it, which is what the solo layout wants and
 * what this rule leaves alone.
 *
 * AND NOT .is-below. That variant IS the white box -- it is what the district
 * and leaf maps wrap their legend in, since those two have no left column to
 * dissolve. Dissolving it took the box away and put their keys straight back
 * on the page background, which is the very thing it was added to fix.
 */
.mg-map-pair .mg-map-aside:not(.is-below) { display: contents; }

/*
 * THE FOOTNOTES COME OFF IN THE GALLERY.
 *
 * Six cards each ending in a paragraph of source notes is six paragraphs of
 * small print on a page whose job is to show six maps. The notes belong to
 * the full map -- every card has a link to it -- and here they only push the
 * legends apart and make the shorter card taller than the map it holds.
 *
 * The legend stays, and so does the Shift hint: those are controls for the
 * map that is actually on screen.
 */
.mg-map-pair .mg-note,
.mg-map-pair .mg-map-note,
.mg-map-pair .mg-map-foot,
.mg-map-pair .mg-tree-changes,
.mg-map-pair .mg-legend-label { display: none; }

/*
 * AND THE LEGEND ENDS BEFORE COLUMNS.
 *
 * The election card carries a second legend the others do not: the three
 * party colours, the All Ballots In / In Person switch, and a channel
 * breakdown with two proportion bars and six shares under them. In the full
 * map that column is the point. In a card it is a page of figures hung under
 * a thumbnail -- eleven rows of it, more than the three cards above it have
 * between them -- and it pushed the way out so far down that the card read as
 * having none.
 *
 * So the card stops where the other cards stop: the ramp, the township's one
 * headline number, and the button. The switch is a control for a map you are
 * about to leave, and the figures it controls are the first thing on the
 * page it leads to.
 *
 * THE TABLE GOES WITH THEM. .mg-prism-table is appended to the component
 * rather than to the card, so in the gallery it is a line of summary text
 * loose on the page background below the white box -- which reads as a
 * caption the card has grown rather than as a disclosure anybody will open.
 * Sixteen districts by registered voters is the full map's closing answer to
 * what a turning solid cannot tell you, and on /maps/ it is one more thing
 * between this card and the next.
 */
.mg-map-pair .mg-bars-block,
.mg-map-pair .mg-prism-table { display: none; }
.mg-map-pair .mg-map-aside-head {
  order: 0;
  /* Above the card, on the page, like .mg-map-head on the other two. */
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 14px;
  margin: 0 0 10px;
}
.mg-map-pair .mg-map-aside-head .mg-map-title { font-size: 22px; }
/* The intro is a column of prose in the solo layout and a line of it here;
   out of the flow entirely when there is a map underneath doing the work. */
.mg-map-pair .mg-map-aside-head .mg-map-intro { display: none; }
.mg-map-pair .mg-map-aside-body {
  order: 2;
  flex: 1 1 auto;
  background: var(--white, #fff);
  border: 1px solid var(--border, #DDE3DE);
  border-top: 0;
  border-radius: 0 0 var(--radius-md, 12px) var(--radius-md, 12px);
  padding: 14px 16px 16px;
}
.mg-map-pair .mg-map-aside {
  order: 2;
  border-radius: 0 0 var(--radius-md, 12px) var(--radius-md, 12px);
  /* The seam is gone: the aside is under the map now, not against it, so it
     needs the edge the map's left border used to draw. */
  border-right: 1px solid var(--border, #DDE3DE);
  border-top: 0;
}
/*
 * THE CARD IS THE TOWN'S SHAPE, not a height somebody picked.
 *
 * Millburn's bounding box is 5.6km by 4.9km, so 1.15:1. fitBounds fills
 * whichever side runs out first and leaves the other as empty basemap, which
 * means the only card that wastes nothing is one cut to the same ratio. At a
 * fixed 400 in a full-width row the town filled 34% of the width; at 400 in
 * half a row, 68%; at this ratio, all of it, and it stays right at every
 * width instead of being correct at one and wrong either side of it.
 *
 * A floor for the narrow end, where the ratio would otherwise hand back a
 * 310px map on a phone.
 */
/* The two maps whose legend is a block under the pane rather than a column
   beside it. Same white box, same corners, same seam handling as .mg-map-aside
   gets everywhere else -- without this their keys sat on the page background
   and the card stopped at the foot of the map. */
.mg-map-aside.is-below {
  background: var(--white, #fff);
  border: 1px solid var(--border, #DDE3DE);
  border-top: 0;
  border-radius: 0 0 var(--radius-md, 12px) var(--radius-md, 12px);
  padding: 14px 16px 16px;
}
.mg-map-pair .mg-map-aside.is-below { flex: 1 1 auto; }

.mg-map-pair .mg-edmap.is-compact,
.mg-map-pair .mg-leafmap.is-compact {
  height: auto;
  aspect-ratio: 1.15;
  min-height: 360px;
  /* The legend box is against the bottom edge now, so the rounding belongs to
     it rather than to the map. */
  border-radius: var(--radius-md, 12px) var(--radius-md, 12px) 0 0;
}
/* One column below the width where two maps stop being maps and start being
   postage stamps. The same 860 the legend aside stacks at, so a reader
   crossing that width sees one change rather than two. */
@media (max-width: 860px) {
  /* minmax(0,...), not a bare 1fr. A 1fr track has an implicit auto minimum,
     so it refuses to go below the min-content width of its widest item -- and
     the 250px legend rule below used to survive into this media query, which
     made that minimum wider than a phone. The whole grid stretched to fit it,
     and because there is only ONE column, every card in the gallery inherited
     the blown-out width. That is why the two maps with no legend at all,
     Election Districts and Leaf Collection Zones, were also running off the
     right edge: nothing was wrong with them, they were sharing a column with
     a card that could not fit. Belt as well as braces, now that the cause is
     fixed below. */
  .mg-map-pair { grid-template-columns: minmax(0, 1fr); }

  /* THE GALLERY'S LEGEND MAPS STACK LIKE THE DASHBOARD'S.
     .mg-map-pair .mg-map-layout is two class selectors and the stacking rule
     up at the top of this file is one, so the gallery's fixed 250px legend
     column beat it at every width, media query or not. On a phone that left
     a 250px legend beside a map squeezed to whatever was left, with the card
     wider than the screen -- and since the aside is ordered second, it was
     the legend hanging off the right rather than the map.
     One column here, and the stacking rules at the top of the file -- order,
     corners, borders -- then apply unchanged. Same layout as /dashboard. */
  .mg-map-pair .mg-map-layout { grid-template-columns: minmax(0, 1fr); }

  /* Give the height back. In a pair these three drop their own height on
     purpose, because the legend beside them is the taller of the two and sets
     the row. Stacked there is no legend beside them, so height:auto with
     min-height:0 is a map nought pixels tall. The town's own 1.15 for the
     shape, and a floor so a narrow phone still gets a map rather than a
     letterbox. */
  .mg-map-pair .mg-map,
  .mg-map-pair .mg-treemap,
  .mg-map-pair .mg-floodmap,
  .mg-map-pair .mg-roadmap {
    height: auto;
    aspect-ratio: 1.15;
    min-height: 320px;
    /* WIDTH FIRST, or the ratio decides it. These panes are content-box with
       a 1px border, and only .mg-map declares a width. Without one, the floor
       above settles the height at 320 and aspect-ratio derives the width from
       it -- 320 x 1.15 = 368 in a 358px column, so the tree and flood maps hung
       10px off the right edge while the weather map did not. Declaring the
       width makes the ratio yield to it and drive the height instead, which is
       the direction that respects the screen.
       border-box so the border comes out of the 358 rather than being added to
       it: with content-box, width:100% plus two 1px borders is 360 in a 358
       track, which is where .mg-map's own 2px sliver came from. */
    width: 100%;
    box-sizing: border-box;
  }
}

/* The three kinds, as switches rather than a legend you can only read. */
/* Nothing here any more.
   This carried margin-top: 18px for when the switches opened the column with
   nothing above them, and when "Site type" turned up looking too far from its
   own list I set it to 0 -- which changed nothing. .mg-legend.is-stacked is
   two classes and this is one, so its 20px outranked the override and went on
   applying. The real culprit was the base rule, and that is where it has been
   removed. Diagnosing spacing by reading the nearest rule instead of the
   cascade is how an hour goes missing. */
.mg-tree-key {
  display: flex; align-items: center; gap: 7px;
  font-size: 12.5px; color: var(--ink, #26312C); cursor: pointer; user-select: none;
}
.mg-tree-key input { accent-color: var(--green-deep, #174F48); margin: 0; }
/* Hollow for a vacant site, solid for a tree or a stump -- the same
   distinction the map itself draws, so the key looks like what it switches. */
.mg-tree-dot {
  width: 11px; height: 11px; border-radius: 50%;
  border: 1.5px solid; display: inline-block; flex: 0 0 auto;
}
.mg-tree-n { font-variant-numeric: tabular-nums; opacity: .75; margin-left: auto; }

/* The species list. A name and a number, the number right-aligned so the
   column of figures can be read down without the eye tracking each name to
   the end of its own line. */
/* TWO COLUMNS UNDER THE MAP, one beside it.
   The gallery gives the legend the full card width; the solo page gives it a
   240px column where two of anything would be four words wide. align-items
   start so the short first column does not stretch to the species list's
   height and space its own groups out to match. */
.mg-map-pair .mg-legend-cols {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0 26px;
  align-items: start;
}
/* The first heading in each column opens it, so it gives up the gap above --
   otherwise the two columns start at different heights. */
.mg-map-pair .mg-legend-col > .mg-tree-sub:first-child { margin-top: 0; }
/* A stacked legend opening a column gives up its own top margin for the same
   reason a heading does -- otherwise the two columns start at different
   heights. The Explore legend has no headings, only keys. */
/* Now that .mg-legend.is-stacked carries no top margin, a legend opening a
   column needs no override -- the rule is kept only so the intent survives a
   future change to the base. */

/*
 * ALL / NONE SITS BESIDE ITS HEADING HERE, NOT AT THE FAR RIGHT.
 *
 * margin-left: auto pushes the pair to the end of the heading line, which is
 * fine down a wide single column and wrong in a narrow one: it parked two
 * words hard right, directly above a right-aligned column of counts, and they
 * read as a header for the numbers. They are buttons that tick and untick
 * every species.
 *
 * Beside the label they are plainly attached to "Common species" -- which is
 * what they act on -- and nothing lines up with the counts any more.
 */
.mg-map-pair .mg-tree-pick { margin-left: 10px; }

/* The legend subheading, on every map that needs one -- the road legend's
   "Proposed network" uses it too. The name is from the tree map, where it was
   first needed; it is not tree-specific and should not be duplicated. */
.mg-tree-sub {
  margin: 22px 0 8px;
  font: 600 11px/1.3 var(--font-ui, sans-serif);
  letter-spacing: .08em; text-transform: uppercase;
  color: var(--muted, #65716D);
  /* The heading and its two controls on one line, the controls at the far
     end: this is a label with a pair of switches, not a toolbar. */
  display: flex; align-items: baseline; gap: 10px;
}
.mg-tree-pick { margin-left: auto; display: flex; gap: 10px; }
/* Buttons because they change the map and go nowhere, stripped back to text
   because at 10px an outlined pair beside a section label reads as heavier
   than the list they govern. The letter-spacing and the caps come off -- they
   are words to read, not a heading. */
.mg-tree-pick button {
  appearance: none; background: none; border: 0; padding: 0;
  font: 600 11px/1.3 var(--font-ui, sans-serif);
  letter-spacing: 0; text-transform: none;
  color: var(--green-deep, #174F48);
  cursor: pointer;
}
/* "Show" belongs to the control phrase, not to the heading, so it drops the
   heading's caps and tracking the way the two buttons beside it already do.
   Left in caps it rendered as "SITE TYPE SHOW All None" -- a second heading
   with two words after it, which is close to the confusion this was added to
   clear up. Sentence case, and "Show All" reads as one thing. */
.mg-tree-pick-label {
  font-weight: 400;
  letter-spacing: 0;
  text-transform: none;
  color: var(--muted, #65716D);
}

/*
 * THE UNDERLINE IS STATE, SO HOVER MUST NOT LOOK LIKE IT.
 *
 * Hover was already an underline, and the moment one of these means "this is
 * what the list is doing now" the two have to be told apart. The current one
 * is thicker and sits lower, and takes the full ink colour; hover is the hair
 * line it always was. On the current button hover changes nothing, which is
 * right -- clicking it would change nothing either.
 */
.mg-tree-pick button {
  text-decoration: none;
  text-underline-offset: 3px;
}
.mg-tree-pick button:hover { text-decoration: underline; }
.mg-tree-pick button.is-on {
  color: var(--green-deep, #174F48);
  text-decoration: underline;
  text-decoration-thickness: 2px;
}
/* Neither is current while the reader has a selection of their own, and that
   is the common case -- so the pair has to look live rather than switched
   off. Muted, not faded. */
.mg-tree-pick button:not(.is-on) { color: var(--muted, #65716D); }
.mg-tree-pick button:focus-visible {
  outline: 2px solid var(--green-deep, #174F48); outline-offset: 2px; border-radius: 3px;
}
.mg-tree-pick button:disabled {
  color: var(--muted, #65716D); opacity: .45; cursor: default; text-decoration: none;
}
.mg-tree-species { list-style: none; margin: 0; padding: 0; }
/* The graduated-symbol key: four circles at the radii the map draws, each
   with the width it stands for. Baseline-aligned so the circles sit on one
   line and grow upward, which is how a reader compares them. */
.mg-tree-sizes { display: flex; align-items: flex-end; gap: 12px; }
.mg-tree-size { display: flex; flex-direction: column; align-items: center; gap: 5px; }
.mg-tree-size i {
  display: block; border-radius: 50%;
  /* Grey, not green: fill colour means species on this map, and a coloured
     circle here would read as a fifth one. */
  background: #9AA6A0; border: 1px solid #7C8983;
  box-sizing: border-box;
}
.mg-tree-size em {
  font-style: normal; font-size: 10.5px; line-height: 1.2;
  color: var(--muted, #65716D); white-space: nowrap;
}
/* The row is the label now, so the whole line is the switch -- the name and
   the count are as clickable as the box, which on a phone is the difference
   between a target and a hope. */
.mg-tree-species li {
  font-size: 12.5px; line-height: 1.6;
  color: var(--ink, #26312C);
}
.mg-tree-skey {
  display: flex; align-items: center; gap: 8px;
  cursor: pointer; user-select: none;
}
.mg-tree-skey input { accent-color: var(--green-deep, #174F48); margin: 0; flex: 0 0 auto; }
.mg-tree-species .n { overflow-wrap: anywhere; }
/* The green row is a summary of the ones above it, not another entry. */
.mg-tree-species li.is-rest .n { color: var(--muted, #65716D); }
/* Tree is off, so these switch nothing. Dimmed rather than hidden: the list
   is still the key to a map the reader can turn back on, and a legend that
   disappears takes the explanation with it. */
.mg-tree-species li.is-off { opacity: .45; }
.mg-tree-species li.is-off .mg-tree-skey { cursor: default; }
/* THE FOLD. Display, not removal: the rows keep their state and their
   switches keep working, so All and None still reach every bucket and the map
   never disagrees with a box the reader cannot see. */
.mg-tree-species.is-collapsed li.is-tail { display: none; }
/* The same stripped-back text button as All/None above the list, and for the
   same reason -- an outlined control here would read as heavier than the list
   it governs. Left, under the last row, where the list ends. */
.mg-tree-showmore {
  appearance: none; background: none; border: 0; padding: 0;
  margin-top: 8px;
  font: 600 11px/1.3 var(--font-ui, sans-serif);
  color: var(--green-deep, #174F48);
  cursor: pointer;
}
.mg-tree-showmore:hover { text-decoration: underline; }
.mg-tree-showmore:focus-visible {
  outline: 2px solid var(--green-deep, #174F48); outline-offset: 2px; border-radius: 3px;
}

.mg-tree-species .c {
  margin-left: auto;
  font-variant-numeric: tabular-nums;
  color: var(--muted, #65716D);
}
/*
 * THE WAY IN TO THE FULL PAGE, AS A HOLLOW BUTTON.
 *
 * It carries the theme's own .button .button-secondary rather than a copy of
 * them -- the same pill the homepage hero offers as its second call to
 * action. Copying the border and radius here would have been a second
 * definition of one thing, and the two would have drifted the first time the
 * styleguide moved.
 *
 * Every card in the gallery exists to lead somewhere, so every card now makes
 * the same offer in the same shape: the three with a legend under their
 * subtitle, the two without beside their heading.
 *
 * SMALLER THAN THE HERO'S. That one is 14px on 13/21 padding, sized to a
 * photograph the width of the screen. These sit inside a 250px legend column,
 * where the same pill is most of the width and shouts over the keys it is
 * meant to sit beneath. 12.5 on 9/16 is the same button at card scale.
 */
.mg-map-more {
  display: inline-flex;
  /* Last in the legend now, so it needs the gap above and nothing below. It
     briefly carried a bottom margin too, from when it sat between the title
     and the keys. */
  margin-top: 16px;
  padding: 9px 16px;
  font: 600 12.5px/1 var(--font-ui, sans-serif);
  gap: 8px;
}
/* Beside a heading rather than under a paragraph, so the top margin that
   spaces it from a sentence would push it off the heading's line. */
/* .mg-map-head no longer holds a button -- the way out moved to the foot of
   the legend, and the head is just the title. Kept as a floor in case a map
   puts one back. */
.mg-map-head .mg-map-more { margin-top: 0; }
/* Which gauge is missing from the column above, and why. Quiet like the other
   notes -- it is an aside about the page, not one of its readings -- but the
   name inside it is a link, because the gauge's own page still works and is
   where a reader who wants the thin data should end up. */
.mg-charts-quiet { max-width: 70ch; }
.mg-charts-quiet a { color: var(--green-deep, #174F48); }

.mg-map-note {
  margin: 20px 0 0;
  font-size: 11.5px; line-height: 1.5;
  color: var(--muted, #65716D);
}

/* The popup: a name, then the quieter facts under it. */
.mg-tree-pop { display: flex; flex-direction: column; gap: 2px; font-size: 13px; }
.mg-tree-pop strong { font-size: 14px; color: var(--ink, #26312C); }
.mg-tree-bot { font-style: italic; color: var(--muted, #65716D); font-size: 12px; }
.mg-tree-dbh, .mg-tree-addr { color: var(--muted, #65716D); font-size: 12px; }

.mg-edmap {
  /* A stacking context of its own, the same as .mg-map and for the same
     reason: Leaflet gives its panes z-index 400 and its controls 1000, and
     loose in the page those beat the sticky header's 100 -- the map drew over
     the header on the way past it. Contained here the whole map sits at 0,
     however Leaflet numbers its internals.

     .mg-map had this from the start; these two were written later and did not
     copy it, which is the whole bug. */
  position: relative;
  z-index: 0;
  height: clamp(420px, 68vh, 760px);
  border-radius: var(--radius-md, 12px);
  border: 1px solid var(--border, #DDE3DE);
  overflow: hidden;
}
/* The number sits ON the district, so it has no box of its own -- Leaflet's
   tooltip chrome would draw sixteen little cards over the map. */
.leaflet-tooltip.mg-ed-label {
  background: none; border: 0; box-shadow: none; padding: 0;
  font: 700 13px/1 var(--font-ui, sans-serif);
  color: var(--ink, #26312C);
  /* Centred, so the count sits under the middle of the district number rather
     than ragging off its left edge -- the two are one label and the wider
     line is the one underneath. Leaflet positions the tooltip box, not what
     is inside it. */
  text-align: center;
  /* Outlined rather than shadowed: it has to hold over eight fill colours. */
  paint-order: stroke;
  -webkit-text-stroke: 3px rgba(255, 255, 255, .85);
}
.leaflet-tooltip.mg-ed-label::before { display: none; }   /* no pointer */
/* The registered-voter count, under the district number. Its own line because
   "4 (1,291)" on one line reads as a single long number at this size, and a
   lighter weight because the district's NUMBER is what the label is for -- the
   count is the answer to the next question, not the first one. Line-height 1
   on the parent, so the second line needs its own. */
.leaflet-tooltip.mg-ed-label .mg-ed-voters {
  display: block;
  text-align: center;
  margin-top: 3px;
  font-weight: 500;
  /* 12, not 11. At 11 the thousands comma in "1,120" sat inside a 3px halo
     and came out looking like a semicolon -- the stroke is sized for a 13px
     bold number and was swallowing the punctuation of a smaller one. */
  font-size: 12px;
  line-height: 1;
  /* And its own, thinner halo for the same reason. It only has to hold over
     a 55%-opacity fill, not over the fill and a boundary line. */
  -webkit-text-stroke: 2px rgba(255, 255, 255, .9);
  color: var(--ink, #26312C);
}

/* -- the daily table, folded under its chart --------------------------- */
/* Closed on arrival. It is the reference, not the story: eleven of these
   open at once would be three hundred rows above nothing. */
.mg-daily { margin-top: 14px; border-top: 1px solid var(--border, #DDE3DE); }
.mg-daily > summary {
  cursor: pointer; padding: 12px 2px; font: 600 13px/1.3 var(--font-ui, sans-serif);
  color: var(--ink, #26312C);
  list-style: none; display: flex; align-items: center; gap: 8px;
}
.mg-daily > summary::-webkit-details-marker { display: none; }
/* After the words, so the heading starts at the same x as the note below it. */
.mg-daily > summary::after {
  content: "\25B8"; color: var(--muted, #65716D);
  transition: transform .15s ease; display: inline-block;
}
.mg-daily[open] > summary::after { transform: rotate(90deg); }
.mg-daily > summary:focus-visible { outline: 2px solid var(--green-deep, #174F48); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) { .mg-daily > summary::after { transition: none; } }
.mg-daily-note { font-weight: 400; color: var(--muted, #65716D); }
/* The other half of the border pair turned off at phone width -- see the note
   beside .mg-panel-head's. The panel's own outline is enough at 390px; the
   14px above the summary still separates it from the chart. */
@media (max-width: 560px) {
  .mg-daily { border-top: 0; }
  /* 2px of side padding is what you give a row that has a rule above it to
     line up with. With that border gone the summary is a lone tappable line
     against the panel's own edge, so it takes the same 12px all round -- and
     the touch target grows by the width of the padding on the side a thumb
     actually lands. */
  .mg-daily > summary { padding: 12px; }
}

/* The upper header row groups three columns under one measure, so it needs to
   read as a heading for them rather than as another row of column names. */
.mg-table tr.mg-groups th {
  text-align: center; font-size: 11px; letter-spacing: .04em; text-transform: uppercase;
  color: var(--muted, #65716D); border-bottom: 0; padding-bottom: 2px;
}
.mg-na { color: #AAB4B0; font-size: 12px; letter-spacing: .02em; }
/* A day the station half missed, and one it missed entirely. Colour on the
   date alone: tinting fourteen cells would say the readings are suspect, when
   what is short is the count behind them. */
.mg-table tr.mg-partial > th[scope="row"] { color: #9A6B1F; }
.mg-table tr.mg-nodata  > th[scope="row"] { color: #A13C2F; }
.mg-panel-body .mg-table thead th { font-size: 10px; letter-spacing: .05em; }
.mg-panel-body .mg-table th:first-child,
.mg-panel-body .mg-table td:first-child { padding-left: 0; }
.mg-panel-body .mg-table td:last-child,
.mg-panel-body .mg-table th:last-child { padding-right: 0; }

.mg-panel-foot { padding: 4px 10px 16px; }
.mg-panel-foot a, .mg-expand {
  font: 600 14px/1.2 var(--font-ui, sans-serif);
  color: var(--green-deep, #174F48);
  text-decoration: none;
  background: none; border: 0; padding: 0; cursor: pointer;
}
.mg-panel-foot a span { margin-left: 6px; }

/* The expand control points down because it opens the rest of the list in
   place; the forecast's arrow points right because that one leaves the page.
   One chevron, flipped by the expanded state, so the two directions cannot
   drift apart the way two glyphs would. */
.mg-expand { display: inline-flex; align-items: center; gap: 7px; }
.mg-chev {
  width: 14px; height: 14px; flex: 0 0 auto;
  fill: none; stroke: currentColor; stroke-width: 1.9;
  stroke-linecap: round; stroke-linejoin: round;
  transition: transform .18s ease;
}
.mg-panel.is-expanded .mg-chev { transform: rotate(180deg); }
@media (prefers-reduced-motion: reduce) { .mg-chev { transition: none; } }
/* The timestamp, wherever a panel puts it. A panel whose source is too slow
   for "Live" to mean anything moves this to the footer instead of wearing a
   pill; it should not change size on the way down. */
.mg-panel-when {
  font: 400 11px/1.3 var(--font-ui, sans-serif);
  color: var(--muted, #65716D);
}
.mg-panel-foot a:hover, .mg-expand:hover { text-decoration: underline; }

/* Forecast strip --------------------------------------------------------- */
.mg-fc { display: flex; gap: 4px; text-align: center; }
/* A COLUMN, so the moon can be pushed to the bottom of it. The condition line
   above runs to one line on some days and two on others -- "Sunny" against
   "Partly Cloudy then Slight Chance Rain Showers" -- and with everything
   stacked by margins the moons sat at whatever height the text above them
   happened to end. Five glyphs on five different baselines read as five
   different sizes. */
.mg-fc-day { flex: 1 1 0; min-width: 0; padding: 4px 2px; display: flex; flex-direction: column; }
.mg-fc-name { font: 600 13px/1.2 var(--font-ui, sans-serif); color: var(--ink, #1D2B27); }
.mg-fc-date { margin-top: 2px; font: 400 11px/1.2 var(--font-ui, sans-serif); color: var(--muted, #65716D); }
.mg-fc-temp { margin-top: 6px; font: 400 17px/1.2 var(--font-editorial, Georgia, serif); font-variant-numeric: tabular-nums; }
.mg-fc-temp .hi { color: var(--ink, #1D2B27); }
.mg-fc-temp .lo { color: var(--muted, #65716D); }
.mg-fc-cond { margin-top: 3px; font: 400 10.5px/1.3 var(--font-ui, sans-serif); color: var(--muted, #65716D); }

/*
 * The moon under each forecast column.
 *
 * Drawn, like the weather icons above it -- see moonGlyph(). An emoji moon
 * would arrive as whatever colour glyph the reader's platform ships, at a
 * size and weight nothing here chooses, in the middle of a panel where every
 * other mark is one of the palette's.
 *
 * PRINTED THE WAY AN ALMANAC PRINTS IT: the disc dark, the lit part laid over
 * it. A new moon is then a dark circle -- the moon is there and none of it is
 * showing -- rather than a gap where four other columns have a glyph.
 *
 * The lit gold is the charts' --energy, which is already the page's "sunlight"
 * colour; the dark is a desaturated slate rather than a grey, so the two read
 * as lit and unlit rather than as two greys.
 */
/* margin-top:auto eats the slack in the column, which lands every moon on one
   line across the strip whatever the condition above it did. The 10px that
   used to be this margin is padding now -- a minimum gap the auto cannot take
   back, for the day whose text runs the full height. */
.mg-fc-moon { margin-top: auto; padding-top: 10px; line-height: 0; }
.mg-moon { display: inline-block; vertical-align: middle; }
.mg-moon-dark { fill: #8A9691; }
.mg-moon-lit  { fill: var(--energy, #C69A45); }
/* Over both, so the disc keeps a clean edge where the gold meets the slate
   and where either meets the card. */
.mg-moon-edge { fill: none; stroke: #6F7B76; stroke-width: 1; opacity: .7; }

/* Drawn, not a sprite sheet -- see wxIcon(). Colour comes from the tokens, so
   a palette change carries into the icons with nothing to re-export. */
.mg-wx { width: 38px; height: 38px; margin: 8px auto 0; display: block; }
/* Beside a label rather than above a forecast column: inline, and sized to the
   cap height of the 14px uppercase text it sits with rather than to the line
   box, so it reads as part of the word and not as a second row.
   The -1px lift is the difference between an icon's optical centre and the
   baseline maths -- text-bottom aligns the box, not what is drawn in it. */
.mg-wx.is-inline {
  display: inline-block;
  width: 20px; height: 20px;
  margin: 0 0 0 8px;
  vertical-align: -5px;
}
.mg-wx circle, .mg-wx .cl { fill: var(--energy, #C69A45); stroke: none; }
.mg-wx .cl { fill: #C9D2CC; }
.mg-wx .sm { fill: var(--energy, #C69A45); }
.mg-wx .ray { stroke: var(--energy, #C69A45); stroke-width: 2.1; stroke-linecap: round; fill: none; }
.mg-wx .drop { stroke: var(--water, #3295B5); stroke-width: 2.1; stroke-linecap: round; fill: none; }
.mg-wx .bolt { fill: var(--energy, #C69A45); }
.mg-wx.is-fog .drop { stroke: #AEB8B2; }

/* Rows beyond the featured three are in the table from the start, just folded
   away -- expanding costs no request and keeps the scroll position. */
.mg-panel .mg-table tr.is-extra { display: none; }
/* A caveat that only applies to the full list travels with it. Three
   hand-picked stations are not what "may briefly go offline" is warning about,
   and a collapsed panel is short enough that the note dominated it. */
.mg-panel .mg-note.is-onexpand { display: none; }
.mg-panel.is-expanded .mg-note.is-onexpand { display: block; }
.mg-panel.is-expanded .mg-table tr.is-extra { display: table-row; }
.mg-expand .on-expanded { display: none; }
.mg-panel.is-expanded .mg-expand .on-collapsed { display: none; }
.mg-panel.is-expanded .mg-expand .on-expanded { display: inline; }

/* Scroll shadows on the panel tables.
   The air panel carries nine columns and needs about 750px; in a third of the
   page it gets under 400. It scrolls, and without this it just looks cut off.
   background-attachment mixes local and scroll so the shadow appears only on
   the side there is more table, and fades out at each end -- no JS, no resize
   listener, and it stays correct if a column is ever added. */
.mg-panel-body .mg-scroll {
  background:
    linear-gradient(to right, var(--white, #fff) 30%, rgba(255,255,255,0)) left center,
    linear-gradient(to left,  var(--white, #fff) 30%, rgba(255,255,255,0)) right center,
    radial-gradient(farthest-side at 0 50%,   rgba(23,79,72,.13), rgba(23,79,72,0)) left center,
    radial-gradient(farthest-side at 100% 50%, rgba(23,79,72,.13), rgba(23,79,72,0)) right center;
  background-repeat: no-repeat;
  background-size: 34px 100%, 34px 100%, 13px 100%, 13px 100%;
  background-attachment: local, local, scroll, scroll;
}

/* Chart readout ----------------------------------------------------------
   Positioned against the panel body, which is why that gets position:relative.
   pointer-events:none matters: the tip sits over the hit area that drives it,
   and without this it would steal the very mousemove that keeps it open. */
.mg-panel.is-chart .mg-panel-body { position: relative; }
.mg-viztip {
  position: absolute;
  pointer-events: none;
  opacity: 0;
  transition: opacity .12s;
  background: var(--white, #fff);
  border: 1px solid var(--border, #DDE3DE);
  border-radius: var(--radius-md, 12px);
  padding: 9px 11px;
  font: 400 12px/1.45 var(--font-ui, sans-serif);
  color: var(--ink, #1D2B27);
  box-shadow: 0 4px 16px rgba(0, 0, 0, .16);
  white-space: nowrap;
  z-index: 5;
}
.mg-viztip .d { color: var(--muted, #65716D); margin-bottom: 3px; }
.mg-viztip table { border-collapse: collapse; }
.mg-viztip td { border: 0; padding: 1px 0; font-size: 12px; }
.mg-viztip td.k { color: var(--muted, #65716D); padding-right: 12px; text-align: left; }
.mg-viztip td.v { text-align: right; font-variant-numeric: tabular-nums; }
.mg-viztip i { width: 9px; height: 9px; border-radius: 2px; display: inline-block; margin-right: 7px; }
@media (prefers-reduced-motion: reduce) { .mg-viztip { transition: none; } }

/* -- the leaf collection calendar -------------------------------------- */
/* Six zones across, three months down. The two states are the whole point of
   the thing, so they get named tokens rather than literals buried in a rule. */
.mg-cal-grid {
  --cal-place: #F7E3BC;     /* put leaves out */
  --cal-collect: #8A6A62;   /* the crew comes */
  display: grid;
  grid-template-columns: repeat(6, minmax(0, 1fr));
  gap: 28px 20px;
  margin-top: 20px;
}
.mg-cal-zone-name {
  margin: 0 0 10px;
  font: 400 22px/1.2 var(--font-editorial, Georgia, serif);
  /* colour inline, from the zone -- the same one the map draws it in */
}
.mg-cal { width: 100%; border-collapse: collapse; margin-bottom: 18px; }
.mg-cal caption {
  padding-bottom: 6px;
  font: 600 13px/1.2 var(--font-ui, sans-serif);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--green-deep, #174F48);
  text-align: left;
}
.mg-cal th {
  padding: 4px 0 6px;
  border-bottom: 1px dotted var(--border, #DDE3DE);
  font: 600 11px/1 var(--font-ui, sans-serif);
  color: var(--muted, #65716D);
  text-align: center;
}
/* Sunday, in the brand green rather than the printed calendar's red. */
.mg-cal th.is-sun, .mg-cal td.is-sun { color: var(--green, #679B58); font-weight: 600; }
.mg-cal th abbr { text-decoration: none; border: 0; }
/* The line-height matches the mark's height on purpose. Without it a row with
   a circle in it is taller than one without, every zone marks different days,
   and the six columns drift apart -- November began at six different heights
   across the row, which is exactly the comparison this layout exists to make. */
.mg-cal td {
  padding: 2px 0;
  font: 400 12px/22px var(--font-ui, sans-serif);
  color: var(--ink, #26312C);
  text-align: center;
}
/* The mark sits INSIDE the cell rather than being the cell's own background:
   a circle and a square have to be the same size as each other and smaller
   than the column, and a table cell is neither. */
.mg-cal-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px; height: 22px;
  vertical-align: middle;
}
.mg-cal-mark.is-place   { background: var(--cal-place);   border-radius: 50%; }
.mg-cal-mark.is-collect { background: var(--cal-collect); border-radius: 3px; color: #fff; }
/* The dates, hung off the zone label. One month to a line: ten dates run
   together is a paragraph, and the tooltip would be wider than it is tall. */
.leaflet-tooltip.mg-leaf-dates { text-align: left; line-height: 1.45; }
.mg-dates-head {
  display: block;
  margin-top: 2px;
  font-size: 11px;
  opacity: 0.75;
}
.mg-dates-row { display: block; white-space: nowrap; }

.mg-cal-swatch {
  display: inline-block;
  width: 14px; height: 14px;
  margin-right: 8px;
  vertical-align: -2px;
}
.mg-cal-key .is-place   { background: #F7E3BC; border-radius: 50%; }
.mg-cal-key .is-collect { background: #8A6A62; border-radius: 3px; }

/* Six 210px columns need about 1360. Below that they halve rather than shrink:
   a month is seven columns wide and stops being readable long before it stops
   fitting. */
@media (max-width: 1279px) {
  .mg-cal-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (max-width: 860px) {
  .mg-cal-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 520px) {
  .mg-cal-grid { grid-template-columns: 1fr; gap: 24px; }
}

/* -- the events calendar ------------------------------------------------ */
/* The grid is .mg-cal, borrowed whole from the leaf collection calendar --
   same month name, same Sundays in green, same 22px cells. Only the marks
   differ: there a day is a state, here it is a link to the entry below. */
/* Clear of whatever the page puts above it, for EVERY view. On the wrapper
   rather than repeated on each one -- a view that wants something different
   can say so, and none of them do. */
.mg-events { margin-top: 20px; }
.mg-events-head {
  display: flex;
  align-items: baseline;
  gap: 16px;
  margin-bottom: 14px;
}
/* The middle of the header: the month, and under it the way up to the year.
   Takes the space between the arrows, so both centre together. */
.mg-events-heading { flex: 1 1 auto; text-align: center; }
.mg-events-all {
  display: inline-block;
  margin-top: 2px;
  font: 500 12px/1.3 var(--font-ui, sans-serif);
  color: var(--muted, #65716D);
  text-decoration: none;
}
.mg-events-all:hover { color: var(--green-deep, #174F48); text-decoration: underline; }
/* THE HEADING, not the month view. These shared .mg-events-month until a
   request to centre "the month" would have centred every cell of the grid
   as well -- the wrapper is .mg-events-monthview now and this is the title.

   flex:1 so it takes the space between the arrows and centres its text in
   it; the arrows keep their own ends. */
.mg-events-title {
  margin: 0;
  font: 400 26px/1.2 var(--font-editorial, Georgia, serif);
  color: var(--green-deep, #174F48);
}
/* Clear of whatever the page puts above it -- the standfirst on /events sits
   right on top of January otherwise. */
/* The later arrow to the right, the earlier one where it already is: a month
   runs left to right and so should the way out of it. */
/* No margin-left:auto on the last step now -- .mg-events-heading takes the
   middle with flex:1, so the next arrow already sits at the right edge. */
.mg-events-step {
  font: 500 13px/1.3 var(--font-ui, sans-serif);
  color: var(--muted, #65716D);
  text-decoration: none;
  white-space: nowrap;
}
.mg-events-step:hover { color: var(--green-deep, #174F48); text-decoration: underline; }
/* Wider than the leaf calendar's: that one is a sixth of a row of six, this
   one is the page. */
.mg-events-grid { max-width: 420px; }
.mg-events-grid td { font-size: 13px; }

/* A day with something on it. A filled circle like the leaf calendar's, but a
   link -- and underlined on hover rather than recoloured, because the colour
   is what marks it as a day with an event at all. */
.mg-cal-mark.is-event {
  background: var(--green, #679B58);
  border-radius: 50%;
  color: #fff;
  text-decoration: none;
  font-weight: 600;
}
.mg-cal-mark.is-event:hover { background: var(--green-deep, #174F48); }
.mg-cal-mark.is-event:focus-visible {
  outline: 2px solid var(--green-deep, #174F48);
  outline-offset: 2px;
}

/* .mg-event-list and its rows are gone with mgd_event_row(), the list the
   calendar drew under the grid before the month view put the events in the
   cells. .mg-event-source stays: the featured cards still credit a borrowed
   event, and .mg-card-where carries the address styling now. */
/* The pin, riding on the end of the address. Inline-block with a nudge down so
   it sits on the text's baseline rather than hanging off the cap height, and
   nowrap on the last comma part so the glyph never lands alone on a line
   below the address it belongs to. */
.mg-address-pin {
  display: inline-block;
  vertical-align: -2px;
  margin-left: 4px;
  color: var(--green, #679B58);
  line-height: 0;
}
.mg-address-pin:hover { color: var(--green-deep, #174F48); }
.mg-address-pin:focus-visible { outline: 2px solid var(--green-deep, #174F48); outline-offset: 2px; border-radius: 2px; }

.mg-event-source { margin: 6px 0 0; font-size: 12.5px; color: var(--muted, #65716D); }
.mg-event-source a { color: var(--green-deep, #174F48); }


@media (max-width: 560px) {
  .mg-events-head { flex-wrap: wrap; gap: 8px 12px; }
  /* Onto its own line and back to the left: centred over a wrapped row of
     arrows it floats between them rather than heading anything. */
  .mg-events-heading { order: -1; width: 100%; flex: none; text-align: left; }
  .mg-events-head .mg-events-step:last-of-type { margin-left: auto; }
}

/* -- the events calendar: the year, and one month ----------------------- */

/* THE YEAR: twelve small months, six across, the whole thing on one screen.
   That is the whole point of it -- a reader who lands in a quiet month wants
   to see where the events actually are, not an empty grid. */
.mg-yv {
  display: grid;
  grid-template-columns: repeat(6, minmax(0, 1fr));
  gap: 26px 20px;
  /* Matching the gap's own 20px. It was 6, which put the first row of months
     tighter to the year heading than the second row is to the first -- the
     grid looked like it started early. */
  margin-top: 20px;
}
.mg-yv-name {
  margin: 0 0 6px;
  font: 600 12px/1.2 var(--font-ui, sans-serif);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--green-deep, #174F48);
}
.mg-yv-name a { color: inherit; text-decoration: none; }
.mg-yv-name a:hover { text-decoration: underline; text-underline-offset: 3px; }
.mg-yv-grid { width: 100%; }
.mg-yv-grid td { font-size: 11px; line-height: 20px; padding: 1px 0; }
.mg-yv-grid .mg-cal-dow { font-size: 10px; padding-bottom: 4px; }
.mg-yv-grid .mg-cal-mark.is-event { width: 20px; height: 20px; }
@media (max-width: 1279px) { .mg-yv { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
@media (max-width: 900px)  { .mg-yv { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 640px)  { .mg-yv { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 400px)  { .mg-yv { grid-template-columns: 1fr; } }

/* ONE MONTH, with the events written into the days.
   A grid of divs rather than a table: a day here is a box holding a list, not
   a cell holding a value, and the cells have to be the same height across a
   week whatever is in them -- which is what grid does and a table fights. */
.mg-mv { margin-top: 6px; }
.mg-mv-head,
.mg-mv-grid {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
}
.mg-mv-head { border-bottom: 1px solid var(--border, #DDE3DE); }
.mg-mv-head .mg-cal-dow {
  padding: 0 0 8px 2px;
  text-align: left;
  font: 600 11px/1.2 var(--font-ui, sans-serif);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted, #65716D);
}
.mg-mv-head .mg-cal-dow.is-sun { color: var(--green, #679B58); }
.mg-mv-grid { gap: 1px; background: var(--border, #DDE3DE); border: 1px solid var(--border, #DDE3DE); border-top: 0; }
.mg-mv-cell {
  min-height: 116px;
  padding: 6px;
  background: var(--white, #fff);
}
/* The days either side of the month, which belong to another one. Greyed
   rather than blank so the shape of the month still reads as a month. */
.mg-mv-cell.is-empty { background: #FAFBF9; }
.mg-mv-cell.is-today { background: #F4F8F2; }
.mg-mv-num {
  display: block;
  font: 600 12px/1.2 var(--font-ui, sans-serif);
  color: var(--muted, #65716D);
}
.mg-mv-cell.is-today .mg-mv-num { color: var(--green-deep, #174F48); }
.mg-mv-chips { display: flex; flex-direction: column; gap: 4px; margin-top: 6px; }
/* The event itself, in the box. A reader should not have to scroll to a list
   below to find out what happened on the day they are looking at. */
.mg-mv-chip {
  display: block;
  padding: 4px 7px;
  border-radius: 4px;
  font: 500 11.5px/1.35 var(--font-ui, sans-serif);
  color: #fff;
  background: var(--green-solid, #527C46);
  text-decoration: none;
  overflow-wrap: break-word;
}
a.mg-mv-chip:hover { filter: brightness(1.08); }
a.mg-mv-chip:focus-visible { outline: 2px solid var(--green-deep, #174F48); outline-offset: 2px; }
/* The same three colours the tags use, so a Meeting is the same blue wherever
   it appears on the site. */
.mg-mv-chip.is-event    { background: var(--green-solid, #527C46); }
.mg-mv-chip.is-meeting  { background: #256F88; }
.mg-mv-chip.is-calendar { background: var(--orange-solid, #B8560E); }
.mg-mv-time { opacity: 0.85; }

@media (max-width: 860px) {
  .mg-mv-cell { min-height: 92px; padding: 5px; }
  .mg-mv-chip { font-size: 11px; padding: 3px 6px; }
}
/* Below this a seven-column month is 50px a column, which fits a date and
   nothing else. The grid becomes a column of days that have something on. */
@media (max-width: 560px) {
  .mg-mv-head { display: none; }
  .mg-mv-grid { grid-template-columns: 1fr; gap: 0; border: 0; background: none; }
  .mg-mv-cell { min-height: 0; border-bottom: 1px solid var(--border, #DDE3DE); }
  .mg-mv-cell.is-empty { display: none; }
  .mg-mv-cell:not(.has-events) { padding: 4px 6px; opacity: 0.55; }
}

/* The hover card on the month view.
   A chip is a name in a box six words wide. Everything else about the event
   has nowhere to go in a day cell, so rather than make a reader open the link
   to find out whether the link is worth opening, the card comes to them. */
.mg-mv-event { position: relative; }
.mg-mv-card {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  z-index: 30;
  width: 250px;
  padding: 12px 14px;
  background: var(--white, #fff);
  border: 1px solid var(--border, #DDE3DE);
  border-radius: var(--radius-md, 12px);
  box-shadow: 0 6px 22px rgba(38, 49, 44, .14);
  /* Hidden by visibility and not display:none, so it can be transitioned and
     so a screen reader's own hiding is not the only thing keeping it out. */
  visibility: hidden;
  opacity: 0;
  transition: opacity .12s ease, visibility .12s;
}
.mg-mv-event:hover .mg-mv-card,
.mg-mv-event:focus-within .mg-mv-card { visibility: visible; opacity: 1; }
/* The hovered cell rises above its neighbours; without this the card slides
   under the next day's white background. */
.mg-mv-cell { position: relative; }
.mg-mv-cell:hover,
.mg-mv-cell:focus-within { z-index: 20; }
/* Friday and Saturday open to the left -- a 250px card hanging off the right
   of the last column leaves the page. */
.mg-mv-grid > .mg-mv-cell:nth-child(7n) .mg-mv-card,
.mg-mv-grid > .mg-mv-cell:nth-child(7n-1) .mg-mv-card { left: auto; right: 0; }

.mg-mv-card-when {
  margin: 0;
  font: 600 11px/1.3 var(--font-ui, sans-serif);
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--green, #679B58);
}
.mg-mv-card-title {
  margin: 4px 0 0;
  font: 400 17px/1.25 var(--font-editorial, Georgia, serif);
  color: var(--green-deep, #174F48);
}
.mg-mv-card-where,
.mg-mv-card-summary,
.mg-mv-card-source { margin: 5px 0 0; font-size: 12.5px; line-height: 1.45; }
.mg-mv-card-where { color: var(--muted, #65716D); }
.mg-mv-card-summary { color: var(--ink, #26312C); }
.mg-mv-card-source { color: var(--muted, #65716D); }
.mg-mv-card-tag { margin: 9px 0 0; }
.mg-mv-card-tag .mg-tag { padding: 2px 7px; font-size: 10px; }

/* No hover on a touch screen, and no room for a floating card beside a
   one-column month. The chip already carries the title and the time. */
@media (max-width: 560px), (hover: none) {
  .mg-mv-card { display: none; }
}

/* -- gt_voters: registered voters as extruded districts -------------------- */

.mg-prism {
  position: relative;
  border: 1px solid var(--border, #DDE3DE);
  border-radius: var(--radius-md, 12px);
  background: var(--white, #fff);
  overflow: hidden;
}
.mg-prism-canvas {
  display: block;
  width: 100%;
  /* THE SAME HEIGHT AS THE FLAT MAP, and written as the same expression
     rather than as a number that matches today. The two sit one above the
     other on /election-district-map/ and are two views of one township; a
     pair of panels that differ by eighty pixels reads as a mistake before it
     reads as anything else. If .mg-edmap's clamp moves, this has to move with
     it -- which is why it is copied verbatim and not approximated. */
  height: clamp(420px, 68vh, 760px);
  /* grab, not pointer: the thing under the cursor moves with it rather than
     being activated by it, and the two cursors mean different promises. */
  cursor: grab;
  touch-action: none;   /* or a drag to turn scrolls the page instead */
  outline: none;
}
.mg-prism-canvas.is-turning { cursor: grabbing; }
.mg-prism-canvas:focus-visible { outline: 2px solid var(--green-deep, #174F48); outline-offset: -2px; }
/* No phone override. The flat map has none either -- its clamp floors at 420
   -- and the whole point here is that the two are the same height. */

.mg-prism-tip {
  position: absolute;
  pointer-events: none;           /* or the tip chases the pointer away */
  transform: translate(14px, -50%);
  max-width: 230px;
  padding: 8px 10px;
  border-radius: var(--radius-sm, 6px);
  background: rgba(38, 49, 44, .94);
  color: #F7F6F1;
  font: 400 12px/1.45 var(--font-ui, sans-serif);
}
.mg-prism-tip strong { display: block; font-size: 13px; }
.mg-prism-tip span { display: block; }
.mg-prism-off { color: #B9C4BD; }

/* TOP LEFT, where the flat map keeps its controls.
   Leaflet puts zoom and the fit button in that corner on every other map on
   the site, so a reader looking for the controls of the map underneath looks
   there first. Bottom right was where there happened to be room. */
.mg-prism-hint {
  position: absolute; left: 12px; top: 10px; margin: 0;
  font: 400 11.5px/1 var(--font-ui, sans-serif);
  color: var(--muted, #65716D);
}
.mg-prism-reset {
  appearance: none; background: none; border: 0; padding: 0;
  font: 600 11.5px/1 var(--font-ui, sans-serif);
  color: var(--green-deep, #174F48); cursor: pointer;
}
.mg-prism-reset:hover { text-decoration: underline; }

/* The ramp, as one continuous strip rather than six separate keys: it stands
   for a scale without steps, and gaps between the swatches would say the
   opposite. */
.mg-prism-key { align-items: center; margin-top: 12px; }
.mg-prism-ramp { display: inline-flex; }
.mg-prism-ramp .sq { width: 26px; height: 11px; border-radius: 0; }
.mg-prism-ramp .sq:first-child { border-radius: 3px 0 0 3px; }
.mg-prism-ramp .sq:last-child { border-radius: 0 3px 3px 0; }

/* The figures under the drawing. A bar per district, because the column the
   3D view is worst at reading -- how close the districts are to each other --
   is the one a flat bar answers at a glance. */
.mg-prism-table .mg-table { width: 100%; }
.mg-prism-table th { text-align: left; font-weight: 600; white-space: nowrap; }
.mg-prism-table td:last-child { text-align: right; font-variant-numeric: tabular-nums; }
.mg-prism-bar { width: 100%; padding-inline: 12px; }
.mg-prism-bar span {
  display: block; height: 10px; border-radius: 2px; min-width: 2px;
}

/* -- the street-tree difference view ------------------------------------- */

/* The same row shape as the species keys above it, because it is the same
   kind of control and the reader has just learned how that one works. The
   count sits hard right, so the dates scan as a column of numbers. */
.mg-tree-times { list-style: none; margin: 0; padding: 0; }
.mg-tree-times li { margin: 0 0 4px; font-size: 13px; }
.mg-tree-tkey {
  display: flex; align-items: center; gap: 8px;
  cursor: pointer; user-select: none;
}
.mg-tree-tkey input {
  accent-color: var(--green-deep, #174F48); margin: 0; flex: 0 0 auto;
}
.mg-tree-tkey span { flex: 1 1 auto; }
/* Tabular, so a column of counts lines up on its digits rather than drifting
   with the width of a 7 against a 1. */
.mg-tree-tkey b {
  font-weight: 400; color: var(--muted, #65716D);
  font-variant-numeric: tabular-nums;
}

/* A note that belongs to the control above it, not to the section. The
   section's own spacing would put it a paragraph away and it would read as a
   new thought instead of a caveat on the thing just above. */
.mg-note.is-tight { margin: 6px 0 0; font-size: 12px; }

/* The provenance line inside a road tooltip. Smaller and muted: it qualifies
   the number above it rather than competing with it, and it is longer than
   anything else in a tip, so it needs its own measure. */
.mg-tip .mg-tip-note {
  display: block;
  margin-top: 6px;
  max-width: 30ch;
  font-size: 11px;
  line-height: 1.35;
  opacity: .8;
}

/* -- gt_roads: pavement width -------------------------------------------- */

/* Its own stacking context, as .mg-leafmap and .mg-map have: Leaflet gives
   its panes z-index 400 and its controls 1000, which loose in the page beat
   the sticky header's 100 and draw over it on the way past. */
/* The road map. Same shape as the flood map -- a legend column on the left
   and the pane on the right inside .mg-map-layout -- so the two sit together
   in the gallery grid instead of one being a different kind of card. The
   radius is squared off on the left for the same reason: the aside is against
   it. */
.mg-roadmap {
  position: relative;
  z-index: 0;
  height: clamp(420px, 70vh, 800px);
  border-radius: 0 var(--radius-md, 12px) var(--radius-md, 12px) 0;
  border: 1px solid var(--border, #DDE3DE);
  overflow: hidden;
}
.mg-roadmap.is-compact { height: auto; min-height: 400px; }

/* Six bins plus the dashed case, under the map rather than over it -- the
   same call as the leaf key, and for the same reason: a corner cannot hold
   seven entries without covering the streets they describe. */
.mg-road-keys span { display: flex; align-items: center; gap: 8px; }
.mg-road-keys .n { flex: 1 1 auto; }
/* The mileage is secondary to the range it belongs to, and tabular so a
   column of figures lines up on its digits. */
/* Never broken across lines. "0.3 / 1.4 mi" is one quantity, and in the
   240px aside of the solo page -- narrower again now that each row carries a
   checkbox -- it was splitting after the slash and leaving "mi" alone on the
   second line. The name beside it may wrap; the figure may not. */
.mg-road-keys b {
  font-weight: 400; color: var(--muted, #65716D);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* The sharrow-route key. A broad flat bar rather than a rule, because that is
   what the casing looks like on the map: a band under the street, not a line
   beside it. */
/* The casing swatch. Wider and shorter than a line key on purpose: on the map
   this is a band under the road, not another road, and a swatch the same shape
   as the width keys above it would invite reading it as one more step on that
   scale. The colour arrives inline from the feed -- it is a row in
   bike_treatments, not a decision taken here. */
.mg-road-keys .mg-road-glow {
  width: 22px; height: 9px; border-radius: 2px;
  background: #F2C230; opacity: .8;
  display: inline-block; flex: 0 0 auto;
}
/* Built, not proposed -- the same hue, fainter, matching both the map and
   Figure 12's own way of telling an existing sharrow from a planned one. */
.mg-road-keys .mg-road-glow.is-built { opacity: .4; }

/* THE WHOLE ROW IS THE SWITCH -- box, swatch, name and mileage inside one
   label, so there is no small target to hunt for and no dead space between
   the tick and the thing it names. The species list made the same call.
   A label, not the <span> the width keys use, so it needs the layout saying
   again rather than inheriting `.mg-road-keys span`. */
.mg-road-keys .mg-road-skey {
  display: flex; align-items: center; gap: 8px;
  cursor: pointer; user-select: none;
}
.mg-road-keys .mg-road-skey input {
  accent-color: var(--green-deep, #174F48); margin: 0; flex: 0 0 auto;
}
/* `.mg-legend.is-stacked i` nudges every swatch down 2px, which is right for
   the width keys: those rows are top-aligned, so the swatch has to drop to
   meet the first line of a label that may wrap. A switch row is centred on
   its checkbox and the nudge puts the band 2px low against it. Three classes
   to beat two classes and an element -- the cascade, not an !important. */
.mg-road-keys .mg-road-skey .mg-road-glow { margin-top: 0; }

/* THE THREE DASHES NEED AIR, NOW THAT THEY SIT UNDER THE SCALE.
   They are not a seventh, eighth and ninth width -- they are the streets that
   have no single width at all -- and at the 10px that separates two keys
   inside a block they read as more of the same list. The base .mg-legend's
   12px top margin was that same nothing, so it is stated here instead: the
   gap between the two groups wants to be plainly larger than the gap between
   two rows of one. */
.mg-road-keys.is-dashes { margin-top: 20px; }

/* THE CONTROL PAIR DROPS TO A SECOND LINE RATHER THAN OVERFLOWING.
   .mg-tree-sub is a flex row with no wrap, which is right where the pair is
   two short words. "Draw in Colour Greyscale" beside "ROAD WIDTH" in half a
   card's width is not, and an overflowing heading pushes the legend sideways
   instead of just taking another line. Scoped to this map's headings: the
   others have nothing long enough to need it. */
.mg-tree-sub.is-road { flex-wrap: wrap; }

/* -- gt_ballots ---------------------------------------------------------- */

/* Same stacking context and the same clamp as the other full-width maps, so
   two of them stacked on a page are not two different heights. */
.mg-ballotmap {
  position: relative;
  z-index: 0;
  height: clamp(420px, 70vh, 800px);
  border-radius: 0 var(--radius-md, 12px) var(--radius-md, 12px) 0;
  border: 1px solid var(--border, #DDE3DE);
  overflow: hidden;
}
.mg-ballotmap.is-compact { height: auto; min-height: 400px; }

/*
 * THE ONE NUMBER THE MAP CANNOT DRAW.
 *
 * The map is sixteen shares; the town is one total, and that total is the
 * first thing a reader wants. It gets the size of a heading because it is
 * one, and its denominator gets a line underneath, because "1,870" is a fact
 * about nothing until you know it is 11% of 17,035.
 */
.mg-ballot-big {
  margin: 0 0 14px;
  font: 400 13px/1.35 var(--font-ui, sans-serif);
  color: var(--muted, #65716D);
}
.mg-ballot-big b {
  display: block;
  font: 700 30px/1 var(--font-ui, sans-serif);
  color: var(--ink, #26312C);
  font-variant-numeric: tabular-nums;
  margin-bottom: 4px;
}
.mg-ballot-big span { display: block; margin-top: 2px; }

/* The two halves of that total, which are two different acts -- a ballot
   posted weeks ago and a trip to a polling place -- and want telling apart
   rather than summing in the reader's head. */
.mg-ballot-split {
  margin: 0; padding: 0; list-style: none;
  display: flex; flex-direction: column; gap: 8px;
  font: 400 12px/1.35 var(--font-ui, sans-serif);
  color: var(--muted, #65716D);
}
.mg-ballot-split b {
  color: var(--ink, #26312C);
  font-variant-numeric: tabular-nums;
}
.mg-ballot-split span { display: block; }

/*
 * THE THREE SWITCHES TAKE A ROW OF THEIR OWN.
 *
 * .mg-tree-pick is built to sit at the far end of a heading -- margin-left:
 * auto, pushed right by the flex line it shares. That works for two short
 * words; the road map's pair only just fits in a 240px aside and had to lose
 * its label to do it. Three do not fit at all, so the auto margin comes off
 * and they start under the heading instead, which also puts them in reading
 * order with the scale they govern.
 */
.mg-tree-pick.is-ballot-mode {
  margin: 2px 0 0; gap: 14px;
  font-size: 12px;
}
.mg-tree-pick.is-ballot-mode button { font-size: 12px; }

/* What the shading currently means, in words, under the switches. The swatches
   below carry only numbers, and a column of percentages with no sentence over
   it is three different maps that look identical. */
.mg-ballot-sub {
  margin: 8px 0 12px;
  font: 400 12px/1.4 var(--font-ui, sans-serif);
  color: var(--muted, #65716D);
}

/* Swatch, range, and how many districts fall in it -- the count at the far
   end in the muted weight the road map's mileages use, for the same reason:
   it answers the next question, not the first one. Sixteen polygons is few
   enough that a reader tries to count them off the map and gets it wrong. */
.mg-ballot-keys { gap: 7px; }
.mg-ballot-keys span { width: 100%; gap: 8px; }
.mg-ballot-keys .n { font-variant-numeric: tabular-nums; }
.mg-ballot-keys b {
  margin-left: auto;
  font-weight: 400; color: var(--muted, #65716D);
  font-variant-numeric: tabular-nums;
}
/* A touch larger than the round keys elsewhere: these are area fills, and a
   12px dot is a poor sample of a colour that covers a quarter of the map. */
.mg-ballot-keys i.sq { width: 18px; height: 12px; border-radius: 2px; }

/* The share under the district number. Lighter than .mg-ed-voters' count
   because it is a percentage rather than a figure to read off -- the exact
   number is in the popup, and this is here so a reader can rank two districts
   without clicking either. */
.mg-ballot-label .mg-ed-voters { font-size: 11px; }

/*
 * ON THE GALLERY, THE SCALE LIES DOWN.
 *
 * Side by side with another map this card's block under the map came out
 * taller than the map itself -- 490px of figures and seven stacked keys
 * under a 390px map -- which is the complaint the road map's legend earned,
 * arriving again in the layout with the least room for it. A card in the
 * gallery is a way in to its own page, so the scale goes horizontal and the
 * per-band counts come off: the range is what makes the colours readable, and
 * how many districts are in each band is detail for the page this links to.
 *
 * Three classes, so it beats .mg-legend.is-stacked outright rather than on
 * source order -- the two are otherwise tied, and a tie that depends on which
 * rule is further down the file is a tie waiting to be lost to an edit.
 */
.mg-map-pair .mg-legend.mg-ballot-keys {
  flex-direction: row;
  gap: 8px 14px;
}
.mg-map-pair .mg-legend.mg-ballot-keys span { width: auto; }
.mg-map-pair .mg-legend.mg-ballot-keys b { display: none; }

/* -- the party columns on the prism map ---------------------------------- */

/* The drawing is the map cell of .mg-map-layout now, so it loses its left
   corners to the seam the aside runs into. */
.mg-map-layout > .mg-prism {
  border-radius: 0 var(--radius-md, 12px) var(--radius-md, 12px) 0;
}

/* The ramp has to fit a fifth of the grid -- about 200px -- so the two
   figures go under it rather than either side of it, and the swatches come
   down from 26px to 22. */
.mg-map-aside .mg-prism-key {
  margin-top: 8px; flex-wrap: wrap; gap: 4px 8px;
  font-variant-numeric: tabular-nums;
}
.mg-map-aside .mg-prism-ramp { flex: 0 0 100%; }
.mg-map-aside .mg-prism-ramp .sq { width: 22px; height: 10px; }
/* The two ends pushed apart under the bar they label. */
.mg-map-aside .mg-prism-key > span:last-child { margin-left: auto; }

.mg-aside-fig {
  margin: 8px 0 0;
  font: 400 12px/1.45 var(--font-ui, sans-serif);
  color: var(--muted, #65716D);
}
.mg-aside-fig b { color: var(--ink, #26312C); font-variant-numeric: tabular-nums; }

/* The one number the drawing cannot show: the map is sixteen districts and
   the township is one total. */
.mg-ballot-big {
  margin: 0 0 4px;
  font: 400 12px/1.4 var(--font-ui, sans-serif);
  color: var(--muted, #65716D);
}
.mg-ballot-big b {
  display: block;
  font: 700 26px/1 var(--font-ui, sans-serif);
  color: var(--ink, #26312C);
  font-variant-numeric: tabular-nums;
  margin-bottom: 3px;
}
.mg-ballot-big span { display: block; }

/* One block per channel: a label with its count, a proportion bar, and the
   three shares under it. */
.mg-share-head {
  display: flex; justify-content: space-between; align-items: baseline;
  gap: 8px; margin: 16px 0 6px;
  font: 600 11px/1.3 var(--font-ui, sans-serif);
  text-transform: uppercase; letter-spacing: .06em;
  color: var(--muted, #65716D);
}
.mg-share-head b {
  font: 700 13px/1 var(--font-ui, sans-serif);
  text-transform: none; letter-spacing: 0;
  color: var(--ink, #26312C); font-variant-numeric: tabular-nums;
}
.mg-share-bar {
  display: flex; height: 10px; border-radius: 3px; overflow: hidden;
  background: var(--border, #DDE3DE);
}
.mg-share-bar i { display: block; height: 100%; }

.mg-share {
  list-style: none; margin: 8px 0 0; padding: 0;
  display: flex; flex-direction: column; gap: 7px;
  font: 400 12px/1.35 var(--font-ui, sans-serif);
  color: var(--muted, #65716D);
}
/* Swatch in its own track, the share and its label stacked in the second:
   side by side they wrapped on every row at this width. */
.mg-share li { display: grid; grid-template-columns: 10px 1fr; gap: 0 7px; }
.mg-share i {
  grid-column: 1; grid-row: 1; align-self: center;
  width: 10px; height: 10px; border-radius: 2px;
}
.mg-share b {
  grid-column: 2; font: 700 15px/1.1 var(--font-ui, sans-serif);
  color: var(--ink, #26312C); font-variant-numeric: tabular-nums;
}
.mg-share span { grid-column: 2; }

.mg-bars-key { gap: 14px; margin-top: 8px; }
.mg-bars-key i.sq { width: 11px; height: 11px; border-radius: 2px; }
.mg-map-aside .mg-tree-pick.is-bars { margin: 8px 0 0; gap: 14px; }

/*
 * TWO MORE COLUMNS IN THE TABLE, and the bar keeps the room it had.
 *
 * .mg-prism-table td:last-child right-aligns the one figure the table used to
 * carry; with two more after it, every number wants that treatment and the
 * header has to say which is which. Tabular figures because these are read
 * down the column, not across.
 */
.mg-prism-table-bars thead th {
  font: 600 11px/1.3 var(--font-ui, sans-serif);
  text-transform: uppercase; letter-spacing: .06em;
  color: var(--muted, #65716D); text-align: right; padding-bottom: 4px;
}
.mg-prism-table-bars td { text-align: right; font-variant-numeric: tabular-nums; }
/* 26, not 34: there are three figure columns after it now. */
.mg-prism-table-bars td.mg-prism-bar { width: 26%; }
.mg-prism-table-bars .mg-prism-dem { color: #2166AC; font-weight: 600; }
.mg-prism-table-bars .mg-prism-rep { color: #B2182B; font-weight: 600; }
.mg-prism-table-bars .mg-prism-other { color: #5F6C67; font-weight: 600; }

/* -- full-viewport blocks ------------------------------------------------ */

/*
 * [gt_voters full="1"] -- the block takes the height of the screen.
 *
 * FOR A PAGE WHOSE WHOLE JOB IS ONE MAP. In a column of cards a map is one
 * card among several and 420 to 760px is right; on a page that exists to show
 * it, that leaves the drawing in a letterbox with the browser's chrome and a
 * band of nothing either side, and a reader scrolls past the thing they came
 * for to find out there was nothing else.
 *
 * THE THEME IS THE STATION PAGES', because they already solved this: a tinted
 * ground with the content as white cards on it, which is what makes a full
 * screen of one component read as a considered page rather than as a card
 * that failed to stop. Those pages are static HTML outside WordPress with
 * their own stylesheet, so the five values are repeated rather than shared --
 * they are a palette and not a mechanism, and an import to carry five hex
 * codes across two serving paths would be the more fragile answer.
 *
 *   --ink #1c2321  --muted #5c6b66  --line #dfe5e2  --bg #f7f9f8  --card #fff
 *
 * dvh, not vh. On a phone vh is the viewport with the browser's address bar
 * HIDDEN, which it is not when the page loads -- so a 100vh block starts
 * taller than the screen and the foot of it is under the chrome until the
 * reader scrolls. dvh follows the bar as it comes and goes.
 */
.mg-component.is-full {
  background: #F7F9F8;
  border: 1px solid #DFE5E2;
  border-radius: 10px;
  padding: 14px;
}

/*
 * The 200px is the page around the block: the theme's header, the page title
 * above this, and room to see that the table below exists. Floored, because
 * on a short laptop screen in landscape the subtraction wins and the map
 * would end up shorter than it is in a card.
 */
/*
 * AS TALL AS THE DRAWING NEEDS, AND NO TALLER.
 *
 * Two goes at this. First the height was on the whole grid, so the footnotes'
 * own row -- two paragraphs at full width, 104px of it -- came out of the map
 * and the panel, and the panel then overflowed by almost exactly that much
 * while appearing to have room to spare. Moving it to the map row fixed that
 * and left a worse thing behind: a flat calc(100dvh - 200px), which is the
 * wrong question. The township is wide and short, so the fit is limited by
 * WIDTH and the surplus height was empty canvas -- half a screen of it on a
 * tall window.
 *
 * The cost of that is not the white space. It is that the footnotes sat under
 * it and nobody scrolled far enough to read them, and this map's footnotes
 * are where it says what a ballot in hand is, and that party here means
 * registration rather than a vote.
 *
 * --mg-map-h is published by the renderer on every draw: the height at which
 * the drawing exactly fills the width it has been given. The stylesheet keeps
 * the last word over it -- the floor stops a very wide, very flat container
 * collapsing the map to a strip, and the ceiling keeps it on one screen.
 *
 * THE CEILING RESERVES THE FOOTNOTES, which is the whole point of this. At
 * 100dvh - 160px a 1013px window put the foot of them eleven pixels below the
 * fold: better than the half-screen it was, and still a map that ends exactly
 * where the reader stops. 260 is the two paragraphs, the table summary under
 * them, and the page's own chrome -- measured, not guessed. On a tall screen
 * it never binds, because the drawing's own height is the smaller number.
 */
.mg-component.is-full .mg-map-layout {
  grid-template-rows:
    clamp(420px, var(--mg-map-h, 560px), calc(100dvh - 260px)) auto;
}
.mg-component.is-full .mg-prism,
.mg-component.is-full .mg-prism-canvas,
.mg-component.is-full .mg-ballotmap { height: 100%; min-height: 0; }

/*
 * The panel is the one thing here with no natural height, so it is what
 * scrolls when the screen is short -- and it has to SAY so. On a 1440x800
 * laptop the last share row is cut through the middle, which on its own reads
 * as a rendering fault rather than as an invitation.
 *
 * A styled scrollbar rather than a fade at the foot: a mask would dim the
 * last 28px whether or not there is anything below it, so on the common case
 * -- where it all fits -- the switch at the bottom would look disabled. A
 * scrollbar appears exactly when the element can scroll, which is the
 * condition we actually want to signal. macOS hides overlay scrollbars until
 * they move; giving ::-webkit-scrollbar a width opts out of overlay mode.
 */
.mg-component.is-full .mg-map-aside {
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: #C7D0CB transparent;
}
/* The classes are set from measurement -- see syncAsideFade() -- because a
   fade applied unconditionally dims the foot of every panel that fits, and
   one left on at the end of the scroll half-lights the last row for good. */
.mg-component.is-full .mg-map-aside.is-scrollable:not(.is-atend) {
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 34px), transparent);
          mask-image: linear-gradient(to bottom, #000 calc(100% - 34px), transparent);
}

/* The notes sit on the tinted ground rather than in a card, as they do on the
   station pages, so they take that palette's muted ink. */
.mg-component.is-full .mg-map-foot .mg-map-note { color: #5C6B66; }

/* The table under the map is a card on the tint, matching the station pages'
   own closed-on-arrival table. */
.mg-component.is-full .mg-prism-table {
  background: #fff;
  border: 1px solid #DFE5E2;
  border-radius: 10px;
  margin-top: 14px;
  padding: 0 14px;
}

/*
 * Below 860 the layout stacks, and a stacked block cannot also be one screen
 * tall -- the map and the panel would have half a screen each, which is too
 * little for either. The height comes off and both take their natural size.
 */
@media (max-width: 860px) {
  .mg-component.is-full { padding: 10px; }
  /*
   * grid-template-rows, not height. This said `height: auto` and had stopped
   * doing anything: the screen height moved onto the ROWS when the footnotes
   * turned out to be eating 104px of the map, and nothing reset it here. So
   * the stacked layout kept a map row of calc(100dvh - 200px) with a 760px
   * canvas in it, and left six hundred pixels of white between the drawing
   * and the legend under it.
   */
  .mg-component.is-full .mg-map-layout {
    grid-template-rows: auto; height: auto; min-height: 0;
  }
  .mg-component.is-full .mg-prism,
  .mg-component.is-full .mg-prism-canvas,
  .mg-component.is-full .mg-ballotmap { height: clamp(420px, 68vh, 760px); }
}

/*
 * STACKED, THE PANEL IS A WIDE BAND AND ITS BARS ARE NOT.
 *
 * Below 860 the legend goes full width under the map, and everything in it
 * that was drawn to a 200px column stretches: a proportion bar eight hundred
 * pixels long, and a scale whose two end figures sit a third of a screen
 * apart from the 130px ramp they label. Neither is more readable for the
 * room -- a bar that long is read as a timeline, and the ramp's "1,417" had
 * stopped pointing at anything.
 *
 * Capped rather than left-aligned at their natural width, so they still read
 * as measured things that start at the left margin of the column they are in.
 */
/*
 * ON THE PANEL'S OWN WIDTH, NOT THE VIEWPORT'S.
 *
 * This was @media (max-width: 860px), on the reasoning that the legend only
 * goes full width when the layout stacks and the layout only stacks below
 * 860. That is not the only way it happens -- the gallery stacks it at any
 * width, and so does any page that gives the block a single column -- and in
 * those cases the rule simply never fired while the bars went on filling a
 * fourteen-hundred-pixel band.
 *
 * A container query asks the question that actually matters: how wide is the
 * column these are sitting in. 400px is clear of every sidebar the layout
 * produces -- a fifth of the grid at 1280 is about 240, a quarter at 1024
 * about 250 -- and well under the full-width band, so the bars keep the whole
 * sidebar when they are in one and stop at half of it when they are not.
 */
/*
 * THE CONTAINER IS THE BODY, NOT THE ASIDE, and that is not a detail.
 *
 * In the gallery `.mg-map-pair .mg-map-aside:not(.is-below)` is
 * `display: contents` -- the box dissolves so its head and body become grid
 * items of the card. An element with `display: contents` generates no box at
 * all, so it cannot be a query container, and a container-type on the aside
 * is silently ignored exactly where the panel goes full width: a lone odd
 * card spans the whole row (see the :last-child:nth-child(odd) rule), which
 * is the case this was reported from.
 *
 * The body is a real box in both layouts -- it carries the card's background
 * and border in the gallery -- and everything measured here is inside it.
 */
.mg-map-aside-body { container-type: inline-size; container-name: mg-aside; }

@container mg-aside (min-width: 400px) {
  .mg-share-bar { max-width: 50%; }
  .mg-prism-key { max-width: 50%; }
  /* The channel's total is ranged right against the END of its own bar. Left
     at full width it sat four hundred pixels past it, which reads as a figure
     belonging to the panel rather than to the bar underneath it. */
  .mg-share-head { max-width: 50%; }
}

/*
 * THE PANEL HAS TO FIT A LAPTOP SCREEN, not merely scroll on one.
 *
 * At 900px tall the block gets 700 and the panel needs about 676 for its
 * two channel blocks, so "Columns" and its switch were cut in half at the
 * foot of a column that gave no sign of scrolling. A control a reader cannot
 * see is a control that is not there.
 *
 * Tightened here and not in the base rules: in a card among cards the aside
 * is as long as it is and the page scrolls, which is fine. It is only the
 * promise of one screen that makes the last 50px worth finding.
 *
 * The headings keep more space above than below -- 16 and 6, where the base
 * is 22 and 8. Even spacing would leave each one floating between the group
 * it names and the group above it.
 */
.mg-component.is-full .mg-map-aside { padding: 18px 10px; }
.mg-component.is-full .mg-map-aside .mg-tree-sub { margin: 16px 0 6px; }
.mg-component.is-full .mg-map-aside .mg-tree-sub:first-child { margin-top: 0; }
.mg-component.is-full .mg-share-head { margin: 12px 0 5px; }
.mg-component.is-full .mg-share { gap: 6px; }
.mg-component.is-full .mg-ballot-big b { font-size: 24px; }
