/* The light-pollution map's own stylesheet — loaded only by
   /light-pollution-map/ (the how-it-works page stays on site.css alone, as a
   normal reference article). This file does not start a second design
   system: it reuses site.css's tokens by name — --ink, --ink-soft, --paper,
   --card, --line, --accent, --star, --void, --radius, --ui, --data, plus the
   glass/elevation/control tokens site.css's :root added for this feature
   (--glass, --glass-strong, --glass-blur, --hairline, --lift-1/2/3, --hit,
   --rail-gap, --panel-w). Nothing below restates one of those.

   Design spec: docs/superpowers/specs/2026-09-07-light-pollution-map-ui.md
   Mobile is the base. Two breakpoints only, both in rem, both min-width:
   48rem (sheet gains a third rail button) and 64rem (sheet becomes a docked
   panel). There is no max-width query in this file. */

:root {
  /* THE TYPE SCALE (spec §3). Six steps and one floor, local to the map on
     purpose — the article pages' 25 sizes are drift the spec explicitly
     declines to fix here (§9), so this scale is not added to site.css where
     it would look like an invitation to use it there too.

     Size AND line-height live in one token (`size/line-height`, the same
     slash form site.css already uses in its own `font:` shorthands), so a
     consumer that only ever writes `font-size` can't silently drop the
     ratio — every use below is `font: [weight] var(--t-x) var(--family);`,
     never `font-size` alone. Weight is spec's default for the step unless a
     component section says otherwise (card titles are the one documented
     exception — see .card-title). */
  --t-hero: 2.4rem/1.0;   /* the SQM number, and nothing else — 600, --data */
  --t-lg: 1.15rem/1.3;    /* sheet title, page headings — 600, --ui (unused: no such element ships yet) */
  --t-md: 1rem/1.5;       /* body inside cards, list values — 400, --ui */
  --t-sm: 0.875rem/1.45;  /* labels, secondary values — 400, --ui */
  --t-xs: 0.75rem/1.4;    /* chips, legend ticks, axis labels — 500, --ui — the floor itself */

  /* MOTION (spec §6). Named so prefers-reduced-motion below is one small
     override instead of a find-and-replace through the file. */
  --lp-move: 180ms cubic-bezier(.2, .8, .2, 1); /* panel/sheet transitions */
  --lp-fade: 120ms linear;                       /* card content fade-in */

  /* THE RAMP, AS A GRADIENT, EXISTS IN EXACTLY TWO PLACES BELOW. The fifteen
     zone colours live in src/engine/lp-scale.js as RAMP and are not restated
     in CSS as a rule (spec §2.3/§8) — this is the one sanctioned exception,
     for the two surfaces the spec itself names as showing the ramp: the
     in-panel position bar (§5.1, "the only place the fifteen zone colours
     appear inside the panel") and the darkness-filter slider track (§5.5,
     "carries the ramp itself as its background, so you are dragging along
     the legend"). Stops are the 15 zones spaced evenly; six-digit hex only —
     #424 silently becomes purple where zone 1b's grey belongs. */
  --lp-ramp: linear-gradient(
    to right,
    #000000 0%, #222222 7.14%, #424242 14.29%, #142f72 21.43%,
    #2154d8 28.57%, #0f5714 35.71%, #1fa12a 42.86%, #6e641e 50%,
    #b8a625 57.14%, #bf641e 64.29%, #fd9650 71.43%, #fb5a49 78.57%,
    #fb998a 85.71%, #a0a0a0 92.86%, #f2f2f2 100%
  );
}

@media (prefers-reduced-motion: reduce) {
  :root {
    /* §6: "every transition becomes an instant swap except opacity" — so
       fades keep a gentle timing and movement collapses toward zero. §4.2
       carves out one explicit exception on top of that: the sheet's spring
       becomes "a 120ms ease" rather than vanishing outright, because a
       height jump with truly no transition reads as a layout bug, not as
       respect for the setting. Both rules are honoured below rather than
       one overriding the other silently. */
    --lp-move: 120ms ease;
    --lp-fade: 120ms linear;
  }
  .lpmap-rail, .lpmap-legend, .lpmap-panel-dismiss .icon, .lpmap-skeleton,
  .lpmap-status-bar {
    animation: none;
    transition: none;
  }
  /* With the sweep gone the bar would be a stray dash; the sentence is the
     message and stands on its own. */
  .lpmap-status-bar { display: none; }
}

/* --- the app shell -------------------------------------------------------- */

/* NO .wrap, NO .site-header. The map is the page (spec §1); this is the one
   route in the site that opts out of the shared chrome entirely. */
.lpmap-app {
  position: fixed;
  inset: 0;
  overflow: hidden;
  background: var(--void);
  font-family: var(--ui);
  overscroll-behavior: none;
}
/* iOS rubber-bands the whole document behind a fixed full-viewport app
   unless the document itself is told there is nothing to scroll. */
body:has(.lpmap-app) { overflow: hidden; overscroll-behavior: none; }

/* THE MAP IS A STACKING CONTEXT, AND THAT ONE DECLARATION IS THE WHOLE UI.
   Leaflet numbers its own panes from 200 (`.leaflet-tile-pane`) to 700, in the
   library's stylesheet, and `.leaflet-container` is left at `z-index: auto` —
   so it creates NO stacking context and every pane competes directly against
   whatever else is on the page. The panel (30), the rail (40) and the legend
   (20) below are all positioned siblings of this element in the same context,
   so 200 and 400 beat every one of them: the overlay canvas painted over the
   panel's glass, the marker SVG painted over the coordinates, and the visitor
   read 12px text at 3.08:1 against the map instead of 9.4:1 against the glass.

   `z-index: 0` (rather than `isolation: isolate`, which would do the same
   containment but leave this box's own level implicit) puts every pane inside
   this element's context and puts the element itself at the bottom of the
   positioned stack, which is where a base layer belongs. Nothing else in this
   file changes number.

   WHY IT SURVIVED REVIEW AND CI. Hit-testing was never affected — the panes
   above the panel do not swallow its clicks — so every check that reads the
   DOM, dispatches an event or asserts a computed `z-index` passed while the
   product was illegible. The guard for it therefore samples RENDERED PIXELS
   (`test/browser-light-pollution-map.mjs`, the "chrome over map" scenario):
   an empty patch of panel background must be the panel's near-opaque glass
   and cannot be the map's own colour. An assertion over `getComputedStyle`
   would have passed all along and is not evidence. */
.lpmap-map {
  position: absolute;
  z-index: 0;
  inset: 0;
  background: var(--void); /* shows before tiles paint, never a flash of white */
}

/* `.lpmap-brand` used to be here: a pill standing in for the header bar this
   page was originally designed without. The canonical-frame contract gave every
   route a real site-header, so no markup has referenced it since — the file's
   own note at the 64rem block already said "which no route ships". Deleted
   rather than left as a rule nobody can see the effect of. */

/* --- the location panel / bottom sheet (spec §4) --------------------------
   One element in the markup, two shapes here: a bottom sheet with three
   detents by default (mobile is the base), redrawn as a docked left panel
   at 64rem. Contract with Task 13: it sets `data-detent="peek|half|full"`
   on .lpmap-panel; with no attribute the panel sits at "half" so a no-JS or
   pre-hydration load shows more than the bare minimum rather than less. */
.lpmap-panel {
  position: fixed;
  z-index: 30;
  inset-inline: 0;
  bottom: 0;
  display: flex;
  flex-direction: column;
  block-size: 55vh; /* fallback = "half", see contract note above */
  max-block-size: 92vh;
  background: var(--glass-strong);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border-top: 1px solid var(--hairline);
  border-radius: 1rem 1rem 0 0;
  box-shadow: var(--lift-3);
  transition: block-size var(--lp-move);
}
.lpmap-panel[data-detent="peek"] { block-size: 7rem; }
.lpmap-panel[data-detent="half"] { block-size: 55vh; }
.lpmap-panel[data-detent="full"] { block-size: 92vh; }

/* Dismissal ([hidden] on the body, toggled by lpmap-panel-dismiss) shrinks
   the sheet to just its own header row rather than to zero — the sheet is
   never modal and never fully leaves the screen (spec §4.2). */
.lpmap-panel:has(> #lpmap-panel-body[hidden]) { block-size: 3rem; }

/* THE GRABBER IS A CONTROL, AND IT USED TO BE 40 x 5 PIXELS OF ONE.
   It was a decorative `aria-hidden` div, and the sheet responded ONLY inside
   the visible pill: QA swept the touch-down point and found a live area of
   x 175-215 by about five pixels -- roughly 200 px2 against the 1936 px2 of a
   44 x 44 target. Three pixels lower did nothing, the panel body did nothing,
   and the rest of the sheet's top edge did nothing. It was also the ONLY way
   to move the sheet below 64rem, where `.lpmap-panel-dismiss` is `display:
   none`, and being `aria-hidden` with no role and no tabindex it had no
   keyboard route at all -- so a keyboard or switch-control user on any
   viewport under 1024px was stuck with a 254px map (390x844) or a 162px one
   (360x640) and no control to change it.
   It is a real `<button>` now (see the markup), and this makes the whole
   44px band grabbable while the pill it draws stays exactly the size it was.
   The top padding the body used to carry moves here, so the panel's own
   header costs about 18px more than before rather than 28. */
.lpmap-panel-handle {
  flex: none;
  align-self: center;
  display: flex;
  align-items: center;
  justify-content: center;
  inline-size: 6rem;
  block-size: var(--hit);
  padding: 0;
  border: 0;
  appearance: none;
  background: none;
  color: inherit;
  cursor: grab;
  touch-action: none; /* the handle owns vertical drag, the body owns scroll */
}
.lpmap-panel-handle::before {
  content: "";
  display: block;
  inline-size: 2.5rem;
  block-size: 0.3rem;
  border-radius: 999px;
  background: var(--hairline);
}
.lpmap-panel-handle:focus-visible { outline: 2px solid var(--star); outline-offset: -4px; border-radius: var(--radius); }
.lpmap-panel-handle:active { cursor: grabbing; }

/* Dismissible is a desktop idea (spec §4.1 places the sentence under the
   docked panel, not under the mobile sheet in §4.2); on the sheet, the
   grabber's drag already moves the panel out of the way, and reserving a
   44px button's clearance at the very top of a 7rem peek would eat most of
   the headline it exists to protect. Hidden below 64rem for that reason. */
.lpmap-panel-dismiss {
  display: none;
  position: absolute;
  z-index: 1;
  top: 0.6rem;
  right: 0.6rem;
  inline-size: var(--hit);
  block-size: var(--hit);
  align-items: center;
  justify-content: center;
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  background: var(--glass);
  color: var(--ink);
  cursor: pointer;
}
.lpmap-panel-dismiss:focus-visible { outline: 2px solid var(--star); outline-offset: 2px; }
.lpmap-panel-dismiss .icon { inline-size: 1.25rem; block-size: 1.25rem; transition: transform var(--lp-fade); }
/* Chevron points at what tapping it will do: down while shown, up once hidden. */
.lpmap-panel-dismiss[aria-expanded="false"] .icon { transform: rotate(180deg); }

.lpmap-panel-body {
  flex: 1 1 auto;
  min-block-size: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  display: flex;
  flex-direction: column;
  gap: 0.6rem; /* spec §5.3: ".6rem gap between cards" */
  /* No top breathing room any more: the grabber above is a full 44px control
     and carries it. */
  padding: 0 1rem 1.25rem;
}

.lpmap-place {
  margin: 0.35rem 0 0;
  color: var(--ink-soft);
  font: var(--t-sm) var(--ui);
}

/* "THERE IS MORE BELOW", WHICH NOTHING ON THIS PANEL USED TO SAY. The body is
   `overflow-y: auto` and does scroll -- 635px of box over 2394px of content at
   1440x900 -- but with no scrollbar, no fade and no gradient every viewport
   showed a card sliced through the middle of a line of text against a hard
   edge. On a 360x640 phone the cut lands just under card 1's chips, so "What
   you will see", "Your horizon", "Tonight", the sky dome, the exposure table
   and the required attribution were all below a fold with nothing suggesting
   they existed. `lpmap-sheet.js` sets `data-scroll` on the panel; this is what
   it looks like. Pointer-events off, so the fade is never a target. */
.lpmap-panel::after {
  content: "";
  position: absolute;
  inset-inline: 0;
  bottom: 0;
  block-size: 1.5rem;
  border-radius: 0 0 var(--radius) var(--radius);
  background: linear-gradient(to top, var(--void), rgba(5, 7, 10, 0));
  opacity: 0;
  transition: opacity var(--lp-fade);
  pointer-events: none;
}
.lpmap-panel[data-scroll="more"]::after { opacity: 0.9; }

@media (min-width: 64rem) {
  .lpmap-panel {
    inset-inline: auto;
    left: 1rem;
    top: 4.5rem; /* clears the brand pill: 1rem + its --hit + a gap */
    bottom: 1rem;
    inline-size: var(--panel-w);
    block-size: auto;
    max-block-size: none;
    border-radius: var(--radius);
    border: 1px solid var(--hairline);
    /* --glass-panel, not --glass — see its definition in site.css for the
       measured contrast that set 0.86. */
    background: var(--glass-panel);
  }
  /* Detents are a sheet-only idea; the docked panel just is or is not shown. */
  .lpmap-panel[data-detent] { block-size: auto; }
  .lpmap-panel-handle { display: none; }
  .lpmap-panel-dismiss { display: flex; }
  /* Clears the dismiss button now reserved above it: top 0.6rem + --hit
     2.75rem = 3.35rem, rounded up. */
  .lpmap-panel-body { padding-top: 3.5rem; }
  .lpmap-panel:has(> #lpmap-panel-body[hidden]) {
    block-size: var(--hit);
    inline-size: var(--hit);
    overflow: hidden;
  }
}

/* --- cards (spec §5.3) -----------------------------------------------------
   Cards do not nest (spec §8: "a nested card" is forbidden outright). If a
   card ever wants a card inside it, it wants a sibling card instead. */
.card {
  padding: 0.9rem;
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  /* Spec §5.3 gives this a rgba(232,234,240,.04) tint; dropped after review —
     on a dark surface carrying light text, lightening the *background* to
     "lift" the card off the panel works against the very contrast it needs.
     The border and the .6rem gap between cards already delimit it; removing
     the tint recovers 0.47:1 (measured on the docked panel: 4.59:1 without
     the tint vs 4.11:1 with it, against --ink-soft). No fill of its own —
     it reads as the panel's own glass, framed. */
  background: transparent;
}
.card-title {
  margin: 0 0 0.5rem;
  color: var(--ink-soft);
  /* --t-sm at 600, not the scale's default 400 for that step — a faint
     uppercase label at 400 reads as decoration, not structure. Spec §5.3
     amended after review to say so explicitly. */
  font: 600 var(--t-sm) var(--ui);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
/* Content fade-in (spec §6: "no movement", opacity only). Not bound to
   data-state automatically — a card's content is replaced in place rather
   than re-inserted, so CSS alone cannot see "just became ready". Contract:
   Task 13 adds .lpmap-fade-in to whatever element it just filled in; the
   animation plays once and the class can be left in place afterwards. */
.lpmap-fade-in { animation: lpmap-card-in var(--lp-fade); }
@keyframes lpmap-card-in { from { opacity: 0; } to { opacity: 1; } }

/* Async states (spec §5.9). Task 13 sets data-state on the .card itself;
   "ready" is the default and needs no attribute at all. */
.lpmap-skeleton {
  display: none;
  border-radius: calc(var(--radius) - 2px);
  background: linear-gradient(90deg,
    rgba(232, 234, 240, 0.06) 25%, rgba(232, 234, 240, 0.12) 37%, rgba(232, 234, 240, 0.06) 63%);
  background-size: 400% 100%;
  animation: lpmap-shimmer 1.6s ease-in-out infinite;
}
@keyframes lpmap-shimmer { from { background-position: 100% 0; } to { background-position: 0 0; } }
.card[data-state="loading"] .lpmap-skeleton { display: block; block-size: var(--skel-h, 4rem); }
/* `:not(.lpmap-tonight-controls)` (Task 16): the Tonight card's date field
   and Moon toggle are controls, not reading-dependent content -- they do not
   wait on the reading the skeleton stands in for, and hiding them here would
   make them flicker out and back on every card load for no reason. */
.card[data-state="loading"] > :not(.card-title):not(.lpmap-skeleton):not(.lpmap-tonight-controls) { display: none; }

.card[data-state="stale"] { border-color: var(--warn); }
.lpmap-stale-note, .lpmap-unavailable-note {
  display: none;
  margin: 0.5rem 0 0;
  font: 500 var(--t-xs) var(--ui);
}
.lpmap-stale-note { color: var(--warn); }
.lpmap-unavailable-note { color: var(--ink-soft); font-style: italic; }
.card[data-state="stale"] .lpmap-stale-note { display: block; }
.card[data-state="unavailable"] { opacity: 0.85; }
.card[data-state="unavailable"] .lpmap-unavailable-note { display: block; }
/* `:not(.lpmap-tonight-controls)` (Task 16): a visitor whose chosen date has
   no astronomical darkness has to be able to try a different one -- the
   control that lets them cannot be the thing this state just hid. */
.card[data-state="unavailable"] > :not(.card-title):not(.lpmap-unavailable-note):not(.lpmap-tonight-controls) { display: none; }

.lpmap-placeholder {
  margin: 0;
  color: var(--ink-soft);
  font: var(--t-md) var(--ui);
}
.lpmap-placeholder a { color: var(--accent); }

.lpmap-hint {
  margin: 0.6rem 0 0;
  color: var(--ink-soft);
  font: var(--t-sm) var(--ui);
}
.lpmap-hint a { color: var(--accent); }

/* --- the headline block (spec §5.1) ---------------------------------------
   "One number rules the panel" (spec §1) — everything else in this card, and
   in the whole panel, is sized to support it, not compete with it. */
.lpmap-headline {
  display: flex;
  align-items: baseline;
  gap: 0.4rem;
  flex-wrap: wrap;
}
.lpmap-headline-value {
  font: 600 var(--t-hero) var(--data);
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}
.lpmap-headline-unit {
  color: var(--ink-soft);
  font: 500 var(--t-xs) var(--ui);
  letter-spacing: 0.02em;
}

/* The ramp bar: the one ramp colour this file is allowed to paint outside
   the legend, because the spec names it as such (§5.1). The marker is a
   plain white line with a dark outline, never one of the fifteen zone
   colours, so it reads over every zone from black to near-white. Contract:
   Task 13 sets --pos (0%-100%) on this element per reading. */
.lpmap-ramp-bar {
  position: relative;
  inline-size: 100%;
  block-size: 4px;
  margin: 0.65rem 0 0.5rem;
  border-radius: 999px;
  background: var(--lp-ramp);
}
.lpmap-ramp-bar::after {
  content: "";
  position: absolute;
  top: 50%;
  left: var(--pos, 50%);
  inline-size: 2px;
  block-size: 10px;
  background: #ffffff;
  box-shadow: 0 0 0 1px var(--void);
  transform: translate(-50%, -50%);
}

/* --- chips (spec §5.2) ------------------------------------------------------ */
.lpmap-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin: 0.5rem 0 0;
}
.chip {
  display: inline-flex;
  align-items: center;
  padding: 0.2rem 0.5rem;
  border: 1px solid var(--hairline);
  border-radius: 999px;
  background: rgba(232, 234, 240, 0.07);
  color: var(--ink-soft);
  font: 500 var(--t-xs) var(--ui);
  white-space: nowrap;
}
/* One variant, used for exactly one thing on screen at a time (spec §5.2). */
.chip-accent {
  background: color-mix(in oklab, var(--accent) 14%, transparent);
  color: var(--accent);
  border-color: transparent;
}

/* A reusable readout face for any numeric value Task 13 renders outside the
   headline (slider values, curve/dome hover readouts) — same rule as the
   headline: --data, tabular, never reflowing the row it sits in. */
.lpmap-figure {
  font-family: var(--data);
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}

/* The night curve's readout is also its keyboard control (`role="slider"` on
   the value, because the chart itself is a `role="img"` and everything inside
   one is presentational). It is in the tab order, so it needs the ring every
   other control on this page has — and a little padding, so the ring does not
   sit on the digits. */
.lpmap-cursor {
  margin-inline-end: -0.3rem;
  padding: 0.1rem 0.3rem;
  border-radius: 0.35rem;
  cursor: ew-resize;
}
.lpmap-cursor:focus-visible { outline: 2px solid var(--star); outline-offset: 1px; }

/* --- the night curve and sky dome containers (spec §5.7/§5.8) --------------
   Both are drawn by Task 13 (inline SVG, and a <canvas> respectively); this
   only reserves their footprint so nothing jumps in when they paint. */
.lpmap-nightcurve {
  display: block;
  inline-size: 100%;
  block-size: 6rem;
  margin: 0.6rem 0;
}
.lpmap-dome {
  display: block;
  inline-size: 100%;
  block-size: 12rem;
  margin: 0.6rem 0;
  border-radius: var(--radius);
  background: var(--void);
}

/* The horizon profile strip (spec §5.3.3), drawn as inline SVG from the same
   180-sample profile the dome silhouettes. Its own footprint is reserved the
   way the curve's and the dome's are, so a horizon arriving after the card is
   already on screen does not push the rest of the panel down. The N/E/S/W
   ticks inside it are drawn at 12px, the floor the UI spec sets. */
.lpmap-horizon-strip {
  display: block;
  inline-size: 100%;
  block-size: 3.5rem;
  margin: 0.6rem 0 0;
}

.lpmap-compare-btn {
  inline-size: 100%;
  min-block-size: var(--hit);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: transparent;
  color: var(--ink-soft);
  font: var(--t-sm) var(--ui);
  cursor: pointer;
}
.lpmap-compare-btn:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
.lpmap-compare-btn:focus-visible { outline: 2px solid var(--star); outline-offset: 2px; }
.lpmap-compare-btn:disabled { opacity: 0.55; cursor: default; }

/* --- the slider (spec §5.5) -------------------------------------------------
   No slider ships in the markup yet (Task 13 wires the opacity, darkness
   filter and time-of-night controls); these are the names implied by the
   spec's own component description. Contract: wrap the row in
   .lpmap-slider-wrap (it establishes the positioning context the floating
   value reads --fill against); the current value renders as a
   .chip/.lpmap-figure pairing beside the label at rest, or floating above
   the thumb while dragging via .lpmap-slider-value.is-live, using the same
   --fill custom property that colours the track. */
.lpmap-slider-wrap { position: relative; }
.lpmap-slider-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  margin-bottom: 0.3rem;
  color: var(--ink-soft);
  font: var(--t-sm) var(--ui);
}
.lpmap-slider-value { font: 500 var(--t-xs) var(--ui); }
.lpmap-slider-value.is-live {
  position: absolute;
  left: var(--fill, 50%);
  transform: translate(-50%, -145%);
}

.lpmap-slider {
  position: relative;
  appearance: none;
  -webkit-appearance: none;
  inline-size: 100%;
  block-size: var(--hit); /* full 44px hit area even though the track is 4px */
  margin: 0;
  background: transparent;
  cursor: pointer;
}
.lpmap-slider::-webkit-slider-runnable-track {
  block-size: 4px;
  border-radius: 999px;
  background: linear-gradient(to right,
    color-mix(in oklab, var(--star) 60%, transparent) 0 var(--fill, 50%),
    var(--line) var(--fill, 50%) 100%);
}
.lpmap-slider::-moz-range-track { block-size: 4px; border-radius: 999px; background: var(--line); }
.lpmap-slider::-moz-range-progress {
  block-size: 4px;
  border-radius: 999px;
  background: color-mix(in oklab, var(--star) 60%, transparent);
}
.lpmap-slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  margin-top: -0.6rem; /* centre a 1.25rem thumb on a 4px track */
  inline-size: 1.25rem;
  block-size: 1.25rem;
  border-radius: 50%;
  background: var(--ink);
  box-shadow: var(--lift-1);
}
.lpmap-slider::-moz-range-thumb {
  inline-size: 1.25rem;
  block-size: 1.25rem;
  border: 0;
  border-radius: 50%;
  background: var(--ink);
  box-shadow: var(--lift-1);
}
.lpmap-slider:focus-visible { outline: 2px solid var(--star); outline-offset: 2px; }

/* The darkness filter: the one other sanctioned appearance of the ramp
   (spec §5.5, "you are dragging along the legend"). The gradient is the
   track; no separate fill overlay is drawn on top of it. */
.lpmap-slider-ramp::-webkit-slider-runnable-track { background: var(--lp-ramp); }
.lpmap-slider-ramp::-moz-range-track { background: var(--lp-ramp); }
.lpmap-slider-ramp::-moz-range-progress { background: transparent; }

/* --- the rail (spec §5.4) ---------------------------------------------------
   Mobile ships three buttons (search, layers, terrain); 48rem adds the
   darkness filter, completing the search/layers/moon trio the spec's own
   desktop mockup (§4.1) draws; 64rem, with room to spare, restores the
   remaining two (locate, share) rather than leaving built, labelled controls
   permanently unreachable. Order in the markup is search, locate, layers,
   darkness filter, terrain, share.

   TERRAIN IS ON THE PHONE TOO, and that is a correction. It used to appear
   only at 64rem while card 3 — visible at every width — told the visitor to
   "turn it on with the Terrain control". A card that names a control the
   visitor cannot see is the defect, and of the two ways to close it (show the
   control, or stop naming it) showing it is the honest one: the horizon at a
   spot you are standing in is a field question, asked on a phone, and a
   fourth 44px button costs a vertical rail nothing. */
/* MOBILE IS THE BASE, AND ON MOBILE THE RAIL IS A ROW. As a column it ran off
   the bottom of the map at every phone-landscape size QA drove (railClipped at
   667x375, 740x360, 844x390 and 932x430), and in portrait it reached down over
   the two things under it: at 360x640 the rail spanned y 137-389 while the
   legend was 226-276 and the sheet started at 283, so `elementFromPoint` at
   the legend's "Brighter sky" caption returned a RAIL BUTTON, and the right
   44px of the sheet's own top edge was not the sheet.
   Across the top it costs the map 44px of height instead of 252, and five
   44px buttons plus their gaps are 252px against the 328 a 360px phone has to
   spare. The docked-panel breakpoint puts it back into a column, where there
   is height for it and the panel is beside the map rather than over it. */
.lpmap-rail {
  position: fixed;
  z-index: 40;
  top: 1rem;
  right: 1rem;
  display: flex;
  flex-direction: row;
  /* NO `flex-wrap` HERE, and that is a decision with a measurement behind it.
     A short viewport could in principle run out of room for the column, and
     `flex-wrap: wrap-reverse` looks like the answer — but a column flex
     container with wrap enabled and an auto inline-size resolves its
     shrink-to-fit width from the max-content size as though the items were in
     a row, so the rail's box became 252px WIDE (measured, Chromium) and
     swallowed every pointer event across a quarter of the map, including the
     sheet's own drag handle. It is not needed either: the header on this route
     is now one row, so the map keeps ~535px at 360x640 against five 44px
     buttons and their gaps, 252px. */
  gap: var(--rail-gap);
}
.rail-btn {
  inline-size: var(--hit);
  block-size: var(--hit);
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  background: var(--glass);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  box-shadow: var(--lift-2);
  color: var(--ink);
  cursor: pointer;
}
.rail-btn .icon { inline-size: 1.25rem; block-size: 1.25rem; }
.rail-btn[aria-pressed="true"], .rail-btn.is-active {
  background: color-mix(in oklab, var(--accent) 16%, var(--glass));
  color: var(--accent);
}
.rail-btn:focus-visible { outline: 2px solid var(--star); outline-offset: 2px; }

/* WHICH BUTTONS A PHONE GETS, CORRECTED. This used to hide `locate` and
   `share` below 64rem and restore the darkness filter at 48rem — which left
   the two controls that only make sense on a phone reachable only on a
   desktop. Geolocation is the whole reason a map asks where you are, and
   Share is the reason the URL is a permalink; neither has any other
   affordance on this page. The darkness filter is the one that can wait: it
   refines a picture the visitor is already looking at, and every value it can
   set is also readable off the legend. So the phone gets five buttons and the
   filter joins at 48rem. */
.lpmap-rail .rail-btn:nth-child(4) { display: none; } /* darkness filter */

@media (min-width: 48rem) {
  .lpmap-rail .rail-btn:nth-child(4) { display: flex; }
}

@media (min-width: 64rem) {
  .lpmap-rail { flex-direction: column; }
}

/* --- the legend (spec §5.6) -------------------------------------------------
   Mobile: a full-width strip above the sheet. 64rem: a small card bottom
   right, per the desktop mockup in spec §4.1.

   IT USED TO BE PINNED ABOVE THE *PEEK* DETENT AND THE SHEET OPENS AT *HALF*.
   Measured by QA: at 390x844 the legend occupied y 452-559 with the sheet
   starting at 412, so it was entirely behind the panel on the default visit,
   and `elementFromPoint` at its own top returned the panel body; at 360x640
   the two overlapped by 35px and the attribution ran through the panel's place
   line. It cleared only at `peek`, which the visitor had to discover by
   dragging. The legend now follows the detent — the same three ratios the
   panel itself uses, and the panel is the containing block for both, so the
   two cannot drift apart. At `full` the panel covers 92% of the map and the
   legend is not shown; the attribution it used to carry has moved into the
   panel body, so nothing is unreachable there. */
.lpmap-legend {
  position: fixed;
  z-index: 20;
  inset-inline: 0;
  bottom: calc(55% + 0.5rem); /* the `half` detent, which is also the default */
  padding: 0.5rem 0.9rem 0.65rem;
  border-top: 1px solid var(--hairline);
  background: var(--glass);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  box-shadow: var(--lift-2);
}
.lpmap-legend-bar {
  display: flex;
  margin: 0;
  padding: 0;
  list-style: none;
  block-size: 0.5rem;
}
.lpmap-swatch { flex: 1 1 0; min-inline-size: 0; }
.lpmap-swatch:first-child { border-start-start-radius: 999px; border-end-start-radius: 999px; }
.lpmap-swatch:last-child { border-start-end-radius: 999px; border-end-end-radius: 999px; }
/* "Darker skyBrighter sky", which is what `justify-content: space-between`
   prints when there is no free space to distribute: at 64rem and above the
   legend is `inline-size: auto`, so it shrink-wraps to exactly the caption's
   own width and the two labels touch with ZERO pixels between them at 1024,
   1440, 2560 and 3440. A `gap` is a floor on the distance rather than a
   distribution of what is left, so it holds at every width; `min-inline-size`
   below gives the shrink-wrapped card enough room that the colour bar is a
   scale and not a stripe. */
.lpmap-legend-caption {
  display: flex;
  justify-content: space-between;
  gap: 1.5rem;
  margin: 0.35rem 0 0;
  color: var(--ink-soft);
  font: 500 var(--t-xs) var(--ui);
}
/* Now the last thing in the panel body rather than the last line of the
   legend — see the legend's own note above for why it moved. A little top
   space so it reads as a footnote to the cards and not as an eighth card. */
.lpmap-attribution {
  margin: 0.25rem 0 0;
  color: var(--ink-soft);
  /* Was a hand-set line-height: 1.45 here — the --t-xs step is 1.4 (spec
     §3); the token now carries the ratio so this can't drift again. */
  font: 500 var(--t-xs) var(--ui);
}

/* The detent the sheet is actually at. `half` is the base rule above, and is
   also what an attribute-less panel (no JS yet) resolves to. */
.lpmap-app:has(.lpmap-panel[data-detent="peek"]) .lpmap-legend { bottom: calc(7rem + 0.5rem); }
.lpmap-app:has(.lpmap-panel[data-detent="full"]) .lpmap-legend { display: none; }

@media (min-width: 64rem) {
  /* The docked panel is a column beside the map, not a sheet over it, so the
     legend is back in the corner at every detent. Both `:has` selectors are
     restated because a media query adds no specificity and they would
     otherwise keep winning. */
  .lpmap-legend,
  .lpmap-app:has(.lpmap-panel[data-detent="peek"]) .lpmap-legend {
    inset-inline: auto;
    right: 1rem;
    bottom: 1rem;
    inline-size: auto;
    min-inline-size: 15rem;
    max-inline-size: var(--panel-w);
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
  }
  .lpmap-app:has(.lpmap-panel[data-detent="full"]) .lpmap-legend { display: block; }
}

/* ===========================================================================
   TASK 13 ADDITIONS — the wiring's own surfaces.

   Everything above was written against Task 11's first markup, which had no
   site chrome: spec §1 says "the map is the page". Task 11 was then reconciled
   with build.mjs's canonical-frame contract (commit 1442c5293), which makes a
   real site-header and a real footer mandatory on every route, each inside its
   own `.wrap`. That landed after this file was written and nothing here was
   updated for it, so as committed `.lpmap-app` (position: fixed; inset: 0)
   paints straight over both of them: the navigation and the rights notice are
   on the page, invisible and unreachable, with `body { overflow: hidden }`
   ruling out scrolling to them. The first block below is that fix.

   The rest are the four component shapes the wiring needs and Task 12 had no
   markup to style: a rail popover, a text field, a label/value row list, and a
   transient confirmation. They are named in this file's own vocabulary rather
   than invented per call site, and they are listed in the Task 13 report as an
   extension of Task 12's class list rather than a silent addition.
   =========================================================================== */

/* --- the canonical frame, above the map, and the map flush between ---------
   Body becomes the column: header, map, footer. `.lpmap-app` stops being
   viewport-fixed and becomes the flexible middle of it, so the chrome is
   visible and the map still fills everything that is left. Its own children
   keep every offset they were given; they only change which box those offsets
   are measured against, which is why nothing else in this file moves.

   `.wrap`'s `padding: 1.5rem 1.15rem 4rem` (site.css) is what stops the map
   being flush against the header and footer — 4rem of empty page under the
   nav. Only the padding is overridden; the width and centring stay, so the
   brand still lands where it does on every other route. */
body:has(.lpmap-app) {
  display: flex;
  flex-direction: column;
  min-block-size: 100vh;
  min-block-size: 100dvh;
}
body:has(.lpmap-app) > .wrap { flex: none; padding: 0.9rem 1.15rem; }
/* AND THE FOOTER'S OWN 3rem OF AIR IS THE MAP'S. `site.css` gives every footer
   `margin-top: 3rem; padding-top: 1.2rem` to separate it from an article; on
   this route there is no article above it, only the map, and the separation is
   the map's own edge. 48px of empty page under a full-bleed map, on every
   viewport, for nothing -- and on a phone in landscape it was a seventh of the
   whole screen. */
body:has(.lpmap-app) footer.site-footer { margin-top: 0; padding-top: 0.6rem; }
/* THE HEADER IS TAKING THE MAP'S ROOM. At 360x640 the shared nav wraps to
   three 44px rows and the header eats 202px of a 640px viewport, leaving the
   map a 268px strip — and the nav has since gained a sixth entry (the map's
   own). On this one route the nav becomes a single row that scrolls sideways
   inside itself: every link keeps its 44px target and its place in the tab
   order, the page itself still has no horizontal scroll, and the map gets
   about 90px back. Only here; every other route keeps the wrapping nav. */
body:has(.lpmap-app) header.site-header { margin-bottom: 0; }
body:has(.lpmap-app) header.site-header nav {
  flex-wrap: nowrap;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
}
body:has(.lpmap-app) header.site-header nav::-webkit-scrollbar { display: none; }
/* AND THE ITEMS INSIDE IT MUST NOT SHRINK. The rule above opted this route
   into a horizontal scroller and then forgot the half that makes one work:
   `nav a` is a flex item with `flex-shrink: 1`, so instead of overflowing (and
   scrolling) the six links compressed to their own 44px tap-target floor while
   their `nowrap` labels overprinted each other. QA read the result at 390px as
   a single illegible line -- "ck aLight pollutioPlan & toolsDemo galleryGuides
   Why it is diff..." -- clipped at both ends, and it was the first thing above
   the map on every phone visit. Measured there: scrollWidth 386 against
   clientWidth 353, six boxes of 44-55px each.

   `flex-shrink: 0` is the whole fix: the links keep their natural width, the
   nav's own `overflow-x: auto` carries the difference, and the page still has
   no horizontal scroll of its own because the nav's `min-width: auto` resolves
   to 0 under a non-visible overflow. `/light-pollution-map/how-it-works/` has
   no `.lpmap-app`, keeps the wrapping nav, and was never affected. */
body:has(.lpmap-app) header.site-header nav a { white-space: nowrap; flex-shrink: 0; }
.lpmap-app {
  position: relative;
  inset: auto;
  flex: 1 1 auto;
  min-block-size: 0;
}
.lpmap-panel, .lpmap-rail, .lpmap-legend { position: absolute; }

/* The three detents were `vh` when `.lpmap-app` was the viewport. It is now the
   flexible middle of a column with a header above it and a footer below, so a
   92vh sheet is taller than the box it lives in and its top is clipped. The
   same three ratios, measured against the map instead: a percentage height on
   an absolutely positioned child resolves against exactly that box. */
.lpmap-panel { block-size: 55%; max-block-size: 92%; }
.lpmap-panel[data-detent="peek"] { block-size: 7rem; }
.lpmap-panel[data-detent="half"] { block-size: 55%; }
.lpmap-panel[data-detent="full"] { block-size: 92%; }

/* --- A PHONE HELD SIDEWAYS (QA-C2) -----------------------------------------
   Measured on the live page, as visible map pixels between the header and
   whichever of legend/sheet came first:

     667x375  header 92px, legend at y 106, sheet at y 164 ->   0 px of map
     740x360  header 92px, legend at y 100, sheet at y 157 ->   0 px of map
     844x390  header 44px, legend at y  87, sheet at y 145 ->  14 px of map
     932x430  header 44px, legend at y 105, sheet at y 163 ->  32 px of map

   A map page showing no map. Nothing here is a new mechanism: the detents,
   the legend's detent-following and the header's single-row treatment all
   already exist. What was missing is that none of them had a floor, so the
   55%/45% split of a 290px column left nothing on either side of it.

   Four moves, in the order they pay:
     * the shared header stops wrapping to two rows. At 667 and 740 the brand
       and the nav did not fit on one line, so the header cost 92px instead of
       44; the nav on this route is already a horizontal scroller and can take
       the overflow.
     * the chrome's padding shrinks to a third.
     * the sheet opens at `peek` rather than `half` (`lpmap-sheet.js` owns
       that, and clamps a remembered `half`/`full` here too).
     * the legend hides at `peek`, exactly as it already does at `full`.

   Scoped under 64rem on purpose: a short but WIDE window gets the docked
   panel, which is beside the map rather than over it, and needs none of this.
   The four sizes above are all 667-932px wide. */
@media (max-height: 32rem) and (max-width: 63.99rem) {
  body:has(.lpmap-app) > .wrap { padding: 0.3rem 1.15rem; }
  body:has(.lpmap-app) footer.site-footer { padding-top: 0.3rem; }
  body:has(.lpmap-app) footer.site-footer p { margin: 0; }
  body:has(.lpmap-app) header.site-header { flex-wrap: nowrap; }
  body:has(.lpmap-app) header.site-header a.brand { flex: none; white-space: nowrap; }
  /* The legend is a strip over the map; at `peek` there is not enough map for
     both it and the sheet, and the colour key is the one of the two that is
     also printed in words in the panel. */
  .lpmap-app:has(.lpmap-panel[data-detent="peek"]) .lpmap-legend { display: none; }
}

@media (min-width: 64rem) {
  /* 4.5rem cleared a brand pill this route stopped shipping when the canonical
     frame gave it a real site-header; the panel starts at the top instead. */
  .lpmap-panel { top: 1rem; }
  /* Restated after the detent block above, which would otherwise reintroduce a
     sheet height on the docked panel. Detents are a sheet-only idea. */
  .lpmap-panel, .lpmap-panel[data-detent] { block-size: auto; max-block-size: none; }
}

/* --- Leaflet's own chrome --------------------------------------------------
   Task 12 flagged this as unstylable until the library was loaded. Only two
   rules are needed, because the map is created with `zoomControl: false` and
   `attributionControl: false`: this page draws neither, so Leaflet's light
   control chrome never appears. The container's default `#ddd` fill would
   otherwise flash white before the first tile paints, on a page whose whole
   premise is that a white flash at 1 a.m. is an injury. */
/* ONE ELEMENT, NOT TWO. These were written `.lpmap-map .leaflet-container`
   -- a DESCENDANT combinator -- but Leaflet puts its `leaflet-container`
   class on the very element it is handed, so `#lpmap-map` carries BOTH
   classes and the selector matched nothing on this page. Measured live:
   `.lpmap-map .leaflet-container` has no match in the document, and the map,
   which is `tabindex="0"` and a real tab stop with arrow-key panning, drew
   the BROWSER's default 1px `auto` ring instead of this page's 2px
   `var(--star)` -- the only one of thirteen focusable things here that did
   not look focused the way the rest do. The background declaration was dead
   too, though harmlessly: `.lpmap-map` above already sets `--void`, later in
   this same file than Leaflet's `#ddd`, so no white ever flashed. */
.lpmap-map.leaflet-container {
  background: var(--void);
  font: var(--t-xs) var(--ui);
}
.lpmap-map:focus-visible {
  outline: 2px solid var(--star);
  outline-offset: -2px;
}

/* --- rail popovers ---------------------------------------------------------
   Search, layers and the darkness filter open here: a small glass card
   anchored under the rail. Never modal, never over the panel (spec §8 forbids
   a modal over the map on desktop, and this is not one — the map stays live
   and a click outside closes it). */
/* Under the rail on a phone, beside it on a desktop -- the rail changes
   direction at 64rem and a popover anchored to the wrong side of it would
   land on top of the buttons that opened it. */
.lpmap-popover {
  position: absolute;
  z-index: 45;
  top: calc(1rem + var(--hit) + var(--rail-gap));
  right: 1rem;
  inline-size: min(18rem, calc(100% - 2rem));
  padding: 0.75rem;
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  background: var(--glass-strong);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  box-shadow: var(--lift-3);
  color: var(--ink);
}
.lpmap-popover > * + * { margin-block-start: 0.6rem; }
@media (min-width: 64rem) {
  .lpmap-popover {
    top: 1rem;
    right: calc(1rem + var(--hit) + var(--rail-gap));
    inline-size: min(18rem, calc(100% - 2rem - var(--hit) - var(--rail-gap)));
  }
}

.lpmap-field {
  inline-size: 100%;
  min-block-size: var(--hit);
  padding: 0 0.6rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: rgba(232, 234, 240, 0.05);
  color: var(--ink);
  font: var(--t-sm) var(--ui);
}
.lpmap-field::placeholder { color: var(--ink-soft); }
.lpmap-field:focus-visible { outline: 2px solid var(--star); outline-offset: 2px; }

/* --- label/value rows ------------------------------------------------------
   The exposure table, the horizon figures, the tonight figures and the
   compare columns are all the same shape: a word on the left and a `--data`
   number on the right that must not reflow the row as it changes. */
.lpmap-rows {
  margin: 0.6rem 0 0;
  padding: 0;
  list-style: none;
}
.lpmap-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.25rem 0;
  border-block-start: 1px solid var(--hairline);
  color: var(--ink-soft);
  font: var(--t-sm) var(--ui);
}
.lpmap-row:first-child { border-block-start: 0; }
/* A ROW OF TEXT AND A ROW WITH A CONTROL IN IT ARE NOT THE SAME ROW.
   `align-items: baseline` is right for a label against a `--data` figure and
   wrong for a checkbox or a radio: a form control's baseline is its bottom
   edge, so every control in the Layers popover and in the Tonight card floated
   about 17px above the label it belonged to inside a 44px row. And they were
   drawn in the OS's own blue against an amber/graphite page, because nothing
   ever set `accent-color`. */
.lpmap-row:has(input[type="checkbox"]),
.lpmap-row:has(input[type="radio"]) { align-items: center; }
.lpmap-row input[type="checkbox"],
.lpmap-row input[type="radio"] {
  flex: none;
  inline-size: 1.1rem;
  block-size: 1.1rem;
  accent-color: var(--accent);
}

/* --- the share confirmation ------------------------------------------------
   One line, announced politely, gone in a few seconds. It is not a toast that
   stacks, animates or can be dismissed: there is only ever one, and it says
   whether the link reached the clipboard. */
/* --- what the map is doing, in words (QA-C2) --------------------------------
   The map area is --void until an overlay tile paints, and the wide-area grid
   used to arrive whole before the first pixel: 15.2 seconds of black rectangle
   at z6 on a throttled phone, measured, with no base map under it either. This
   is the affordance that stops that being an unexplained blank. It sits at the
   top of the map, centred in the band between the panel (left, at 64rem) and
   the rail (top right) so it can never be under either -- 35 keeps it above the
   panel's 30 and below the rail's 40. --t-sm rather than the --t-xs the toast
   uses: this is the one thing on screen when there is nothing else to read. */
.lpmap-status {
  position: absolute;
  z-index: 35;
  /* Below the rail on a phone, where the rail is a row across the top; beside
     it at 64rem, where it is a column down the right. */
  top: calc(1rem + var(--hit) + var(--rail-gap));
  left: 1rem;
  right: 1rem;
  margin-inline: auto;
  inline-size: fit-content;
  max-inline-size: 100%;
  padding: 0.5rem 0.85rem;
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  background: var(--glass-strong);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  box-shadow: var(--lift-2);
  color: var(--ink);
  font: 500 var(--t-sm) var(--ui);
}
@media (min-width: 64rem) {
  /* Clear of the docked panel: 1rem + --panel-w + a 1rem gap, and clear of
     the rail, which is a column again here. */
  .lpmap-status {
    top: 1rem;
    left: calc(2rem + var(--panel-w));
    right: calc(1rem + var(--hit) + var(--rail-gap));
  }
}
/* An indeterminate bar, because a line of static text over a black rectangle
   reads as stuck rather than as working. Motion only; the sentence above it
   carries the whole message on its own. */
.lpmap-status-bar {
  display: block;
  block-size: 2px;
  margin-block-start: 0.4rem;
  border-radius: 999px;
  background: linear-gradient(to right,
    transparent 0%, var(--star) 35%, var(--star) 65%, transparent 100%);
  background-size: 60% 100%;
  background-repeat: no-repeat;
  animation: lpmap-status-sweep 1.4s linear infinite;
}
@keyframes lpmap-status-sweep {
  from { background-position: -60% 0; }
  to { background-position: 160% 0; }
}
/* A NOTICE IS NOT A WAIT. The same element carries two kinds of message now:
   "the wide-area grid is still arriving" (something is coming, and the bar
   says so) and "both layers are switched off" (a settled answer, and nothing
   is coming). An indeterminate sweep under the second reads as a page still
   trying, which is the opposite of what it says. `lpmap.js` sets the
   attribute; the sentence itself is unchanged either way. */
.lpmap-status[data-kind="notice"] .lpmap-status-bar { display: none; }

/* NEVER OVER A RAIL BUTTON. Both the share confirmation and the terrain
   toast rendered at the top right, directly on top of "Use my location" --
   QA caught the toast that says a fetch has started covering the control
   that started it. On a phone the rail is a row now, so one row's clearance
   puts the toast below every button; on a desktop the rail is a column and
   the clearance has to be the whole column (six 44px buttons and their gaps),
   which is restated at 64rem below. */
.lpmap-toast {
  position: absolute;
  z-index: 50;
  top: calc(1rem + var(--hit) + var(--rail-gap));
  right: 1rem;
  max-inline-size: min(20rem, calc(100% - 2rem));
  padding: 0.5rem 0.75rem;
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  background: var(--glass-strong);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  box-shadow: var(--lift-2);
  color: var(--ink);
  font: 500 var(--t-xs) var(--ui);
}
@media (min-width: 64rem) {
  .lpmap-toast { top: calc(1rem + 6 * (var(--hit) + var(--rail-gap))); }
}

/* ===========================================================================
   TASK 16 ADDITIONS -- the Tonight card's date field and Moon toggle (design
   spec §7), the stacker handoff link (§9, styled entirely by the existing
   `.cta` rule in site.css -- nothing new needed there), and the base map
   choice (§5.4).
   =========================================================================== */

/* THE WRAPPER AROUND THE STACKER LINK, WHICH IS NOT ITSELF A BUTTON. It
   shipped as `<p class="cta">` holding `<a class="cta">`, so site.css's pill
   rule ran twice: the paragraph became a second inline-flex accent pill with
   its own 1.4rem of side padding and 48px min-height. Same colour as the
   anchor, so nobody saw two pills -- but it took 44.8px off the anchor's
   available line box, which is what made "Stack a night from this sky" wrap
   its arrow onto a second line in the 22rem docked panel at EVERY desktop
   width, not only on a phone. The wrapper's whole job is the margin above the
   link, so that is all it does. */
.lpmap-cta { margin: 1rem 0 0; }

/* The date field and Moon toggle sit above the curve as ordinary rows, using
   the same `.lpmap-row` shape the Layers popover's toggles already use. The
   wrapper only exists so the loading/unavailable state rules above can name
   it once and exempt it -- see their own comments for why it must stay on
   screen through both states. */
.lpmap-tonight-controls {
  display: flex;
  flex-direction: column;
  margin: 0 0 0.6rem;
}

.lpmap-date-input {
  min-block-size: var(--hit);
  padding: 0 0.6rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: rgba(232, 234, 240, 0.05);
  color: var(--ink);
  font: var(--t-sm) var(--data); /* --data: a date is a row of digits */
}
.lpmap-date-input:focus-visible { outline: 2px solid var(--star); outline-offset: 2px; }
/* AND THE SEGMENT INSIDE IT. A native date input is several keyboard stops in
   one element -- day, month, year -- and only the element carries the ring
   above, so arrowing between segments moved a focus nothing on screen showed
   (QA measured `outline: 0px none` on one of them). The browser's own default
   highlight is a system colour that does not read on a dark page, so the
   focused segment is painted in this page's own accent instead. */
.lpmap-date-input::-webkit-datetime-edit-day-field:focus,
.lpmap-date-input::-webkit-datetime-edit-month-field:focus,
.lpmap-date-input::-webkit-datetime-edit-year-field:focus {
  background: color-mix(in oklab, var(--star) 40%, transparent);
  color: var(--ink);
  border-radius: 2px;
}

/* The escape from a chosen date, as a real button rather than as "empty the
   field": Chromium and WebKit both draw an `<input type="date">` with no
   clear affordance at all, so the documented way out of an override was one
   the visitor had no way to take. Sits under the two rows, aligned to their
   right edge where the controls' own values are, and is deliberately never
   disabled -- pressing it while already on tonight is a no-op, and a disabled
   button would leave the tab order every time the panel resolved. */
.lpmap-date-reset {
  align-self: flex-end;
  min-block-size: var(--hit);
  margin-block-start: 0.2rem;
  padding: 0 0.7rem;
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  background: var(--glass);
  color: var(--ink);
  font: 500 var(--t-xs) var(--ui);
  cursor: pointer;
}
.lpmap-date-reset:focus-visible { outline: 2px solid var(--star); outline-offset: 2px; }

/* "Times in America/Los_Angeles (UTC−7)". At the --t-xs floor, which is the
   smallest type this site allows and the size the chips and the legend ticks
   already use — it is a caption for the controls above it, not a control.
   A paragraph rather than a row so it WRAPS: an IANA name is one unbreakable
   token of up to about twenty characters, and the panel is 22rem on a desktop
   and the whole width of a phone. `overflow-wrap: anywhere` is the guard for
   the longest of them (America/Argentina/ComodRivadavia) at the narrowest
   panel this site supports; a 320px sideways scroll on this site has come
   from one unbreakable word before. */
.lpmap-zone {
  margin: 0.35rem 0 0;
  color: var(--ink-soft);
  font: 500 var(--t-xs) var(--ui);
  overflow-wrap: anywhere;
}

/* A named choice between exactly as many options as honestly exist under this
   page's CSP (spec §5.4) -- never a bigger menu than that. Rows reuse
   `.lpmap-row`; this only groups them under a caption smaller than the
   popover's own `.card-title` so two heading weights are not stacked. */
.lpmap-choice { display: flex; flex-direction: column; }
.lpmap-group-label {
  margin: 0 0 0.3rem;
  color: var(--ink-soft);
  font: 500 var(--t-xs) var(--ui);
}
