/* ============================================================
   map.css — Sticky scrollytelling map scene.
   ============================================================ */

.scene--map-pinned {
  display: block;
  padding: 0;
  position: relative;
}

/* ── The pinned frame is the slide, so it is the slide's box ──
   Both sticky frames here stick at the chrome's lower edge and are
   exactly the box below it — the same `100svh - var(--chrome-h)` every
   other scene is sized to, and the same offset js/snap.js lands a stop
   at. They used to be `top: 0; height: 100vh`, which agreed with the
   stop only once the sticky had actually engaged: the map's FIRST card
   is landed at the scene's own top, where the frame is still in flow at
   y = --chrome-h, so its 100vh ran --chrome-h past the fold and the
   legend pinned to its bottom was clipped. The next gesture engaged the
   sticky and the legend jumped up by the header's height — a visible
   shift between slide one and slide two, with nothing in the card to
   explain it.

   The panels' negative margin is the inverse of this height, not a
   separate number: it exists to lay the panel stack over the map, so
   if the two disagree the scene grows or shrinks by the difference. */
.map-sticky {
  position: sticky;
  top: var(--chrome-h);
  height: calc(100svh - var(--chrome-h));
  width: 100%;
  overflow: hidden;
  background: var(--bg);
}

.map-container {
  position: absolute;
  inset: 0;
}

.map-panels {
  position: relative;
  z-index: var(--z-map-scrim);
  pointer-events: none;
  /* Pull panels up so they scroll over the sticky map below. Exactly
     one .map-sticky tall, so the scene's own height stays the sum of
     its panels — see the note over .map-sticky. */
  margin-top: calc(var(--chrome-h) - 100svh);
}
/* Each panel is a scroll spacer taller than the viewport, so its callout
   "hangs" in place for a moment between entrance and exit. The inner
   frame is sticky and one slide tall — the rule-of-thirds anchors
   position against that, not against this.

   COUPLED TO js/render.js: PANEL_VH and TAIL_VH there compute the scene's
   min-height from the panel count. If these two disagree the larger wins
   and the other is silently ignored. 130vh was tuned for five panels; at
   seven that ran to 930vh of scrolling, so the dwell is tightened.

   ALSO COUPLED TO js/snap.js, which treats each `.map-panel` as one slide
   and snaps to its top — the offset where the sticky frame below is
   engaged and the card sits at its anchor. The extra height over 100vh is
   therefore no longer dwell a reader scrolls through on a desktop
   pointer; it is the travel of one slide transition, and the slideshow
   crosses it in a single gesture. It is still dwell everywhere the
   slideshow stands down: touch, and reduced motion. */
.map-panel {
  height: 112vh;
  position: relative;
  padding: 0;
}
/* Every panel the same height but the last, which is one plain
   viewport rather than 112vh.

   It is the end of the document: no scene follows the map and the site
   footer has moved into the card itself, so the page stops where this
   panel stops. One viewport is exactly the height that puts the card's
   stop ON the document's maximum scroll offset — a full 112vh would
   leave about a fifth of a screen below the last stop that only a
   native scroll could reach, which is the one thing the slideshow is
   not allowed to have.

   COUPLED TO sceneMinHeight() in render.js, which has to compute the
   same total or the larger of the two silently wins. */
.map-panel:last-child { height: 100vh; }
.map-panel-frame {
  position: sticky;
  top: var(--chrome-h);
  height: calc(100svh - var(--chrome-h));
  width: 100%;
}

/* ── Rule-of-thirds anchored callouts ──
   Cards are absolutely positioned to one of four thirds-corner anchors
   or to dead center within the sticky frame.

   CENTRE IS FOR THE CARDS THAT POINT AT NOTHING. A corner anchor exists
   to leave the map room — moveCamera() even charges the card's width to
   the padding on its side, so the cities a card rings land clear of it.
   A card with no highlighted cities has nothing to stand clear of, and
   the sequence has exactly two of those: the one that explains how to
   read the map and the one that sends you off it. They bookend the
   run, both centred; the five that ring cities take the corners.

   (This read "only one panel may use `is-pos-c`" while none did.) */
.map-panel .map-annotation { position: absolute; }
.map-panel.is-pos-tl .map-annotation { top: 14vh;    left: 14vw; }
.map-panel.is-pos-tr .map-annotation { top: 14vh;    right: 14vw; }
.map-panel.is-pos-bl .map-annotation { bottom: 14vh; left: 14vw; }
.map-panel.is-pos-br .map-annotation { bottom: 14vh; right: 14vw; }
.map-panel.is-pos-c  .map-annotation {
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
}

.map-annotation {
  pointer-events: auto;
  /* Was a hard 380px, which crammed a three-column stat row into ~110px
     per cell and wrapped city names. Fluid now: wider where there is
     room, never much more than a third of the viewport so the map it
     sits on stays the subject.

     560 rather than 460 because the copy is the article's body size
     now, not a caption's. At 20px in a 388px column the dek set about
     thirty characters to the line; this gives it thirty-eight. The
     scene dek gets 672px for the same type, so the card is still the
     narrower measure — it is a card — just no longer a slot. */
  width: min(560px, 36vw);
  max-width: 560px;
  padding: var(--space-6);
  background: var(--map-panel-grad);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.45);
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}
.map-annotation > * { margin: 0; }

/* ── THE CARDS ARE THE ARTICLE, NOT A LEGEND ───────────────
   Every rule in this block used to override the site's type downward:
   the headline was set below the h3 the rest of the page uses, the
   copy was 13px against the scene dek's 20, and the numbers were mono
   where the page sets figures in Lato. The map cards read as chrome
   floating over a map rather than as the piece continuing across it.

   So the overrides come off. What is left here is what a card
   genuinely needs that a full-width scene does not — a tighter gap on
   the lead, a truncating city name — and the type comes from base.css
   and the tokens like everywhere else.

   Nothing sets font-feature-settings, smoothing or text-rendering:
   `body` in base.css already does, and the cards have always inherited
   them. This is what "carry the styles over" mostly means — deleting
   the local ones so the inherited ones can land. */

/* No font-size, weight, tracking or leading: this is the site's own h3
   (base.css), which is clamp(18px, 2vw, 24px) at 900. The card used to
   pin it to 20px, which put the headline a hair above its own body
   copy once that copy became the dek's size. */

/* The card's body copy IS the scene dek — the same declaration, so
   there is one place to change it and no chance of the two drifting
   a step apart again. */
.map-annotation p {
  color: var(--muted);
  font-size: clamp(var(--text-lg), 1.35vw, var(--text-xl));
  line-height: 1.65;
}
.map-annotation p strong { color: var(--text); font-weight: 700; }

/* ── Lead stat: the single anchoring number that opens every card ── */
.annotation-lead {
  display: flex;
  flex-direction: column;
  gap: 4px;
}
/* The page's figure treatment, at a card's scale: Lato 900 with the
   tight tracking and tabular figures every other headline number on
   the site is set in (see `.display` and `.scene--big-number .figure`).
   It was JetBrains Mono at 700 — the face this design reserves for
   tabular DATA, rows and readouts — which made the one number the card
   leads with look like a table cell rather than a headline. */
.annotation-lead-value {
  font-family: var(--font-display);
  font-weight: 900;
  font-size: clamp(30px, 2.6vw, 40px);
  line-height: 1;
  color: var(--text);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.03em;
}
/* The label under it stays mono and stays small — it is a caption on a
   readout, which is exactly what this file's other mono labels are. */
.annotation-lead-label {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-weight: 500;
}

/* ── Personalization: locate button, stats list, CTA card ── */
.map-annotation.is-locked   { border-style: dashed; border-color: var(--gold-soft); }
.map-annotation.is-locked h3,
.map-annotation.is-locked p { opacity: 0.92; }

.annotation-cta {
  width: 100%;
  font-family: var(--font-body);
  font-weight: 700;
  letter-spacing: -0.005em;
}
.annotation-cta[disabled] { opacity: 0.7; cursor: progress; }
.annotation-cta-note {
  margin: 0;
  font-size: var(--text-xs);
  color: var(--muted);
  line-height: 1.4;
}
.annotation-cta-note.is-error { color: var(--avail-50); }

/* ── City picker (fallback to "Use my location") ──
   Hidden by default; revealed by the "Or pick a city"
   toggle, or auto-shown when geolocation is denied/unavailable.

   It is the HERO'S TYPEAHEAD, mounted in place by wireLocate() —
   not a second way to pick a city and not a native <select> of
   121 names to scroll. So there is no control styling here: all
   of it lives in nav-search.css, and this only fits that
   component to a card that is narrower than a hero. */
.annotation-picker-toggle {
  background: none;
  border: 0;
  padding: 0;
  margin: 0;
  font-family: var(--font-body);
  font-size: var(--text-xs);
  color: var(--muted);
  cursor: pointer;
  text-align: left;
  text-decoration: underline;
  text-decoration-color: var(--line-strong);
  text-underline-offset: 3px;
  transition: color var(--dur-fast) var(--ease-out);
}
.annotation-picker-toggle:hover,
.annotation-picker-toggle:focus-visible {
  color: var(--primary-bright);
  text-decoration-color: var(--primary-bright);
}
.annotation-picker-toggle[hidden] { display: none; }

.annotation-picker {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-top: 2px;
}
.annotation-picker[hidden] { display: none; }

/* The component sets its own rhythm for a hero; in here the card's
   gap owns the spacing and the card's width owns the width. */
.annotation-picker .nav-search,
.annotation-picker .nav-search.is-skeleton {
  margin: 0;
  max-width: none;
}
.annotation-picker .nav-search__input,
.annotation-picker .nav-search__skeleton-field {
  min-height: 44px;
  background: var(--map-glass-soft);
}
.annotation-picker .nav-search__listbox {
  background: var(--surface-2);
}

/* Inline "Not [State]?" button inside the personalized stats dek. */
.map-annotation .link-button {
  background: none;
  border: 0;
  padding: 0;
  margin: 0;
  font: inherit;
  color: var(--primary-bright);
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 2px;
}
.map-annotation .link-button:hover,
.map-annotation .link-button:focus-visible {
  color: var(--gold-bright);
}

.annotation-stats {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-3);
  margin: 0;
  padding: 0;
}
/* Four stats, so 2x2 rather than a row of three with one stranded
   below it. The count is written into the class by
   renderAnnotationStats(); five is left on the three-column default,
   where it fills a row of three and a row of two and reads as a
   block rather than a ladder. */
.annotation-stats--4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.annotation-stat { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.annotation-stat dt {
  font-size: var(--text-sm);
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
.annotation-stat dd {
  margin: 0;
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: var(--text-xl);
  color: var(--text);
  line-height: 1.1;
  display: flex;
  align-items: baseline;
  gap: 4px;
}
.annotation-stat dd .unit {
  font-size: var(--text-md);
  font-weight: 500;
  color: var(--muted);
}

/* GOLD-ON-GOLD NEEDS ITS OWN INK ON LIGHT, and this card is where the
   theme's one sub-AA accent stopped being decorative.

   tokens.css keeps --gold / --gold-bright / --gold-text at the brand
   value on both themes, knowing the light theme buys that identity at
   1.56:1 against the page. That trade was costed for punctuation and
   for marks the eye finds by colour. Here the same ink was painting
   the eyebrow — real words — on top of a PALE GOLD WASH, which is the
   worst ground the value can land on: --gold-bright measured 1.22:1
   against the gradient's dark end, so "YOUR CITY, IN DEPTH" was very
   nearly invisible, and the arrow that signals where the card goes
   measured the same.

   tokens.css says what to do about it in as many words — give the
   field its own accent rather than splitting the shared token
   quietly — so that is what this is. Scoped to this card, dark on
   the gradient at 5.24:1, and unused on the dark theme, where the
   same card already measures 7.61:1 and the brand value stands. */
.map-cta-card {
  --cta-accent: var(--gold-bright);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-4) var(--space-5);
  background: linear-gradient(135deg, rgba(227, 167, 47, 0.22), rgba(245, 196, 94, 0.08));
  border: 1px solid var(--gold);
  border-radius: var(--radius-md);
  color: var(--text);
  text-decoration: none;
  transition: transform var(--dur-fast) var(--ease-out),
              box-shadow var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out);
}
.map-cta-card:hover,
.map-cta-card:focus-visible {
  transform: translateY(-1px);
  border-color: var(--gold-bright);
  box-shadow: 0 12px 32px rgba(227, 167, 47, 0.18);
}
/* The lift is the decorative third of a hover that already says itself
   twice, in border-color and box-shadow — so under reduced motion it
   goes rather than being made instant. Same judgement as .ci-highlight
   in rankings-page.css. */
@media (prefers-reduced-motion: reduce) {
  .map-cta-card {
    transition: box-shadow var(--dur-fast) var(--ease-out),
                border-color var(--dur-fast) var(--ease-out);
  }
  .map-cta-card:hover,
  .map-cta-card:focus-visible { transform: none; }
}
.map-cta-card-body { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.map-cta-card-eyebrow {
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--cta-accent);
  font-weight: 700;
}
.map-cta-card-label {
  font-family: var(--font-body);
  font-weight: 700;
  font-size: var(--text-lg);
  color: var(--text);
  letter-spacing: -0.005em;
}
.map-cta-card-arrow {
  font-family: var(--font-mono);
  font-size: var(--text-xl);
  color: var(--cta-accent);
  flex-shrink: 0;
}
/* The card's ground is a gold wash on both themes, so the accent is
   pinned to the theme rather than to the surface underneath it. */
:root[data-theme='light'] .map-cta-card { --cta-accent: var(--gold-on-wash); }
.map-cta-card.is-locked {
  pointer-events: none;
  border-style: dashed;
  border-color: var(--line-strong);
  /* --map-glass-strong, like every other overlay here that carries
     words. See the note on .map-legend below. */
  background: var(--map-glass-strong);
}
.map-cta-card.is-locked .map-cta-card-arrow { color: var(--muted); }
.map-cta-card.is-locked .map-cta-card-eyebrow { color: var(--muted); }

.map-legend {
  position: absolute;
  /* Lift clear of the Mapbox attribution row pinned to the bottom-left. */
  bottom: calc(var(--space-4) + 30px);
  left: var(--space-4);
  z-index: var(--z-map-overlay);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-4);
  /* GLASS OVER A MAP IS A VARIABLE BACKGROUND, and this panel is the one
     place on the site where that was left unmeasured — "the effective
     background is variable" is exactly what the accessibility note in
     .impeccable.md said about it.

     It is measurable as a bound. The panel composites its own colour
     over whatever tile is beneath, so the worst case for a light ink is
     the lightest possible tile. At --map-glass (0.72 dark / 0.82 light)
     the effective background bottoms out at rgb(79,85,96) on dark, and
     --muted lands at 2.40:1 there — a clear AA failure that passes at
     6.19:1 over a dark tile, which is why nobody caught it: the legend
     was legible most of the time and illegible over water, motorway
     casings and pale landuse.

     --map-glass-strong (0.92 / 0.95) is already the token every
     text-bearing map overlay uses — the tooltips have used it all
     along. Worst case it holds --muted at 4.86:1 on dark and 5.12:1 on
     light, and the title ink at 13.3:1 / 15.6:1. The panel is still
     translucent and still blurred; it simply no longer bets legibility
     on which tile is underneath.

     The rule this settles: an overlay that carries words gets the
     strong glass. The soft glass is for decoration. */
  background: var(--map-glass-strong);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  backdrop-filter: blur(8px);
  font-size: var(--text-xs);
  color: var(--muted);
}
/* The metric's name, then what it means. The gloss is deliberately
   lower-contrast and set tighter to the title than the legend's own
   gap — it is a subtitle to that line, not a third item in the
   stack. */
.map-legend__title { color: var(--text); }
.map-legend__gloss {
  max-width: 220px;
  margin-top: calc(var(--space-2) * -0.5);
  color: var(--muted);
  line-height: 1.35;
  text-wrap: balance;
}
.map-legend .scale {
  width: 220px;
  height: 8px;
  border-radius: var(--radius-pill);
  background: linear-gradient(90deg, var(--avail-100), var(--avail-50), var(--avail-25));
}
.map-legend .labels { display: flex; justify-content: space-between; font-family: var(--font-mono); }

/* ── The downtown-area key ─────────────────────────────────
   Only one thing on this map is not a colour on the ramp: the dashed
   gold ring drawn around a located city's downtown area. The ramp's
   own gradient explains every other mark, so the ring was the single
   unglossed symbol on screen — asked about in the 1.1 review.

   It is hidden until there is a ring to explain. A legend entry for a
   mark that is not drawn names nothing, which is the same rule the
   hero's key already follows for its spiral row.

   THE SWATCH IS THE MARK'S OWN GEOMETRY, so the two cannot drift into
   describing different things — and since the mark gained a filled
   field (see addDowntownAreaLayers() in render.js) the swatch is a
   gold-filled box rather than the bare gold rule it was. That rule
   had the same problem the ring did: brand gold on this legend's
   near-white glass is about 1.7:1, so the one symbol on the map that
   needed glossing was itself the palest thing in the gloss. The fill
   is what fixes it, on the swatch for the same reason it does on the
   map. */
.map-legend__key {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-2);
  color: var(--text-soft);
}
.map-legend__key[hidden] { display: none; }
.map-legend__swatch {
  /* One token, published per theme — the same name downtownAreaGold()
     hands the map, so the swatch and the ring cannot show two
     different golds. */
  --swatch-gold: var(--area-gold);
  position: relative;
  flex: 0 0 auto;
  width: 22px;
  height: 14px;
  /* THE SAME THREE-PART MARK THE MAP DRAWS, in the same order: a
     continuous casing, the gold dash over it, a field inside. The
     casing is this element's own border and the dash is the pseudo-
     element's, laid exactly over it by inset: -2px, so the two are
     the same ring at the same width and the dark shows only in the
     gaps. Doing it the other way — one dashed border over a
     background layer clipped to the border box — tints the field
     with the casing as well, because the field is translucent over
     map glass and cannot be mixed to an opaque value. */
  border: 2px solid color-mix(in srgb, var(--area-casing) 55%, transparent);
  /* The field. Stronger than the 0.24 the map fills the disc with, for
     the reason the stripe below is: 22x14 of near-white glass cannot
     show an alpha tuned to read across a few hundred pixels of map.
     Clipped to the padding box so it stays inside the casing. */
  background: color-mix(in srgb, var(--swatch-gold) 40%, transparent) padding-box;
}
.map-legend__swatch::after {
  content: '';
  position: absolute;
  inset: -2px;
  border: 2px dashed var(--swatch-gold);
}
/* No light override: --area-gold is already published per theme. */
/* The coverage area's key. Its mark on the map is a solid blue outline
   over a STRIPED blue ground, so the swatch is a striped box — the
   same distinction the eye has to make on screen, where one mark is a
   dashed gold ring over a flat field and the other is a solid blue
   outline over a striped one. Both halves are the same token the
   layer paints with.

   The stripe is scaled and strengthened for the swatch, the way the
   dash on the ring's key already is: 20px of glass cannot show a 11px
   band at 6% and 13% and still read as striped, so the bands are 3px
   and the two weights are pulled apart. The map wants a texture a
   reader can find under a choropleth; the swatch wants one a reader
   can see at all. */
.map-legend__swatch--premium {
  width: 20px;
  height: 12px;
  /* 1.5px solid at 0.75 is premium-area-line's own paint, read off the
     layer rather than approximated: line-width 1.5, line-opacity 0.75,
     line-color --primary. */
  border: 1.5px solid color-mix(in srgb, var(--primary) 75%, transparent);
  background:
    repeating-linear-gradient(-45deg,
      color-mix(in srgb, var(--primary) 14%, transparent) 0 3px,
      color-mix(in srgb, var(--primary) 32%, transparent) 3px 6px),
    var(--primary-soft);
}
/* No dash on this one. The base rule's ::after is the downtown ring's
   gold dash, and the coverage area has no such mark — premium-area-line
   is solid. Inheriting it put a dark-gold dashed ring around the striped
   box and made the swatch claim an edge treatment the polygon does not
   draw, which is the one thing a legend key may never do. */
.map-legend__swatch--premium::after {
  content: none;
}

/* Hover tooltip — floats with the cursor over the choropleth. */
.map-tooltip {
  position: absolute;
  z-index: var(--z-map-control);
  pointer-events: none;
  min-width: 168px;
  padding: var(--space-3) var(--space-4);
  background: var(--map-glass-strong);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-md);
  box-shadow: 0 18px 40px rgba(0, 0, 0, 0.45);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  color: var(--text);
  font-family: var(--font-body);
  font-size: var(--text-sm);
  transition: transform var(--dur-fast) var(--ease-out);
}
.map-tooltip-name {
  font-weight: 700;
  font-size: var(--text-md);
  letter-spacing: -0.01em;
  margin-bottom: var(--space-2);
}
.map-tooltip-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-bottom: 2px;
}
.map-tooltip-val {
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: var(--text-lg);
  color: var(--text);
}
.map-tooltip-label {
  font-size: var(--text-xs);
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
.map-tooltip-sub {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  margin-top: var(--space-2);
  padding-top: var(--space-2);
  border-top: 1px solid var(--line);
}
.map-tooltip-count {
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: var(--text-md);
  color: var(--text);
}
.map-tooltip-sublabel {
  font-size: var(--text-xs);
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

/* ── The cursor says what you can do with this map ─────────
   Mapbox paints `grab` over any map it considers interactive, and by
   its own test this one is: it is constructed with `interactive: true`
   so the city dots can answer a hover. But every gesture that cursor
   promises is switched off — dragPan, scrollZoom, boxZoom, rotate,
   touch (see the Map options in render.js) — because the scroll
   position is what drives the camera here. An open hand over a map
   that cannot be dragged is an offer the page does not honour, and on
   a page where scrolling means something specific it also invites the
   one gesture that would fight it.

   So: the ordinary arrow over the map, and a pointer over the city
   points, which DO respond — setupHover() sets that inline on the
   canvas, and an inline style outmatches this. On mouseleave it
   restores the empty string, which lands back here rather than on
   Mapbox's `grab`, because `cursor` inherits and the canvas takes it
   from this container.

   Scoped to `.map-container`, which is this scrollytelling map alone.
   The city page's map is a real map — you can drag it — and Mapbox is
   right about that one. */
.map-container .mapboxgl-canvas-container.mapboxgl-interactive,
.map-container .mapboxgl-canvas-container.mapboxgl-interactive:active {
  cursor: default;
}

/* ── Mapbox controls ────────────────────────────────────
   The zoom pair is the same kind of object as the masthead's
   light/dark toggle: a small bordered control carrying an icon, sat
   on a surface that changes with the theme. So it is built the same
   way — a hairline border, a pill, a transparent button that tints on
   hover, the site's own duration and easing — and it does the one
   thing the toggle exists to prove a control can do, which is follow
   the mode.

   34px rather than the toggle's 44: this one sits on the reader's
   content, and the toggle has a bar to itself. */
.mapboxgl-ctrl-bottom-right { display: none; }
.mapboxgl-ctrl-bottom-left  { opacity: 0.45; }
.mapboxgl-ctrl-bottom-left:hover { opacity: 1; }
.mapboxgl-ctrl-group {
  background: var(--map-glass-strong) !important;
  border: 1px solid var(--line-strong) !important;
  /* Mapbox ships a 4px radius and a 2px black glow. Both are its
     chrome; the pill and the hairline are ours. */
  border-radius: var(--radius-pill) !important;
  box-shadow: none !important;
  backdrop-filter: blur(8px);
}
.mapboxgl-ctrl-group button {
  width: 34px;
  height: 34px;
  background-color: transparent !important;
  transition: background-color var(--dur-fast) var(--ease-out);
}
/* The end buttons carry the capsule's curve themselves rather than the
   group clipping them to it: `overflow: hidden` on the group would
   also crop the focus ring, which base.css draws 3px OUTSIDE the
   button. A keyboard ring that is half a rectangle is worse than a
   square corner. */
.mapboxgl-ctrl-group button:first-child {
  border-radius: var(--radius-pill) var(--radius-pill) 0 0 !important;
}
.mapboxgl-ctrl-group button:last-child {
  border-radius: 0 0 var(--radius-pill) var(--radius-pill) !important;
}
.mapboxgl-ctrl-group button:not(:disabled):hover {
  background-color: var(--hover-tint) !important;
}
.mapboxgl-ctrl-group button + button {
  border-top: 1px solid var(--line) !important;
}
/* THE LINE THAT HAS TO FOLLOW THE THEME. Mapbox's + and − ship as
   near-black SVG, so dark mode inverts them and light mode must not.
   state-page.css used to re-declare this filter with the dark value
   hard-coded and at a higher specificity, which painted white glyphs
   on the white light-mode control: two blank buttons. The token
   resolves per theme in tokens.css and is the only place this is
   decided. */
.mapboxgl-ctrl-group button .mapboxgl-ctrl-icon { filter: var(--map-ctrl-icon-filter); }

/* ============================================================
   In-annotation rank list. Compact rank / city / availability row.

   It was written for a card and then used only by a full-width
   leaderboard scene, which is gone; the regional cards use it now, as
   the heading always claimed. One row per city the card has ringed on
   the map, so a reader can put a name to every ring.
   ============================================================ */
.rank-list-mini {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.rank-card-mini {
  display: grid;
  grid-template-columns: 24px 1fr auto;
  align-items: center;
  gap: var(--space-3);
  padding: 4px var(--space-2);
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--line);
  border-radius: 0;
}
.rank-card-mini:last-child { border-bottom: 0; }
.rank-card-mini .rank {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--muted);
  font-weight: 700;
}
.rank-card-mini .name {
  font-weight: 600;
  font-size: var(--text-md);
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.rank-card-mini .metric {
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: var(--text-md);
  color: var(--text);
}
.rank-card-mini .metric .unit {
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--muted);
  margin-left: 2px;
}

.annotation-chart .chart {
  width: 100%;
  height: 140px;
}

/* ── The four ways out, on the closing card ───────────────
   The sign-off scene that used to carry these is gone, so the last
   map card ends the article. Its buttons drop the `btn--xl` size that
   scene used — at 16px/26px padding four of them will not sit two-up
   inside a 460px card — and wrap two to a row at the base size. */
.annotation-actions {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-3);
}
/* No font-size: the site's own .btn size (--text-md). Only the padding
   is trimmed, and only because four of these have to sit two-up inside
   a card rather than across a full-width scene. */
.annotation-actions .btn {
  justify-content: center;
  padding: 10px 16px;
  text-align: center;
}
/* The report CTA takes the whole first row. Five buttons in a
   two-column grid otherwise leave one orphaned on the last row, and
   the lead action sat exactly as wide as the four routes it is there
   to outrank. Keyed to the variant, not to a position, so removing the
   CTA drops the card straight back to its 2×2. */
.annotation-actions .btn--primary { grid-column: 1 / -1; }

/* ── The credits, in the foot of the closing card ──────────
   `.psc-section-footer` is the page's own footer element, relocated here by
   rebuildMapPanels — so these rules undo the band it is styled as
   (full-width row, its own rule above it, generous padding) and let
   this wrapper carry the hairline and the spacing instead. One row,
   pushed to both ends the way the page-wide footer is: the edition on
   the left, the source credit hard against the right of the card, so
   the two read as a foot rule rather than as one run-on line. It
   wraps back to two lines when the card is too narrow to hold them. */
.annotation-foot {
  margin-top: var(--space-2);
  padding-top: var(--space-4);
  border-top: 1px solid var(--line);
}
/* rebuildMapPanels writes the slot before it knows whether there is a
   footer to relocate into it, and on the panels where there is not,
   what is left is a hairline and 28px of padding under nothing — a
   rule drawn across the bottom of a card for no reason. Cheaper to
   let an empty element remove itself than to make every caller
   remember not to emit one. Adopted from the WordPress integration
   sheet, which had been patching it from outside since the bundle was
   first vendored; it was never a host's problem to fix. */
.annotation-foot:empty {
  display: none;
}
/* No font-size or colour: `.psc-section-footer` in base.css already sets both
   (--text-sm, --muted) and this is that element. What is undone here is
   only the BAND — the full-width row, its rule, its padding. */
.annotation-foot .psc-section-footer {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 2px var(--space-3);
  padding: 0;
  border-top: 0;
}
.annotation-foot .psc-section-footer .meta { gap: var(--space-3); }

/* ============================================================
   Mobile reflow for the pinned scrollytelling map scene.
   On narrow viewports the rule-of-thirds anchors (14vw inset) push
   380px-wide cards off-screen. Pin annotations to the bottom of the
   sticky frame as a single full-width sheet — the same affordance
   the NYT Graphics team uses for mobile scrollytelling maps.
   ============================================================ */
@media (max-width: 720px) {
  /* ── The foot of the frame, shared by the card and the legend ──
     On a phone both overlays are full-width sheets, so they cannot sit
     in different corners: they stack. The legend goes on the floor and
     the card stands on top of it, which is the reading order — the
     colours are the picture, the key is the caption under it.

     --map-legend-h is measured and published by reserveLegendBand() in
     js/render.js, because the legend's height is not a constant: the
     gloss wraps to a different number of lines with the viewport, and
     the "Downtown area" key appears only once a reader has named a
     city. The fallback is the two-line-gloss height, so a browser with
     no ResizeObserver still stacks rather than overlaps.

     The 30px is the Mapbox attribution row pinned to the bottom-left,
     which neither sheet may cover. */
  .scene--map-pinned {
    --map-foot-inset: max(var(--space-3), env(safe-area-inset-bottom, 0px));
    /* The floor every full-width overlay stands on: clear of the
       Mapbox attribution row, which nothing may cover. */
    --map-floor: calc(30px + var(--map-foot-inset));
    --map-legend-band: calc(var(--map-legend-h, 132px) + var(--map-floor) + var(--space-3));
  }

  .map-panel {
    /* Shorter dwell on mobile — each callout still hangs for ~0.15 extra
       scroll-screens but the user reaches the next scene faster. Mobile
       is the one place this height is still dwell: the slideshow stands
       down for touch, so these panels are scrolled through. */
    height: 115vh;
  }

  .map-panel.is-pos-tl .map-annotation,
  .map-panel.is-pos-tr .map-annotation,
  .map-panel.is-pos-bl .map-annotation,
  .map-panel.is-pos-br .map-annotation,
  .map-panel.is-pos-c  .map-annotation {
    top: auto;
    left: var(--space-3);
    right: var(--space-3);
    bottom: var(--map-floor);
    transform: none;
    width: auto;
    max-width: none;
  }

  /* ── The first slide is the only one with a legend under it ──
     So it is the only one that stands on the band, and the only one
     that is not pinned to the floor: top and bottom are both set and
     the block margins are auto, which centres the card in the room
     left between the chrome and the legend. The card is what the
     reader is reading and the legend is what the card is read
     against; a centred card sits between the map above it and the key
     below it rather than crowding one of them.

     `height: fit-content` is what stops an over-constrained absolute
     box from stretching to fill the pair — without it, top AND bottom
     resolve by growing the card to a sheet of whitespace. */
  .map-panel[data-panel-index='0'] .map-annotation {
    top: var(--space-4);
    bottom: var(--map-legend-band);
    height: fit-content;
    margin-block: auto;
    max-height: calc(100% - var(--map-legend-band) - var(--space-4));
  }

  .map-annotation {
    padding: var(--space-4);
    gap: var(--space-3);
    /* The room left between the card's foot and the top of the frame,
       less a strip of map above it. Was a flat 62vh, which did not know
       the legend had moved out from over the map. */
    max-height: calc(100% - var(--map-floor) - var(--space-6));
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
  }
  .map-annotation h3 { font-size: var(--text-lg); }
  .map-annotation p  { font-size: var(--text-sm); line-height: 1.55; }

  /* Lead stat shrinks slightly so the card doesn't dominate the viewport. */
  .annotation-lead-value { font-size: 26px; }

  /* Stats grid: 3 columns gets cramped under 320px — wrap to 2 columns. */
  .annotation-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-2); }
  .annotation-stat dd { font-size: var(--text-lg); }

  /* ── The legend belongs to the first slide, and only to it ──
     On a phone it is a full-width sheet on the floor of the frame, so
     it is not furniture in a corner the way it is on a desktop: it is
     a third of the screen standing under every card for the whole
     scroll. The ramp is explained once, on the slide that introduces
     the dots, and then it gets out of the way — the cards that follow
     print their own figures beside their own cities, so the colour is
     never the only thing a reader has.

     Toggled by applyPanel() in js/render.js, which owns which slide is
     on screen. Hidden with visibility rather than display so the
     ResizeObserver behind --map-legend-h keeps measuring a box that is
     still laid out; display: none would publish 0 and collapse the
     band the first card is standing on. */
  .map-legend {
    top: auto;
    bottom: var(--map-floor);
    left: var(--space-3);
    right: var(--space-3);
    padding: var(--space-2) var(--space-3);
    visibility: hidden;
    opacity: 0;
    transition: opacity var(--dur-fast) var(--ease-out),
                visibility 0s linear var(--dur-fast);
  }
  .scene--map-pinned.is-first-panel .map-legend {
    visibility: visible;
    opacity: 1;
    transition: opacity var(--dur-fast) var(--ease-out);
  }
  .map-legend .scale { width: 100%; }
}
