/* Tracker UI.
 *
 * Phone-first. Two things matter most on a handset and drive the whole layout:
 *   - the map must own the full screen, including under the notch and home bar
 *     (viewport-fit=cover + env(safe-area-inset-*) here);
 *   - nothing interactive may be smaller than a fingertip, so controls have a
 *     44px minimum and detail lives in a bottom sheet within thumb reach
 *     rather than a top-anchored popup.
 */

:root {
  /* Native widgets — the <option> list a <select> opens, scrollbars, focus
     rings, date pickers — are drawn by the browser and never see our tokens.
     `color-scheme` is the only thing that reaches them. Reported: the Space
     view's satellite dropdown stayed dark in light mode, which was this and
     not the `.groupsel` rule above it. The <meta> tag theme.js updates is a
     hint for the surrounding UI; this is what actually styles the control. */
  color-scheme: dark;

  --bg: #070b12;
  --panel: rgba(14, 20, 31, 0.92);
  --panel-solid: #0e141f;
  --line: rgba(255, 255, 255, 0.1);
  --text: #e9eef5;
  --muted: #93a2b8;
  --accent: #5ac8fa;
  --danger: #ff453a;

  --aircraft: #5ac8fa;
  --vessel: #32d74b;
  --train: #ffd60a;
  --lightning: #ff375f;
  --weather: #64d2ff;
  --quake: #ff9f0a;
  --camera: #64d2ff;
  --railmap: #a0e8b0;
  --fire: #ff6b00;
  --aurora: #7ee787;
  --volcano: #ff453a;
  --animal: #e8d5b7;
  --sighting: #5ce1c2;
  --sonde: #7ee787;
  --buoy: #4fc3f7;
  --satellite: #d0bcff;
  --metar: #32d74b;
  --disaster: #ff9f0a;
  --airport: #8fa3bf;
  --tide: #4dd0e1;
  --river: #4fc3f7;
  --radiation: #c6ff4d;
  --fireball: #ffd166;
  --place: #b0bec5;
  --dinner: #ffab6b;

  --safe-t: env(safe-area-inset-top, 0px);
  --safe-b: env(safe-area-inset-bottom, 0px);
  --safe-l: env(safe-area-inset-left, 0px);
  --safe-r: env(safe-area-inset-right, 0px);

  --tap: 44px;
  --radius: 14px;

  /* `--fg` was used in thirteen rules and defined in none of them — including
     two that predate the games panel. An undefined custom property makes the
     whole declaration invalid at computed-value time, so `color: var(--fg)`
     silently became "inherit". It looked fine on dark because the parent was
     already the right colour, and it would not have survived light mode. An
     alias fixes all thirteen without touching them. */
  --fg: var(--text);

  /* Surfaces and accents that were written as literals and therefore could not
     switch. A white film over a dark page is the right way to raise a surface;
     on a light page it is invisible, so these have to be tokens rather than
     `rgba(255,255,255,0.03)` repeated fourteen times. */
  --raised: rgba(255, 255, 255, 0.03);
  --raised-hover: rgba(255, 255, 255, 0.06);
  --grip: rgba(255, 255, 255, 0.22);
  --grip-hover: rgba(255, 255, 255, 0.4);
  /* Two degrees of "look at this", used for an advisory's border and a stop
     the hours say is shut. Tokens rather than literals for the reason above —
     the light theme needs a darker, more saturated version of each or they
     wash out to nothing against a white panel. */
  --warn: #ffab6b;
  --bad: #ff7b7b;
  /* Places to stay. Matches the bed icon in icons.js, which is a canvas
     texture and so carries its own literal — the two are kept in step by
     eye, as every other map colour is. */
  --stays: #8fd6a8;
  /* Never a token before — written as #7ee787 in two rules, so it could not
     switch and measured **1.54** as a sheet label on the light theme, which
     is pale green on near-white. */
  --airquality: #7ee787;
  /* Museums. Matches the portico icon in icons.js, which is a canvas texture
     and carries its own literal — kept in step by eye, as every map colour
     is. */
  --culture: #d6b8ff;
  /* "This is the affirmative action" — the approve button and the approved
     badge. Written as a literal `#3fc9a5` with `var(--on-accent)` on top,
     which is right in this theme (near-black ink on mint, 9.25) and wrong in
     the other, where --on-accent turns white and gives 2.08. The pair has to
     move together, so both are tokens. */
  --go: #3fc9a5;
  --on-go: #041019;

  /* Whose day it is, on the calendar.
     These lived in calendar.js as hex constants, which meant they could not
     switch themes — and they are tuned for a dark page, so on a light one
     Jordan's blue measured 2.54 against the panel and 2.63 under the white
     badge ink. Both uses fail at once, because the same colour is a *text*
     colour on the date and a *background* on the count.
     Bright here, deep in the light theme; `--on-accent` already flips the
     right way for the ink on top (near-black on a bright colour, white on a
     deep one). Measured: #ff6fa5 is 7.09 on this panel and takes the dark ink
     at 7.38. */
  --who-sophia: #ff6fa5;
  --who-jordan: #4aa3ff;
  --who-a: #c9a6ff;
  --who-b: #8ce8c3;
  --who-c: #ffd84d;
  --who-d: #ff8a3d;
  --who-e: #4dd0e1;
  /* Text drawn *on* the accent colour. Dark ink on a bright cyan is right;
     the same ink on the light theme's darker accent is unreadable. */
  --on-accent: #041019;
  --shadow: rgba(0, 0, 0, 0.5);
  --shadow-soft: rgba(0, 0, 0, 0.32);
  --scrim-top: rgba(7, 11, 18, 0.95);
  --scrim-bottom: rgba(7, 11, 18, 0);
  /* A field: an input, a select, a card. Darker than the panel behind it. */
  --field: #0a0f18;
}

/* ---- light ----
 *
 * Only the chrome tokens change. The layer colours above are identity — an
 * aircraft is the same blue in both themes — and they are consumed by the
 * legend and the panel swatches rather than by the map, which takes its
 * colours from PALETTE in app.js. A few of them are pale enough to vanish
 * against a light panel, so those are darkened here and nowhere else.
 */
:root[data-theme="light"] {
  color-scheme: light;

  --bg: #f4f6fa;
  --panel: rgba(255, 255, 255, 0.94);
  --panel-solid: #ffffff;
  --line: rgba(12, 22, 38, 0.16);
  --text: #101720;
  --muted: #55637a;
  /* Darker than the dark theme's #5ac8fa, which fails contrast on white —
     and darker again than the #0a7ea4 this started at, because that only just
     cleared the bar on a *plain* panel and quietly failed on a raised one.
     Measured against the composited surfaces rather than against white:

                        plain   raised   raised+hover
         #0a7ea4         4.63     4.27      3.93     <- under AA twice over
         #096e8e         5.78     5.33      4.91

     Every accent link that sits on a raised surface was affected, which is
     most of them — the source-health rows, the detail-sheet links, and now
     the travel advisories. Compositing matters: a 4% black film over a pale
     panel darkens the background a little, and measuring the text against the
     panel instead of against what is actually painted misses it entirely.
     White on the accent improves too, 4.63 to 5.78, which is what the buttons
     that use it as a background need. */
  --accent: #096e8e;
  --danger: #c62828;

  /* Swatch colours that disappear on a pale panel. Same hue, enough contrast
     to be seen — not a different palette, a legible one. */
  --aircraft: #0a7ea4;
  /* Darker than the #1b8e2d this started at, which is a *swatch* colour that
     also paints the detail sheet's kind label: 4.09 as text on a pale panel,
     just under the bar. 6.02 now, and still the same green on a dot. */
  --vessel: #147021;
  --train: #8a6700;
  --weather: #0b7fae;
  --camera: #0b7fae;
  --railmap: #2e7d32;
  --aurora: #2e7d32;
  --sonde: #2e7d32;
  --animal: #8a6d3b;
  --sighting: #0f7368;
  --buoy: #0b7fae;
  --satellite: #6f4bd8;
  --metar: #147021;
  --airport: #4a6285;
  --tide: #0b7684;
  --river: #0b7fae;
  --radiation: #5a7a00;
  --fireball: #8c6215;
  --place: #546e7a;
  --dinner: #c2600f;

  --raised: rgba(12, 22, 38, 0.04);
  --raised-hover: rgba(12, 22, 38, 0.08);
  --grip: rgba(12, 22, 38, 0.25);
  --grip-hover: rgba(12, 22, 38, 0.45);
  --warn: #b35c00;
  --bad: #c62828;
  --stays: #186349;
  --airquality: #1c7a2a;
  --culture: #6a3fb5;
  /* A deeper mint, which sits better on a pale panel and still takes the dark
     ink comfortably: 5.71 against 9.25 for the brighter one. White is what
     fails here — 3.37 on this green and 2.08 on the dark theme's. */
  --go: #1f9e7c;
  --on-go: #041019;

  /* Deep enough to read as text on a near-white panel *and* to take white ink
     as a badge background — the two things each of these has to do. Measured
     on the light panel / under white: Sophia 5.67 and 5.87, Jordan 5.55 and
     5.75. Still unmistakably her pink and his blue; the bright versions are
     2.51 and 2.54 here, which is not a shade of readable. */
  /* The hazard colours, which are *not* only swatches.
     `.kind[data-kind="quake"] { color: var(--quake) }` paints the detail
     sheet's kind label, so the bright amber that reads at 8.97 on the dark
     panel reads at **1.99** here — orange text on near-white. Found by a test
     asking which tokens the light theme never redefines, not by eye: no quake
     sheet happened to be open during the visual sweep, which is exactly the
     sort of thing a walk-the-page audit misses.
     Same hue, dark enough to be text: lightning 5.76, quake 5.35, fire 5.31,
     volcano 5.43. The layer-panel swatches they also feed are backgrounds and
     only get easier to see. */
  --lightning: #c0203f;
  --quake: #8a6100;
  --fire: #b34700;
  --volcano: #c62828;
  --disaster: #8a6100;

  --who-sophia: #c2185b;
  --who-jordan: #1565c0;
  --who-a: #5e35b1;
  --who-b: #15664a;
  --who-c: #7a5500;
  --who-d: #a03f00;
  --who-e: #00626f;
  /* White on the light theme's darker accent, not the dark theme's near-black
     ink — which on #0a7ea4 is close to unreadable. */
  --on-accent: #ffffff;
  --shadow: rgba(22, 34, 54, 0.18);
  --shadow-soft: rgba(22, 34, 54, 0.12);
  --scrim-top: rgba(244, 246, 250, 0.95);
  --scrim-bottom: rgba(244, 246, 250, 0);
  --field: #ffffff;
}

* { box-sizing: border-box; }

/* The `hidden` attribute must actually hide.
 *
 * The browser's own rule is `[hidden] { display: none }`, which any class
 * carrying a `display` beats on specificity — so `element.hidden = true` on
 * something styled `display: flex` or `display: grid` does nothing at all, and
 * the JavaScript looks correct while the element sits there in plain sight.
 * WhenTaken shipped with the naming games' guess box still under it this way.
 *
 * This file had already worked around the same trap seven times, one selector
 * at a time (`.notable[hidden]`, `.lightbox[hidden]`, `.playback[hidden]` and
 * the rest). One rule instead. It is `!important` because it has to outrank
 * every layout class in the file, which is the entire point of it. */
[hidden] { display: none !important; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--bg);
  color: var(--text);
  font: 15px/1.45 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
}

/* The map app is a fixed full-screen surface that must never scroll in any
 * direction. `position: fixed` on the body (rather than just overflow:hidden)
 * is what actually holds on iOS, where rubber-band scrolling otherwise drags
 * the whole page away from under the map.
 *
 * The html rule matters too: the detail sheet is parked off-screen with a
 * transform, and a transform reaching past the viewport creates real
 * scrollable overflow on the root element even when the body is clipped.
 * Without this the page scrolled ~350px sideways. */
html:has(body.app) { overflow: hidden; }

body.app {
  position: fixed;
  inset: 0;
  /* dvh tracks the *dynamic* viewport, so the layout doesn't jump when mobile
     Safari's toolbars collapse mid-scroll the way 100vh does. */
  height: 100dvh;
  overflow: hidden;
  overscroll-behavior: none;
}

#map { position: absolute; inset: 0; }

/* Keep MapLibre's own controls clear of the home indicator and the sheet. */
.maplibregl-ctrl-bottom-right {
  margin-bottom: calc(var(--safe-b) + 96px);
  margin-right: calc(var(--safe-r) + 8px);
}
.maplibregl-ctrl-group button { width: 40px; height: 40px; }

/* ------------------------------------------------------------- topbar --- */

.topbar {
  position: absolute;
  top: 0; left: 0; right: 0;
  z-index: 5;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: calc(var(--safe-t) + 10px) calc(var(--safe-r) + 12px) 10px calc(var(--safe-l) + 12px);
  background: linear-gradient(180deg, var(--scrim-top), var(--scrim-bottom));
  pointer-events: none;
}
.topbar > * { pointer-events: auto; }

.brand {
  display: flex;
  align-items: center;
  gap: 8px;
  font-weight: 650;
  letter-spacing: 0.02em;
}
.brand .dot {
  width: 9px; height: 9px;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 12px var(--accent);
}

.status {
  flex: 1;
  min-width: 0;
  font-size: 12px;
  color: var(--muted);
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.status[data-kind="error"] { color: var(--danger); }

.iconbtn {
  flex: 0 0 auto;
  width: var(--tap); height: var(--tap);
  border: 1px solid var(--line);
  border-radius: 50%;
  background: var(--panel);
  backdrop-filter: blur(12px);
  color: var(--muted);
  font-size: 17px;
  line-height: 1;
  cursor: pointer;
}
.iconbtn:active { background: #182236; }
.logout { display: flex; }

/* ------------------------------------------------------------- search --- */

.searchbar {
  position: absolute;
  z-index: 9;
  top: calc(var(--safe-t) + 8px);
  left: calc(var(--safe-l) + 10px);
  right: calc(var(--safe-r) + 10px);
  display: none;
}
.searchbar.is-open { display: block; }

.search-row {
  display: flex;
  gap: 8px;
  align-items: center;
  padding: 6px;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--panel-solid);
  box-shadow: 0 12px 32px var(--shadow);
}
.search-row input {
  flex: 1;
  min-width: 0;
  min-height: 38px;
  padding: 0 10px;
  border: 0;
  border-radius: 8px;
  background: var(--field);
  color: var(--text);
  /* 16px or iOS Safari zooms the whole page on focus. */
  font-size: 16px;
}
.search-row input:focus { outline: 1px solid var(--accent); }
.search-row button {
  width: 38px; height: 38px;
  border: 0; border-radius: 8px;
  background: none; color: var(--muted);
  font-size: 15px; cursor: pointer;
}

.search-results {
  display: none;
  margin-top: 6px;
  max-height: min(52dvh, 420px);
  overflow-y: auto;
  overscroll-behavior: contain;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--panel-solid);
  box-shadow: 0 12px 32px var(--shadow);
}
.search-results.is-open { display: block; }

.search-results .result {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  min-height: var(--tap);
  padding: 8px 12px;
  border: 0;
  border-bottom: 1px solid var(--raised);
  background: none;
  color: var(--text);
  font-size: 14px;
  text-align: left;
  cursor: pointer;
}
.search-results .result:last-child { border-bottom: 0; }
.search-results .result:active { background: #182236; }
.search-results .name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.search-results .meta { color: var(--muted); font-size: 12px; white-space: nowrap; }
.search-results .dot {
  width: 8px; height: 8px; border-radius: 50%; flex: 0 0 auto; background: var(--muted);
}
/* Your own things, which are not layers and should not borrow a layer's
   colour: a calendar entry is not a kind of map feature. */
.search-results .dot[data-kind="calendar"] { background: var(--accent); }
.search-results .dot[data-kind="trip"]     { background: var(--accent); opacity: 0.6; }
.search-results .dot[data-kind="aircraft"] { background: var(--aircraft); }
.search-results .dot[data-kind="vessel"]   { background: var(--vessel); }
.search-results .dot[data-kind="train"]    { background: var(--train); }
.search-results .dot[data-kind="camera"]   { background: var(--camera); }

/* ------------------------------------------------------------- legend --- */

.legend {
  position: absolute;
  z-index: 5;
  left: calc(var(--safe-l) + 12px);
  bottom: calc(var(--safe-b) + 68px);
}
.legend > button {
  /* Full tap target by default; a mouse is precise enough to warrant the
     smaller pill, so desktop shrinks it back in the min-width branch. */
  min-height: var(--tap);
  padding: 0 14px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--panel);
  backdrop-filter: blur(12px);
  color: var(--muted);
  font-size: 12px;
  cursor: pointer;
}
.legend-scale {
  display: none;
  margin-top: 6px;
  padding: 10px 12px;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--panel-solid);
}
.legend.is-open .legend-scale { display: block; }
.legend-scale .step {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 11.5px;
  color: var(--muted);
  padding: 3px 0;
  white-space: nowrap;
}
.legend-scale .step i {
  width: 12px; height: 12px;
  border-radius: 3px;
  display: inline-block;
}

/* ------------------------------------------------------------- layers --- */

.layersbtn {
  position: absolute;
  z-index: 6;
  left: calc(var(--safe-l) + 12px);
  bottom: calc(var(--safe-b) + 12px);
  min-height: var(--tap);
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 0 16px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--panel);
  backdrop-filter: blur(12px);
  color: var(--text);
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
}
.layersbtn .swatch {
  width: 9px; height: 9px; border-radius: 50%;
  background: var(--accent); box-shadow: 0 0 8px var(--accent);
}
.layersbtn .badge {
  min-width: 20px;
  padding: 1px 6px;
  border-radius: 999px;
  background: var(--raised-hover);
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
}

/* Grouped panel instead of one long scrolling strip of chips: twelve layers
   in a single row meant endless horizontal swiping on a phone. */
.layers-panel {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  z-index: 8;
  max-height: 72dvh;
  /* The panel is a column with a pinned head and a scrolling body, NOT one
     scrolling box. When the whole panel scrolled, the close button went off
     the top with the content and there was no way back out of the sheet on a
     phone without scrolling up again. */
  display: flex;
  flex-direction: column;
  overflow: hidden;
  /* Bottom padding belongs to the scrolling body, so the last row can clear
     the home indicator instead of being trapped under fixed padding. */
  padding: 6px calc(var(--safe-r) + 14px) 0 calc(var(--safe-l) + 14px);
  background: var(--panel-solid);
  border-top: 1px solid var(--line);
  border-radius: var(--radius) var(--radius) 0 0;
  box-shadow: 0 -12px 32px var(--shadow);
  transform: translateY(102%);
  transition: transform 0.24s cubic-bezier(0.32, 0.72, 0, 1);
}
.layers-panel.is-open { transform: translateY(0); }

.panel-grip {
  width: 38px; height: 4px;
  margin: 6px auto 10px;
  border-radius: 2px;
  background: var(--grip);
}
.panel-head {
  display: flex; align-items: center; justify-content: space-between;
}

/* ...and pin it only in the layers panel, which is the one that scrolls.
   `.panel-head` is shared with the space and asteroid panels, so anything
   set on it unscoped changes three pages at once. */
.layers-panel .panel-grip { flex: none; }

.layers-panel .panel-head {
  flex: none;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--line);
}
.panel-head h2 { margin: 0; font-size: 16px; font-weight: 650; }
.panel-head button {
  width: var(--tap); height: var(--tap);
  margin-right: -8px;
  border: 0; background: none; color: var(--muted);
  font-size: 16px; cursor: pointer;
}

/* The scrolling region. min-height:0 is load-bearing: without it a flex
   child refuses to shrink below its content and the body never scrolls —
   the panel just overflows its own max-height instead. */
.layers-panel .panel-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  padding-bottom: calc(var(--safe-b) + 18px);
}

.panel-group { margin-top: 14px; }

/* Group headers are disclosure buttons: 31 layers in a flat list is 1,566 px,
   under half of which fits on a docked panel and about a third on a phone.
   The h3 keeps its existing typography further down; this styles the button
   inside it. */
.grouphead {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  min-height: 34px;
  padding: 0;
  border: 0;
  background: none;
  color: var(--muted);
  font: inherit;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  text-align: left;
  cursor: pointer;
}
.grouphead .grouptitle { flex: 1; min-width: 0; }
.grouphead .chev {
  width: 0; height: 0;
  border-left: 5px solid currentColor;
  border-top: 4px solid transparent;
  border-bottom: 4px solid transparent;
  transform: rotate(90deg);
  transition: transform 0.15s ease;
}
.panel-group.is-collapsed .grouphead .chev { transform: rotate(0deg); }
.panel-group.is-collapsed .groupitems { display: none; }
/* A collapsed group still has to say whether anything inside it is showing. */
.groupcount {
  min-width: 18px;
  padding: 1px 6px;
  border-radius: 999px;
  background: rgba(90, 200, 250, 0.16);
  color: var(--accent);
  font-size: 10px;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0;
}
.groupcount:empty { display: none; }
.panel-group.has-active .grouphead { color: var(--text); }
.panel-group h3 {
  margin: 0 0 6px;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
  font-weight: 600;
}

.panel-body .row,
.toggles .row {
  display: flex;
  align-items: center;
  gap: 11px;
  width: 100%;
  min-height: var(--tap);
  padding: 0 10px;
  margin-bottom: 2px;
  border: 1px solid transparent;
  border-radius: 10px;
  background: none;
  color: var(--muted);
  font-size: 14.5px;
  text-align: left;
  cursor: pointer;
}
.panel-body .row:active,
.toggles .row:active { background: var(--raised); }
.panel-body .row .swatch,
.toggles .row .swatch {
  width: 10px; height: 10px; border-radius: 50%;
  background: var(--c, currentColor);
  opacity: 0.35;
  flex: 0 0 auto;
}
.panel-body .rowlabel,
.toggles .rowlabel { flex: 1; min-width: 0; }
.panel-body .count,
.toggles .count {
  font-size: 11.5px;
  font-variant-numeric: tabular-nums;
  opacity: 0.8;
}
.panel-body .row.is-on,
.toggles .row.is-on {
  color: var(--text);
  background: var(--raised);
  border-color: var(--line);
}
.panel-body .row.is-on .swatch,
.toggles .row.is-on .swatch { opacity: 1; box-shadow: 0 0 8px var(--c, currentColor); }

.row[data-kind="aircraft"].is-on .swatch  { --c: var(--aircraft); }
.row[data-kind="vessel"].is-on .swatch    { --c: var(--vessel); }
.row[data-kind="train"].is-on .swatch     { --c: var(--train); }
.row[data-kind="camera"].is-on .swatch    { --c: var(--camera); }
.row[data-kind="lightning"].is-on .swatch { --c: var(--lightning); }
.row[data-kind="quake"].is-on .swatch     { --c: var(--quake); }
.row[data-kind="fire"].is-on .swatch      { --c: var(--fire); }
.row[data-kind="volcano"].is-on .swatch   { --c: var(--volcano); }
.row[data-kind="animal"].is-on .swatch    { --c: var(--animal); }
.row[data-kind="sighting"].is-on .swatch  { --c: var(--sighting); }
.row[data-kind="weather"].is-on .swatch   { --c: var(--weather); }
.row[data-kind="aurora"].is-on .swatch    { --c: var(--aurora); }
.row[data-kind="railmap"].is-on .swatch   { --c: var(--railmap); }
.row[data-kind="night"].is-on .swatch     { --c: #8ab4f8; }
.row[data-kind="imagery"].is-on .swatch   { --c: #9ccc65; }
.row[data-kind="cables"].is-on .swatch    { --c: #4dd0e1; }
.row[data-kind="sondes"].is-on .swatch    { --c: var(--sonde); }
.row[data-kind="buoys"].is-on .swatch     { --c: var(--buoy); }
.row[data-kind="satellites"].is-on .swatch { --c: var(--satellite); }
.row[data-kind="metar"].is-on .swatch     { --c: var(--metar); }
.row[data-kind="disasters"].is-on .swatch { --c: var(--disaster); }
.row[data-kind="airports"].is-on .swatch  { --c: var(--airport); }
.row[data-kind="tides"].is-on .swatch     { --c: var(--tide); }
.row[data-kind="rivers"].is-on .swatch    { --c: var(--river); }
.row[data-kind="radiation"].is-on .swatch { --c: var(--radiation); }
.row[data-kind="fireballs"].is-on .swatch { --c: var(--fireball); }
.row[data-kind="places"].is-on .swatch    { --c: var(--place); }
.row[data-kind="dinner"].is-on .swatch    { --c: var(--dinner); }
.row[data-kind="airquality"].is-on .swatch { --c: var(--airquality); }

.group-wrap { display: block; margin: 2px 0 8px 34px; }
/* Without this, `display: block` overrides the hidden attribute's implicit
   `display: none` and the picker shows even when its layer is off. */
.group-wrap[hidden] { display: none; }
.groupsel {
  min-height: 38px;
  width: calc(100% - 44px);
  padding: 0 30px 0 12px;
  border: 1px solid var(--line);
  border-radius: 9px;
  background: var(--field);
  color: var(--sighting);
  font-size: 14px;
  cursor: pointer;
  appearance: none;
  background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
                    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: calc(100% - 15px) 50%, calc(100% - 10px) 50%;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
}
.groupsel:focus { outline: 2px solid var(--sighting); outline-offset: 1px; }

/* Cuisine filter, under the restaurant layer row. Same indent as the group
   pickers so it reads as belonging to its layer rather than to the panel. */
.cuisine-wrap { display: block; margin: 2px 0 10px 34px; padding-right: 8px; }
/* As above: `display: block` would otherwise beat the hidden attribute's
   implicit `display: none` and the filter would show with its layer off. */
.cuisine-wrap[hidden] { display: none; }

.cuisine-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  margin-bottom: 6px;
}
/* The denominator. Above the chips deliberately — a caveat read after the
   answer has been believed is not much of a caveat. */
.cuisine-note { font-size: 12px; color: var(--muted); line-height: 1.35; }
.cuisine-clear {
  flex: none;
  min-height: 24px;
  padding: 0 8px;
  border: 1px solid var(--line);
  border-radius: 7px;
  background: transparent;
  color: var(--muted);
  font-size: 12px;
  cursor: pointer;
}
.cuisine-clear:disabled { opacity: 0.4; cursor: default; }
.cuisine-clear:not(:disabled):hover { color: var(--fg); border-color: var(--dinner); }

.cuisine-list { display: flex; flex-wrap: wrap; gap: 6px; }
.cuisine-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  /* Below the 44px tap target used elsewhere, and deliberately: these are
     secondary controls inside a scrolling panel, and at full size twenty of
     them would be taller than the panel they sit in. 30px stays comfortably
     above the 24px minimum for a non-primary control. */
  min-height: 30px;
  padding: 0 9px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: transparent;
  color: var(--muted);
  font-size: 12px;
  cursor: pointer;
}
.cuisine-chip:hover { color: var(--fg); }
.cuisine-chip[aria-checked="true"] {
  color: var(--on-accent);
  background: var(--dinner);
  border-color: var(--dinner);
  font-weight: 600;
}
.cuisine-chip:focus-visible { outline: 2px solid var(--dinner); outline-offset: 1px; }
.cuisine-count { opacity: 0.75; font-variant-numeric: tabular-nums; }
.cuisine-chip[aria-checked="true"] .cuisine-count { opacity: 0.85; }

.sr-only {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}

/* -------------------------------------------------------------- sheet --- */

.sheet {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  z-index: 7;
  background: var(--panel-solid);
  border-top: 1px solid var(--line);
  border-radius: var(--radius) var(--radius) 0 0;
  padding: 6px calc(var(--safe-r) + 16px) calc(var(--safe-b) + 76px) calc(var(--safe-l) + 16px);
  max-height: 62dvh;
  /* A flex column with only the body scrolling, rather than the whole sheet.
     Sticky was tried first and is the wrong tool: the sheet is only a scroll
     container when its content overflows, so the grip escaped to the top of
     the viewport whenever it did not. This way the grip and heading are
     simply always there. */
  display: flex;
  flex-direction: column;
  overflow: hidden;
  /* Parked off-screen rather than display:none so it animates in, and
     transform keeps the animation on the compositor. */
  transform: translateY(102%);
  transition: transform 0.24s cubic-bezier(0.32, 0.72, 0, 1);
  box-shadow: 0 -12px 32px var(--shadow);
}
.sheet.is-open { transform: translateY(0); }

/* The grip and the heading stay put while the body scrolls.
 *
 * Both used to sit inside the scroll area, so opening anything with a long
 * panel scrolled the close button off the top — and on a phone the sheet
 * covers 62% of the screen, leaving little map to tap instead. There was no
 * way back to the map without scrolling up first. */
.sheet-grip {
  flex: 0 0 auto;
  display: block;
  width: 100%;
  padding: 8px 0 4px;
  border: 0;
  background: var(--panel-solid);
  cursor: grab;
  touch-action: none;
}
.sheet-grip::before {
  content: "";
  display: block;
  width: 38px; height: 4px;
  margin: 0 auto;
  border-radius: 2px;
  background: var(--grip);
}
.sheet-grip:hover::before { background: var(--grip-hover); }
.sheet-grip:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.sheet.is-dragging { transition: none; }

.sheet-head {
  flex: 0 0 auto;
  padding-bottom: 6px;
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
}
.sheet-head h2 { margin: 0; font-size: 17px; font-weight: 650; }
.sheet-actions { display: flex; align-items: center; gap: 4px; flex: 0 0 auto; }
.sheet-head button {
  min-width: var(--tap); height: var(--tap);
  border: 0; background: none;
  color: var(--muted);
  font-size: 16px;
  cursor: pointer;
}
#sheet-close { margin-right: -8px; }

.followbtn {
  min-width: auto !important;
  height: 32px !important;
  padding: 0 12px;
  border: 1px solid var(--line) !important;
  border-radius: 999px;
  font-size: 12px !important;
  font-weight: 600;
  white-space: nowrap;
}
.followbtn.is-on {
  color: var(--accent) !important;
  border-color: color-mix(in srgb, var(--accent) 50%, transparent) !important;
  background: color-mix(in srgb, var(--accent) 12%, transparent) !important;
}

.kind {
  display: inline-block;
  margin-top: 3px;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--muted);
}
.kind[data-kind="aircraft"]  { color: var(--aircraft); }
.kind[data-kind="stays"]     { color: var(--stays); }
.kind[data-kind="culture"]   { color: var(--culture); }
.kind[data-kind="vessel"]    { color: var(--vessel); }
.kind[data-kind="train"]     { color: var(--train); }
.kind[data-kind="lightning"] { color: var(--lightning); }
.kind[data-kind="weather"]   { color: var(--weather); }
.kind[data-kind="quake"]     { color: var(--quake); }
.kind[data-kind="camera"]    { color: var(--camera); }
.kind[data-kind="fire"]      { color: var(--fire); }
.kind[data-kind="volcano"]   { color: var(--volcano); }
.kind[data-kind="bear"]      { color: var(--animal); }
.kind[data-kind="sighting"]  { color: var(--sighting); }
.kind[data-kind="sonde"]     { color: var(--sonde); }
.kind[data-kind="buoy"]      { color: var(--buoy); }
.kind[data-kind="satellite"] { color: var(--satellite); }
.kind[data-kind="metar"]     { color: var(--metar); }
.kind[data-kind="disaster"]  { color: var(--disaster); }
.kind[data-kind="airport"]   { color: var(--airport); }
.kind[data-kind="tide"]      { color: var(--tide); }
.kind[data-kind="river"]     { color: var(--river); }
.kind[data-kind="radiation"] { color: var(--radiation); }
.kind[data-kind="fireball"]  { color: var(--fireball); }
.kind[data-kind="place"]     { color: var(--place); }
.kind[data-kind="airquality"] { color: var(--airquality); }

/* Camera snapshot: 16:9 box reserved up front so the sheet doesn't jump
   height each time a new frame loads. */
.snapshot {
  position: relative;
  margin-top: 14px;
  aspect-ratio: 16 / 9;
  border-radius: 10px;
  overflow: hidden;
  background: var(--field);
  border: 1px solid var(--line);
}
/* -------------------------------------------------------------- notable --- */

.notable {
  position: absolute;
  z-index: 9;
  top: calc(var(--safe-t) + 62px);
  right: calc(var(--safe-r) + 12px);
  width: min(340px, calc(100vw - 24px));
  max-height: 60dvh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  padding: 8px 12px 0;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel-solid);
  box-shadow: 0 12px 32px var(--shadow);
}
.notable[hidden] { display: none; }
.notable .panel-head { flex: none; padding-bottom: 8px; border-bottom: 1px solid var(--line); }

.notable-list {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding-bottom: 10px;
}
.notable-item {
  display: flex;
  align-items: baseline;
  gap: 8px;
  width: 100%;
  min-height: var(--tap);
  padding: 8px 4px;
  border: 0;
  border-bottom: 1px solid var(--raised);
  background: none;
  color: var(--text);
  font: inherit;
  font-size: 13.5px;
  text-align: left;
  cursor: pointer;
}
.notable-item:last-child { border-bottom: 0; }
.notable-item:active { background: #182236; }
.notable-item .sev {
  flex: none;
  width: 8px; height: 8px;
  border-radius: 50%;
  background: var(--muted);
}
.notable-item[data-sev="3"] .sev { background: var(--danger); box-shadow: 0 0 8px var(--danger); }
.notable-item[data-sev="2"] .sev { background: #ff9f0a; }
.notable-item[data-sev="1"] .sev { background: var(--accent); }
.notable-item .what { flex: 1; min-width: 0; }
.notable-item .why { display: block; color: var(--muted); font-size: 12px; }
.notable-empty { padding: 14px 4px; color: var(--muted); font-size: 13px; }

/* ------------------------------------------------------------ lightbox --- */

/* A camera frame in the detail sheet is ~300 px wide, which is not enough to
   read a road sign or see what the traffic is doing. Tapping it fills the
   screen. */
.snapshot { cursor: zoom-in; }

.lightbox {
  position: fixed;
  inset: 0;
  z-index: 20;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
  padding: calc(var(--safe-t) + 12px) calc(var(--safe-r) + 12px)
           calc(var(--safe-b) + 12px) calc(var(--safe-l) + 12px);
  background: rgba(4, 8, 14, 0.94);
  backdrop-filter: blur(6px);
}
.lightbox[hidden] { display: none; }

.lightbox-inner {
  display: flex;
  align-items: center;
  justify-content: center;
  max-width: 100%;
  min-height: 0;
  flex: 1 1 auto;
}
.lightbox-inner img,
.lightbox-inner video {
  max-width: 100%;
  max-height: 100%;
  border-radius: 10px;
  /* Black in both themes: this is the letterbox behind a video frame, not a
     surface of the app, and footage reads better against it either way. */
  background: #05080d;
}

.lightbox-close {
  position: absolute;
  top: calc(var(--safe-t) + 10px);
  right: calc(var(--safe-r) + 10px);
  width: var(--tap); height: var(--tap);
  border: 1px solid var(--line);
  border-radius: 50%;
  background: var(--panel);
  color: var(--text);
  font-size: 17px;
  cursor: pointer;
}
.lightbox-caption {
  flex: none;
  max-width: 900px;
  color: var(--muted);
  font-size: 13px;
  text-align: center;
}

.snapshot video {
  display: block;
  width: 100%;
  border-radius: 10px;
  /* Black in both themes: this is the letterbox behind a video frame, not a
     surface of the app, and footage reads better against it either way. */
  background: #05080d;
}
.snapshot.is-failed video { display: none; }

.snapshot img {
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
}
.snapshot-fallback {
  position: absolute; inset: 0;
  display: none;
  place-items: center;
  color: var(--muted);
  font-size: 13px;
}
.snapshot.is-failed img { visibility: hidden; }
.snapshot.is-failed .snapshot-fallback { display: grid; }

/* The only part that scrolls, so the grip and the close button above it can
   never leave the screen — which on a phone was the whole problem: the sheet
   covers 62% of the display and the close button used to scroll away with the
   content. */
.sheet-body {
  flex: 1 1 auto;
  /* Required, not cosmetic: a flex item defaults to min-height:auto and will
     not shrink below its content, so without this the sheet grows to fit and
     the body never becomes the scroll container. */
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}

.sheet-body dl { margin: 14px 0 0; }
.sheet-body .row {
  display: flex;
  justify-content: space-between;
  gap: 16px;
  padding: 9px 0;
  border-bottom: 1px solid var(--raised);
}
.sheet-body dt { color: var(--muted); font-size: 13px; }
.sheet-body dd {
  margin: 0;
  font-variant-numeric: tabular-nums;
  text-align: right;
  font-size: 14px;
}
.sheet-body .alert {
  margin-top: 12px;
  padding: 9px 12px;
  border-radius: 9px;
  background: rgba(255, 69, 58, 0.14);
  border: 1px solid rgba(255, 69, 58, 0.4);
  color: #ffb4ae;
  font-size: 13px;
}

.sheet-body .note.mono {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 11px;
  letter-spacing: -0.01em;
  word-break: break-all;
}

.sheet-body .note {
  margin-top: 12px;
  padding: 9px 11px;
  border-radius: 9px;
  background: var(--raised);
  border: 1px solid var(--line);
  color: var(--muted);
  font-size: 12px;
  line-height: 1.45;
}

.credits {
  margin: 16px 0 0;
  font-size: 10.5px;
  line-height: 1.6;
  /* `--muted`, not the dark theme's muted grey at 65% written out as a
     literal. That could not switch — on the light theme it measured 2.59 —
     and the 65% was the wrong instinct anyway: OSM is ODbL and Overture is
     CDLA-Permissive, so this attribution is required rather than courteous,
     and it should not be the least readable text on the page. */
  color: var(--muted);
}

/* --------------------------------------------------------------- auth --- */

body.auth {
  min-height: 100dvh;
  display: grid;
  place-items: center;
  padding: calc(var(--safe-t) + 24px) 20px calc(var(--safe-b) + 24px);
  background:
    radial-gradient(1100px 620px at 50% -10%, rgba(90, 200, 250, 0.12), transparent 60%),
    var(--bg);
}

.card {
  width: min(100%, 380px);
  padding: 28px 24px 26px;
  background: var(--panel-solid);
  border: 1px solid var(--line);
  border-radius: 18px;
  box-shadow: 0 24px 60px var(--shadow);
}
.card .brand { margin-bottom: 4px; }
.card h1 { margin: 0; font-size: 22px; font-weight: 680; }
.card .sub { margin: 0 0 22px; color: var(--muted); font-size: 13.5px; }

.card label {
  display: block;
  margin: 14px 0 6px;
  font-size: 12px;
  color: var(--muted);
  letter-spacing: 0.02em;
}
.card input {
  width: 100%;
  min-height: var(--tap);
  padding: 0 13px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--field);
  color: var(--text);
  /* 16px minimum, otherwise iOS Safari zooms the page on focus. */
  font-size: 16px;
}
.card input:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px rgba(90, 200, 250, 0.16);
}

.card button {
  width: 100%;
  min-height: var(--tap);
  margin-top: 22px;
  border: 0;
  border-radius: 10px;
  background: var(--accent);
  color: var(--on-accent);
  font-size: 15px;
  font-weight: 650;
  cursor: pointer;
}
.card button:active { transform: translateY(1px); }

.linkbtn {
  display: block;
  margin-top: 18px;
  text-align: center;
  color: var(--accent);
  text-decoration: none;
  min-height: var(--tap);
  line-height: var(--tap);
}

.flash {
  margin: 4px 0 12px;
  padding: 10px 12px;
  border-radius: 10px;
  font-size: 13px;
  background: rgba(255, 69, 58, 0.13);
  border: 1px solid rgba(255, 69, 58, 0.36);
  color: #ffb4ae;
}

.noscript {
  position: absolute;
  z-index: 20;
  inset: auto 12px calc(var(--safe-b) + 12px);
  padding: 12px;
  border-radius: 10px;
  background: var(--panel-solid);
  border: 1px solid var(--line);
}

/* ------------------------------------------------------------ desktop --- */

@media (min-width: 721px) {
  .layers {
    left: 12px;
    right: auto;
    bottom: auto;
    top: calc(var(--safe-t) + 62px);
    flex-direction: column;
    align-items: flex-start;
    overflow: visible;
  }
  .searchbar {
    left: auto;
    right: calc(var(--safe-r) + 12px);
    width: 380px;
  }
  .legend { bottom: calc(var(--safe-b) + 68px); }
  /* Wide screens have room to show every layer at once, so the panel is
     docked open and the button disappears. The collapsing behaviour exists
     for narrow screens, where a dozen rows would eat the map. */
  .layersbtn { display: none; }

  .layers-panel {
    right: auto;
    left: 12px;
    top: calc(var(--safe-t) + 58px);
    bottom: 12px;
    width: 268px;
    border-radius: var(--radius);
    border: 1px solid var(--line);
    max-height: none;
    /* Docked: never translated off-screen, whatever the is-open class says. */
    transform: none !important;
    box-shadow: 0 12px 32px var(--shadow);
    padding-top: 12px;
  }
  /* Scoped to the layers panel, which is docked open at this width and so has
     nothing to close. Unscoped, `.panel-head button` hid the close button of
     every other panel that shares the class — Notable, Source health, the
     orbit controls and the close-approach list all open *over* the map here
     and none of them could be dismissed. It also hid the asteroid view's
     "by date" sort control, which is not a close button at all. */
  .layers-panel .panel-grip,
  .layers-panel .panel-head button { display: none; }
  .panel-head h2 { font-size: 13px; text-transform: uppercase; letter-spacing: 0.07em; color: var(--muted); }
  .panel-group { margin-top: 10px; }
  .panel-body .row { min-height: 34px; font-size: 13.5px; }
  .legend { bottom: calc(var(--safe-b) + 16px); left: calc(var(--safe-l) + 292px); }
  .legend > button { min-height: 32px; padding: 0 12px; }
  .sheet {
    left: auto;
    right: 12px;
    bottom: 12px;
    width: 340px;
    max-height: min(560px, 70dvh);
    border-radius: var(--radius);
    border: 1px solid var(--line);
    padding-bottom: 18px;
    transform: translateY(0) translateX(calc(100% + 24px));
  }
  .sheet.is-open { transform: translateY(0) translateX(0); }
  .sheet-grip { display: none; }
  .maplibregl-ctrl-bottom-right { margin-bottom: calc(var(--safe-b) + 12px); }
}

@media (prefers-reduced-motion: reduce) {
  .sheet { transition: none; }
}


/* ======================================================== space view === */

/* View switcher: the two views are peers, so this reads as tabs rather than
   a link back to somewhere "real". */
.viewnav {
  display: inline-flex;
  gap: 2px;
  padding: 3px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--panel);
  backdrop-filter: blur(12px);
  flex: 0 0 auto;
}
.viewnav a {
  padding: 0 14px;
  min-height: 34px;
  display: inline-flex;
  align-items: center;
  border-radius: 999px;
  color: var(--muted);
  text-decoration: none;
  font-size: 13px;
  font-weight: 600;
}
.viewnav a.is-active {
  color: var(--on-accent);
  background: var(--accent);
}

/* The fourth tab pushed the nav to its limit. Measured: "Surface Space
   Asteroids Sophia's plans" is 365px of nav, and a 390px phone offers about
   366px between the safe-area padding — one pixel of margin, and an overflow
   on the 360px handsets that are still common. The nav already wraps to its
   own row below 720px, so the fix is the label rather than the layout: the
   full name on a desktop, where there is room for it, and "Plans" on a phone,
   where there is not. Shortening beats scrolling, because a nav you have to
   scroll hides the tab you are trying to reach. */
.tab-short { display: none; }

.spacepanel {
  position: absolute;
  z-index: 6;
  left: calc(var(--safe-l) + 12px);
  top: calc(var(--safe-t) + 58px);
  bottom: 12px;
  width: 274px;
  padding: 12px 14px calc(var(--safe-b) + 14px);
  overflow-y: auto;
  overscroll-behavior: contain;
  background: var(--panel-solid);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: 0 12px 32px var(--shadow);
}
.spacepanel .panel-head h2 {
  margin: 0;
  font-size: 13px;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--muted);
}
.spacepanel .panel-head { margin-bottom: 8px; }
.spacepanel .groupsel { width: 100%; margin-bottom: 10px; color: var(--satellite); }

.spacecount {
  font-size: 12px;
  color: var(--muted);
  margin-bottom: 12px;
}
.spacecount span { color: var(--text); font-variant-numeric: tabular-nums; font-weight: 600; }

.toggles { margin-bottom: 18px; }
.toggles .row .swatch { --c: var(--satellite); }

.spacepanel .hint {
  margin: 0 0 10px;
  font-size: 11.5px;
  line-height: 1.5;
  color: var(--muted);
}

.asteroids { display: flex; flex-direction: column; gap: 4px; }
.asteroid {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 2px 8px;
  padding: 8px 10px;
  border: 1px solid var(--line);
  border-radius: 9px;
  text-decoration: none;
  color: var(--text);
  background: var(--raised);
}
.asteroid:hover { background: var(--raised-hover); }
.ast-name { font-size: 13px; font-weight: 600; }
.ast-dist {
  font-size: 12.5px;
  font-variant-numeric: tabular-nums;
  color: var(--satellite);
  text-align: right;
}
.ast-meta { grid-column: 1 / -1; font-size: 11px; color: var(--muted); }
/* Inside one lunar distance is genuinely close — worth the visual shout. */
.asteroid.is-near { border-color: color-mix(in srgb, var(--danger) 55%, transparent); }
.asteroid.is-near .ast-dist { color: var(--danger); }

body.space .sheet { z-index: 7; }

@media (max-width: 720px) {
  .spacepanel {
    top: auto;
    left: 0; right: 0; bottom: 0;
    width: auto;
    max-height: 58dvh;
    border-radius: var(--radius) var(--radius) 0 0;
    border-left: 0; border-right: 0; border-bottom: 0;
  }
  .viewnav a { padding: 0 12px; }

  /* The fifth tab, measured rather than estimated.
     
     Text measured in the browser at the real 600-weight 13px stack: Surface
     49px, Space 39, Asteroids 61, Plans 35, Games 44 — 228px of text. With the
     12px padding above, five tabs plus gaps and the pill's own border come to
     **363px**. A 390px phone offers 366 and clears it by three; a **360px
     phone offers 336 and does not**, and the symptom is the one the fourth tab
     caused: `.status` has `flex: 1` so it collapses to nothing first, then
     search and sign-out push off the right edge.

     Trimming the horizontal padding is what is left. Re-measured at six tabs
     (Surface, Space, Games, Destinations, Calendar, Trips), with the short
     labels below: 226.1px of text, so 8px padding gives a nav of **332.1px**
     against the **336px** a 360px handset offers — it fits, by under 4px.
     That margin is thinner than the difference between one system font stack
     and another, which is why the sideways scroll below is a backstop rather
     than a nicety. A seventh tab does not fit at any padding. */
  @media (max-width: 430px) {
    .viewnav a { padding: 0 8px; }
  }

  /* The sixth tab fits only with the short labels, and only just.
   *
   * Measured in the browser against the real 600-weight 13px stack rather
   * than estimated — the earlier note here predicted 386px from a tab set
   * that no longer exists (Asteroids folded into Space, Plans became Trips)
   * and was wrong in the direction that matters, calling a layout broken that
   * works. Short labels: Surface 49.4, Space 39.2, Games 43.6, Places 41.7,
   * Cal 20.5, Trips 31.7 — 226.1px, so 332.1px of nav at 8px padding against
   * 336px usable. Full labels would be 404.1px and would not.
   *
   * So the nav scrolls sideways instead of overflowing the bar. That scales to
   * a seventh tab too, and the active one is scrolled into view by app.js so
   * you are never looking at a strip that does not contain where you are. */
  .topbar .viewnav {
    overflow-x: auto;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
    justify-content: flex-start;
  }

  .topbar .viewnav::-webkit-scrollbar { display: none; }
  .viewnav a { flex: 0 0 auto; }

  /* The top bar cannot hold everything on one line on a phone.
   *
   * Reported from a real handset — no search, no sign-out, no status — and
   * reproduced at 500px: the view nav alone is 233px, and five 44px controls
   * plus their gaps are another 268, so the bar needs 525px where a 390px
   * phone offers about 366. `.status` has `flex: 1`, so it silently collapses
   * to zero first and then search and sign-out push off the right edge, which
   * is exactly the order the report described.
   *
   * Wrapping is the honest fix: nothing is hidden behind a menu and every
   * control keeps its full 44px target. `flex: 0 0 100%` rather than
   * `1 1 100%` — with shrink allowed the nav simply shrinks back onto one row
   * and nothing wraps at all. */
  .topbar { flex-wrap: wrap; row-gap: 6px; }
  .topbar .viewnav { flex: 0 0 100%; }
  /* Centring a status that shares its row with five buttons puts it in a
     different place depending on how long the text is. */
  .topbar .status { text-align: left; }
}

/* ---- space: passes ---- */

.spacepanel .panel-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  margin-top: 16px;
}
.minibtn {
  border: 1px solid var(--line);
  border-radius: 999px;
  background: none;
  color: var(--accent);
  font-size: 11px;
  padding: 4px 10px;
  cursor: pointer;
  white-space: nowrap;
}
.minibtn:disabled { opacity: 0.5; cursor: default; }

.passes { display: flex; flex-direction: column; gap: 4px; margin-bottom: 6px; }
.pass {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 2px 8px;
  padding: 7px 10px;
  border: 1px solid var(--line);
  border-radius: 9px;
  background: var(--raised);
}
.pass-time { font-size: 13px; font-weight: 600; }
.pass-el {
  font-size: 13px;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
  text-align: right;
}
.pass-meta { grid-column: 1 / -1; font-size: 11px; color: var(--muted); }
/* Above ~40 degrees is high enough to clear buildings and trees — the passes
   actually worth going outside for. */
.pass.is-good { border-color: color-mix(in srgb, var(--aurora) 50%, transparent); }
.pass.is-good .pass-el { color: var(--aurora); font-weight: 700; }

/* Globe leaves the corners empty; a subtle vignette stops it looking broken. */
body.space #map::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: radial-gradient(circle at 50% 45%, transparent 55%, rgba(3, 6, 12, 0.55) 100%);
}


/* ==================================================== asteroids view === */

body.solar #scene {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  cursor: grab;
  touch-action: none;
}
body.solar #scene:active { cursor: grabbing; }

.solarpanel {
  position: absolute;
  z-index: 6;
  left: calc(var(--safe-l) + 12px);
  top: calc(var(--safe-t) + 58px);
  bottom: 76px;
  width: 288px;
  padding: 12px 14px;
  overflow-y: auto;
  overscroll-behavior: contain;
  background: var(--panel-solid);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: 0 12px 32px var(--shadow);
}
.solarpanel .panel-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
}
.solarpanel .panel-head h2 {
  margin: 0;
  font-size: 13px;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--muted);
}
.solarpanel .hint {
  margin: 8px 0 12px;
  font-size: 11.5px;
  line-height: 1.5;
  color: var(--muted);
}
.solarpanel .asteroid {
  width: 100%;
  text-align: left;
  cursor: pointer;
  font: inherit;
}
.tag {
  display: inline-block;
  margin-left: 6px;
  padding: 1px 5px;
  border-radius: 4px;
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: 0.04em;
  vertical-align: 1px;
}
.tag.pha { background: rgba(255, 69, 58, 0.2); color: #ff8a80; }

/* Time scrubber: turns a date in a list into something you can watch. */
.scrubber {
  position: absolute;
  z-index: 6;
  left: 50%;
  transform: translateX(-50%);
  bottom: calc(var(--safe-b) + 16px);
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 12px;
  width: min(560px, calc(100vw - 40px));
  background: var(--panel);
  backdrop-filter: blur(12px);
  border: 1px solid var(--line);
  border-radius: 999px;
}
.scrubber input[type="range"] {
  flex: 1;
  min-width: 0;
  accent-color: var(--accent);
  height: var(--tap);
}
.scrubber output {
  min-width: 96px;
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  color: var(--text);
  text-align: center;
}

.scalehint {
  position: absolute;
  z-index: 5;
  right: calc(var(--safe-r) + 14px);
  bottom: calc(var(--safe-b) + 74px);
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 2px;
  font-size: 11px;
  color: var(--text);
  text-align: right;
  pointer-events: none;
}
.scalehint .muted { color: var(--muted); font-size: 10.5px; }

.detail {
  position: absolute;
  z-index: 7;
  right: calc(var(--safe-r) + 12px);
  top: calc(var(--safe-t) + 58px);
  width: 320px;
  max-height: calc(100dvh - 200px);
  overflow-y: auto;
  padding: 14px 16px;
  background: var(--panel-solid);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: 0 12px 32px var(--shadow);
}
.detail dl { margin: 12px 0 0; }
.detail .row {
  display: flex;
  justify-content: space-between;
  gap: 14px;
  padding: 8px 0;
  border-bottom: 1px solid var(--raised);
}
.detail dt { color: var(--muted); font-size: 12.5px; }
.detail dd { margin: 0; font-size: 13.5px; text-align: right; font-variant-numeric: tabular-nums; }
.detail .note {
  margin-top: 12px;
  padding: 9px 11px;
  border-radius: 9px;
  background: rgba(255, 159, 10, 0.1);
  border: 1px solid rgba(255, 159, 10, 0.3);
  color: #ffd9a0;
  font-size: 11.5px;
  line-height: 1.5;
}
.linkout {
  display: inline-block;
  margin-top: 12px;
  color: var(--accent);
  text-decoration: none;
  font-size: 12.5px;
}

/* A coordinate you can paste into a phone.
   Tokenised like everything else here — see the light-mode audit: a field
   background written as a literal cannot switch themes, and a white film to
   lift a surface is invisible on a light page. */
.mapspin {
  margin-top: 14px;
  padding-top: 12px;
  border-top: 1px solid var(--line);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
}

.mapspin-copy {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 12.5px;
  color: var(--text);
  background: var(--field);
  border: 1px solid var(--line);
  border-radius: 6px;
  padding: 6px 9px;
  cursor: pointer;
  /* Long enough to matter on a narrow sheet, so it is allowed to wrap rather
     than pushing the link off the edge. */
  max-width: 100%;
  word-break: break-all;
  text-align: left;
}

.mapspin-copy:hover { border-color: var(--accent); }

.mapspin-copy:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.mapspin .linkout { margin-top: 0; }

.viewbuttons {
  position: absolute;
  z-index: 6;
  right: calc(var(--safe-r) + 14px);
  top: calc(var(--safe-t) + 58px);
  display: flex;
  gap: 6px;
}

@media (max-width: 900px) {
  .viewbuttons { top: auto; bottom: calc(46dvh + 66px); right: 12px; }
  .solarpanel {
    top: auto;
    left: 0; right: 0; bottom: 0;
    width: auto;
    max-height: 46dvh;
    border-radius: var(--radius) var(--radius) 0 0;
  }
  .scrubber { bottom: calc(46dvh + 12px); }
  .scalehint { display: none; }
  .detail { left: 12px; right: 12px; width: auto; }
}

/* ---- news coverage in the detail sheet ---- */

.news { margin-top: 16px; }
.news h3 {
  margin: 0 0 8px;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--muted);
  font-weight: 600;
}
.news .hint { margin: 8px 0 0; font-size: 11px; color: var(--muted); line-height: 1.5; }
.article {
  display: block;
  padding: 8px 10px;
  margin-bottom: 4px;
  border: 1px solid var(--line);
  border-radius: 9px;
  background: var(--raised);
  text-decoration: none;
  color: var(--text);
}
.article:hover { background: var(--raised-hover); }
.art-title { display: block; font-size: 12.5px; line-height: 1.35; }
.art-meta { display: block; margin-top: 3px; font-size: 10.5px; color: var(--muted); }

/* ---- space weather strip ---- */

.swx {
  margin: -2px 0 6px 34px;
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
  line-height: 1.4;
}
.swx-note { display: block; font-size: 10.5px; opacity: 0.8; }
/* Kp 5 and above is a geomagnetic storm — the point at which the aurora
   overlay is worth switching on from mid-latitudes. */
.swx.is-active { color: var(--aurora); }
.swx.is-active .swx-note { color: var(--aurora); opacity: 1; }

/* ------------------------------------------------------ source health --- */

.sources-summary {
  padding: 6px 12px 10px;
  font-size: 13px;
  color: var(--muted);
  border-bottom: 1px solid var(--line);
}
.sources-summary[data-state="warn"] { color: var(--warn, #ffb84d); }

.source-row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 10px;
  padding: 7px 12px;
  border-bottom: 1px solid var(--line);
}
.source-name { flex: 1 1 auto; font-size: 13px; }
.source-meta { flex: 0 0 auto; font-size: 12px; color: var(--muted); }

/* A dot carries the state, so the row reads at a glance without relying on
   colour alone — the text beside it says the same thing. */
.source-row::before {
  content: "";
  width: 8px; height: 8px;
  border-radius: 50%;
  background: var(--muted);
  flex: 0 0 auto;
}
.source-row[data-state="ok"]::before        { background: #32d74b; }
.source-row[data-state="on demand"]::before { background: #5ac8fa; }
.source-row[data-state="empty"]::before     { background: #ffb84d; }
.source-row[data-state="findings"]::before  { background: #ff453a; }

.source-finding {
  flex: 1 1 100%;
  font-size: 12px;
  color: var(--warn, #ffb84d);
  line-height: 1.4;
}

/* ----------------------------------------------------------- nearest --- */

.nearby-head {
  margin: 14px 0 6px;
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}
.nearby-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 10px;
  width: 100%;
  padding: 7px 0;
  border: 0;
  border-bottom: 1px solid var(--line);
  background: none;
  color: var(--text);
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.nearby-row:last-child { border-bottom: 0; }
.nearby-row:hover .nearby-name { color: var(--accent); }
.nearby-name { flex: 1 1 auto; font-size: 13px; }
.nearby-meta {
  flex: 0 0 auto;
  font-size: 12px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

/* ---------------------------------------------------------- playback --- */

.playback {
  position: absolute;
  z-index: 7;
  left: 50%;
  transform: translateX(-50%);
  bottom: calc(var(--safe-b) + 12px);
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 12px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--panel);
  backdrop-filter: blur(12px);
  max-width: min(560px, calc(100vw - 32px));
}
.playback[hidden] { display: none; }
.playback input[type="range"] { flex: 1 1 220px; min-width: 120px; accent-color: var(--accent); }
.playbtn, .playback button {
  min-width: var(--tap); min-height: var(--tap);
  border: 0; background: none; color: var(--text);
  font-size: 15px; cursor: pointer;
}
.playback-clock {
  flex: 0 0 auto;
  min-width: 68px;
  font-size: 12px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  text-align: right;
}


/* ==================================== dismissable view panels === */
/*
 * The Space and Asteroids views each put a permanent panel over the scene,
 * and neither could be closed at any width. Docked to the bottom edge of a
 * phone they take 58dvh and 46dvh respectively — so half the screen was a
 * control panel with no way out, and you could not simply look at the globe.
 *
 * Collapsed is a class, never the `hidden` attribute: if the script has not
 * run the panel must still be visible. Defaulting to invisible would be a far
 * worse failure than defaulting to open.
 */
.spacepanel,
.solarpanel {
  transition: transform 0.24s cubic-bezier(0.32, 0.72, 0, 1), opacity 0.18s linear;
}

.spacepanel.is-collapsed,
.solarpanel.is-collapsed {
  /* Off to the left on a wide screen, where the panel is a side rail. The
     extra 24px clears its own box shadow, which would otherwise stay visible
     as a dark smear down the edge of the map. */
  transform: translateX(calc(-100% - 24px));
  opacity: 0;
  pointer-events: none;
}

/* The close button must not scroll away with the content.
 *
 * Both panels are scroll containers, so a control in the heading leaves the
 * top of the box as soon as you read down the list — the same bug the detail
 * sheet had, where the close button scrolled off and there was no way back to
 * the map. Sticky keeps the first heading, and therefore the way out, in
 * place. */
.spacepanel > .panel-head:first-of-type,
.solarpanel > .panel-head:first-of-type {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--panel-solid);
}

/* `.panel-head button` is 44px square for a thumb, which is right on a phone
 * and does not fit here: these are ~270px side rails on a wide screen, and
 * measured live the asteroid heading wanted 255px in a 243px box, so the flex
 * gap collapsed to 2px and "by date" ran into the close button. The full tap
 * target is restored below 900px, where the panel spans the screen and there
 * is room — which is where it actually matters. */
.spacepanel .panel-head button,
.solarpanel .panel-head button {
  width: auto;
  min-width: 28px;
  height: 28px;
}
.spacepanel .panel-head h2,
.solarpanel .panel-head h2 { min-width: 0; }

/* Let a crowded panel head wrap instead of pushing its button out of the box.
 *
 * Reported: "Use my location" escaped the side rail. Measured — the head is
 * 229px wide and its content needs 245, so `justify-content: space-between`
 * shoved the button 8px past its own container's right edge and into the
 * panel's padding. `min-width: 0` above lets the *heading* shrink, which does
 * not help when the heading is already at its intrinsic width and the button
 * carries `white-space: nowrap`.
 *
 * Wrapping keeps both labels whole: "Passes over you" and "Use my location"
 * are each worth reading in full, and truncating either to fit one line trades
 * a layout bug for a comprehension one.
 *
 * The wrapped button is left-aligned, deliberately. `margin-left: auto` was
 * the obvious way to keep it on the right and it computed **8px too wide** on
 * a single-item line — it takes the column gap into account where there is no
 * gap to take — putting the button back outside the box it had just wrapped
 * into. Left is where the eye goes next anyway, and it has no arithmetic to
 * get wrong. */
.spacepanel .panel-head,
.solarpanel .panel-head { flex-wrap: wrap; row-gap: 4px; }

@media (max-width: 900px) {
  .spacepanel .panel-head button,
  .solarpanel .panel-head button {
    min-width: var(--tap);
    height: var(--tap);
  }
}

.spacepanel .panel-head button[id$="-close"],
.solarpanel .panel-head button[id$="-close"] {
  flex: 0 0 auto;
  border: 0;
  background: none;
  color: var(--muted);
  font-size: 15px;
  line-height: 1;
  padding: 2px 4px;
  cursor: pointer;
}
.spacepanel .panel-head button[id$="-close"]:hover,
.solarpanel .panel-head button[id$="-close"]:hover { color: var(--text); }

@media (max-width: 900px) {
  /* Docked to the bottom edge here, so it must leave downwards. */
  .solarpanel.is-collapsed { transform: translateY(102%); }
  /* The scrubber and camera buttons are positioned above the panel, so they
     have to come down with it or they float in the middle of the scene. */
  body.panel-collapsed .scrubber { bottom: calc(var(--safe-b) + 12px); }
  body.panel-collapsed .viewbuttons { top: auto; bottom: calc(var(--safe-b) + 66px); }
}

@media (max-width: 720px) {
  .spacepanel.is-collapsed { transform: translateY(102%); }
}

@media (prefers-reduced-motion: reduce) {
  .spacepanel, .solarpanel { transition: none; }
}


/* ---- saved ice cream shops ---- */
/*
 * No catalogue records flavours or ratings, so a note you write is the only
 * place the word "mango" is ever going to exist in this app.
 */
.fav {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin-top: 14px;
}
.favhead {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  flex: 1 1 100%;
}
.favtitle {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--muted);
  font-weight: 600;
}
.favforget {
  border: 0;
  background: none;
  color: var(--muted);
  font-size: 11px;
  text-decoration: underline;
  cursor: pointer;
  padding: 4px 0;
}
.favforget:hover { color: var(--danger); }

.favbtn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 34px;
  padding: 0 12px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: none;
  color: var(--muted);
  font-size: 12.5px;
  cursor: pointer;
}
.favbtn .star { font-size: 13px; color: var(--muted); }
.favbtn.is-on { border-color: var(--accent); color: var(--text); }
.favbtn.is-on .star { color: var(--accent); }
.favbtn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.favnote {
  flex: 1 1 100%;
  min-width: 0;
  min-height: 34px;
  padding: 6px 10px;
  border: 1px solid var(--line);
  border-radius: 9px;
  background: var(--raised);
  color: var(--text);
  font: inherit;
  font-size: 13px;
}
.favnote::placeholder { color: var(--muted); }
.favnote:focus-visible { outline: 2px solid var(--accent); outline-offset: -1px; }
.search-results .star { color: var(--accent); }


/* ---- a shop's own flavour list ---- */
.menu { margin-top: 14px; }
.menu h3 {
  margin: 0 0 8px;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--muted);
  font-weight: 600;
}
.flavours { display: flex; flex-wrap: wrap; gap: 5px; }
.flavour {
  display: inline-flex;
  flex-direction: column;
  padding: 3px 9px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--raised);
  font-size: 12px;
}
.menu .hint { margin: 9px 0 0; font-size: 11px; color: var(--muted); line-height: 1.45; }


/* Attribution sits in the Source health panel and the orbit panel now, not
   under every detail sheet where it was repeated for each shop, aircraft and
   quake anyone opened. It is required for several sources rather than
   courteous, so it is moved rather than dropped. */
/* Folded away, and pinned to its own height.
   As a plain paragraph it was a fourth flex child of a column whose list is
   the `flex: 1 1 auto` scroller — so its natural height squeezed the source
   list to almost nothing and broke the panel. */
#sources .credits-fold {
  flex: 0 0 auto;
  border-top: 1px solid var(--line);
}
#sources .credits-fold summary {
  cursor: pointer;
  padding: 9px 0;
  font-size: 11px;
  color: var(--muted);
}
#sources .credits-fold[open] { max-height: 34dvh; overflow-y: auto; }
#sources .credits { margin: 0 0 10px; }
.spacepanel .credits {
  margin: 14px 0 0;
  padding-top: 12px;
  border-top: 1px solid var(--line);
}

/* The English gloss, secondary to the name you would actually say. */
.flavour-en { font-size: 10.5px; color: var(--muted); }

/* What a search actually covered.
   Above the results, never below: a denominator read after the answer has
   already been believed is not much of a caveat. */
.result-scope {
  padding: 8px 12px;
  border-bottom: 1px solid var(--line);
  font-size: 11px;
  line-height: 1.45;
  color: var(--muted);
}

/* Your own rating. Five buttons rather than a slider: a range input gives no
   keyboard-reachable "three stars" without dragging, and reads to a screen
   reader as a bare number with no unit. */
.stars { display: flex; align-items: center; gap: 2px; }
.starbtn, .starclear {
  min-width: 32px; min-height: 32px;
  padding: 0;
  border: 0;
  background: none;
  color: var(--line);
  font-size: 19px;
  line-height: 1;
  cursor: pointer;
}
.starbtn.is-on { color: var(--accent); }
.starclear { font-size: 14px; color: var(--muted); margin-left: 4px; }
.starbtn:focus-visible, .starclear:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}
@media (max-width: 720px) {
  /* A thumb, not a mouse. */
  .starbtn, .starclear { min-width: var(--tap); min-height: var(--tap); }
}

/* A finding we derived rather than one a source reported. Marked, because the
   Notable list otherwise promises everything in it was flagged upstream. */
.notable-item .derived {
  display: block;
  margin-top: 2px;
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
}

/* ======================================================== plans view === */

/* Same shape as the space panel: docked left on a desktop, a bottom sheet on
   a phone. Closable, because a panel covering most of a handset with no way
   out is exactly how the Space and Asteroid views shipped broken. */
.planpanel {
  position: absolute;
  z-index: 6;
  left: calc(var(--safe-l) + 12px);
  top: calc(var(--safe-t) + 68px);
  width: 340px;
  max-height: min(72dvh, calc(100dvh - var(--safe-t) - var(--safe-b) - 96px));
  display: flex;
  flex-direction: column;
  gap: 9px;
  padding: 10px 14px 14px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel);
  backdrop-filter: blur(12px);
  overflow-y: auto;
  overscroll-behavior: contain;
}
/* Hidden when there is only one plan; without this the attribute loses to
   the select's own display. */
.planpanel .groupsel[hidden] { display: none; }
/* A class, never the hidden attribute: if the script has not run the panel
   must still be visible. Defaulting to invisible is the worse failure. */
.planpanel.is-collapsed {
  transform: translateX(calc(-100% - 24px));
  opacity: 0;
  pointer-events: none;
}
.planpanel { transition: transform .18s ease, opacity .18s ease; }

/* The heading scrolls away otherwise — this panel is a scroll container, and
   the close button leaving the top of the box is the bug the detail sheet
   had. */
.planpanel .panel-head { position: sticky; top: 0; background: var(--panel-solid); z-index: 1; }

.plan-sub { margin: 0; font-size: 12.5px; color: var(--muted); line-height: 1.35; }
.plan-origin { font-size: 12.5px; color: var(--muted); display: flex; align-items: center; gap: 7px; }
.plan-origin-text { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.plan-origin strong { color: var(--text); font-weight: 600; }
.plan-pin-origin {
  width: 11px; height: 11px; border-radius: 50%;
  border: 3px solid var(--text); background: var(--panel-solid); flex: 0 0 auto;
}

.plan-stops { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.plan-stop {
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--field);
  transition: border-color .15s;
}
.plan-stop.is-focused, .plan-stop:hover { border-color: var(--grip); }

/* Collapsed by default except the first, so three stops are a short list
   rather than three screens. The map is the point of the view; a panel that
   covers it is the same mistake the Space and Asteroid panels made, one step
   less severe. */
.plan-det > summary {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  cursor: pointer;
  list-style: none;
  /* A full tap target for the row that opens the detail. */
  min-height: var(--tap);
}
.plan-det > summary::-webkit-details-marker { display: none; }
.plan-det > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; border-radius: 12px; }
.plan-sum-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.plan-sum-name { font-size: 15px; font-weight: 650; }
.plan-sum-meta { font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; }
.plan-chev {
  flex: 0 0 auto; width: 8px; height: 8px;
  border-right: 2px solid var(--muted); border-bottom: 2px solid var(--muted);
  transform: rotate(45deg); transition: transform .15s;
}
.plan-det[open] .plan-chev { transform: rotate(-135deg); }

.plan-body {
  display: flex; flex-direction: column; gap: 8px;
  padding: 2px 12px 12px;
  border-top: 1px solid var(--line);
  margin-top: 2px;
  padding-top: 10px;
}

.plan-num {
  flex: 0 0 auto;
  width: 26px; height: 26px; border-radius: 50%;
  background: var(--c, var(--accent)); color: var(--on-accent);
  font-size: 14px; font-weight: 700; line-height: 26px; text-align: center;
}
.plan-kind { margin: 0; font-size: 12px; color: var(--muted); }
.plan-why { margin: 0; font-size: 13.5px; }
.plan-addr { margin: 0; font-size: 12px; color: var(--muted); }

.plan-facts { margin: 0; display: grid; grid-template-columns: 1fr 1fr; gap: 6px 12px; }
.plan-facts div { display: flex; flex-direction: column; }
.plan-facts dt { font-size: 10px; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }
.plan-facts dd { margin: 0; font-size: 12.5px; font-variant-numeric: tabular-nums; }

.plan-links { display: flex; flex-wrap: wrap; gap: 6px; }
.plan-link {
  display: inline-flex; align-items: center;
  min-height: 30px; padding: 0 10px;
  border: 1px solid var(--line); border-radius: 999px;
  color: var(--accent); text-decoration: none; font-size: 12px;
}
.plan-link:hover { border-color: var(--accent); }
.plan-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

.plan-pc h4 {
  margin: 0 0 4px; font-size: 10px; letter-spacing: .1em; text-transform: uppercase;
}
.plan-pc h4.pro { color: #7ee787; }
.plan-pc h4.con { color: #ff9f7a; }
.plan-pc ul { margin: 0; padding-left: 16px; display: flex; flex-direction: column; gap: 3px; }
.plan-pc li { font-size: 12.5px; color: var(--muted); }

.plan-note {
  margin: 0; font-size: 12px; color: var(--muted); line-height: 1.45;
  border-top: 1px solid var(--line); padding-top: 10px;
}

@media (max-width: 720px) {
  .planpanel {
    left: 0; right: 0; bottom: 0; top: auto;
    width: auto;
    /* Under half the screen. With the stops collapsed the list fits, and the
       map — which is the whole point of plotting a plan — keeps the majority
       of a phone. It still scrolls if a plan has many stops. */
    max-height: 46dvh;
    border-radius: var(--radius) var(--radius) 0 0;
    border-left: 0; border-right: 0; border-bottom: 0;
  }
  .planpanel.is-collapsed { transform: translateY(100%); }
  /* Full name to short name; see .tab-short. Measured at six tabs, this takes
     the nav from **404.1px to 332.1px** — the difference between overflowing
     a 360px handset and fitting it. The scroll stays as a backstop, because
     4px is inside the error of a different system font, and because a tab
     pushed off the edge would otherwise be unreachable. */
  .tab-full { display: none; }
  .tab-short { display: inline; }
  .topbar .viewnav { overflow-x: auto; scrollbar-width: none; }
  .topbar .viewnav::-webkit-scrollbar { display: none; }
}
@media (prefers-reduced-motion: reduce) {
  .planpanel { transition: none; }
}


/* ========================================================= our pins === */

/* The drop button, armed. It is a mode, so it has to look like one — an
   unarmed-looking button that silently changes what the next tap does is how
   you drop a pin you did not mean to. */

/* The drop-a-pin button lives inside MapLibre's own control group, so it needs
   no positioning of its own — only the glyph and the armed state. A fixed
   offset here is what covered the geolocate control on a phone. */
.pinctrl { font-size: 19px; line-height: 1; color: var(--text); }
.pinctrl.is-on { background: var(--accent); color: var(--on-accent); }
.pinctrl:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }


/* Everything in the editor saves as you type: there is no save step, for the
   reason the stars have none — one hid the controls behind it, and pressing it
   twice deleted what had been written. */
.pinedit { display: flex; flex-direction: column; gap: 10px; margin-top: 4px; }
.pininput, .pinnote {
  width: 100%;
  padding: 9px 10px;
  border: 1px solid var(--line);
  border-radius: 9px;
  background: var(--field);
  color: var(--text);
  /* 16px or iOS Safari zooms the whole page when the field takes focus. */
  font: 16px/1.4 inherit;
}
.pinnote { resize: vertical; min-height: 84px; }
.pininput:focus, .pinnote:focus { outline: 2px solid var(--accent); outline-offset: 1px; }

.tagchips { display: flex; flex-wrap: wrap; gap: 6px; }
.tagchip {
  display: inline-flex; align-items: center; gap: 7px;
  min-height: 34px; padding: 0 11px;
  border: 1px solid var(--line); border-radius: 999px;
  background: transparent; color: var(--muted);
  font-size: 13px; cursor: pointer;
}
.tagchip:hover { color: var(--text); }
.tagchip[aria-checked="true"] {
  color: var(--text);
  border-color: var(--c, var(--accent));
  background: var(--raised-hover);
}
.tagchip:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* Colour set through the CSSOM, never a style attribute: CSP drops
   'unsafe-inline' from style-src entirely. */
.tagdot {
  width: 10px; height: 10px; border-radius: 50%;
  background: var(--c, var(--accent)); flex: 0 0 auto;
}

.pinremove {
  align-self: flex-start;
  min-height: var(--tap); padding: 0 14px;
  border: 1px solid var(--line); border-radius: 10px;
  background: transparent; color: var(--danger);
  font-size: 14px; cursor: pointer;
}
.pinremove:hover { border-color: var(--danger); }

/* A place result from the geocoder, offered under its own heading so it reads
   as somewhere to go rather than something this map is tracking. */
.place-block { border-top: 1px solid var(--line); }
.statusbtn {
  min-height: 26px;
  padding: 0 9px;
  margin-left: 6px;
  border: 1px solid var(--accent);
  border-radius: 999px;
  background: transparent;
  color: var(--accent);
  font-size: 12px;
  cursor: pointer;
}
.statusbtn:hover { background: var(--accent); color: var(--on-accent); }
.statusbtn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }



/* ======================================================= the letter === */

/* Opening a message is the one thing in this app that is meant to take a
   moment. Everything else is read at a glance, which is what a map is for; a
   letter's whole character is that it was closed. */
.letter-veil {
  position: fixed;
  inset: 0;
  z-index: 40;
  display: grid;
  place-items: center;
  padding: 24px;
  background: radial-gradient(120% 90% at 50% 40%, rgba(6, 12, 20, 0.72), rgba(3, 6, 11, 0.94));
  backdrop-filter: blur(6px);
  opacity: 0;
  transition: opacity .22s ease;
}
.letter-veil.is-open { opacity: 1; }
.letter-veil[hidden] { display: none; }

.letter-stage { display: flex; flex-direction: column; align-items: center; gap: 16px; }

/* The scroll: two rollers with the sheet growing between them. Because the
   rollers are siblings in a column, the lower one is carried down by the
   sheet as it unrolls — no second animation to keep in step. */
.scroll {
  position: relative;
  width: min(440px, 86vw);
  display: flex;
  flex-direction: column;
  align-items: stretch;
  transform: translateY(26px) scale(.92);
  opacity: 0;
  transition: transform .55s cubic-bezier(.22, 1.1, .36, 1), opacity .4s ease;
}
.letter-veil.is-open .scroll { transform: none; opacity: 1; }

/* A dowel, lit from above: the highlight is what stops it reading as a bar. */
.scroll-roller {
  height: 16px;
  border-radius: 9px;
  background: linear-gradient(180deg, #8a6238 0%, #cf9a5d 34%, #7a5430 72%, #5b3d22 100%);
  box-shadow: 0 2px 8px rgba(0, 0, 0, .45), inset 0 1px 0 rgba(255, 226, 184, .55);
  flex: 0 0 auto;
  z-index: 2;
}

/* The paper. Height is animated from 0 to a value measured in JS, because CSS
   cannot animate to `auto` and a scroll that jumps open is not a scroll. */
.scroll-sheet {
  height: 0;
  overflow: hidden;
  background:
    repeating-linear-gradient(0deg, rgba(120, 90, 50, .045) 0 2px, transparent 2px 5px),
    radial-gradient(90% 70% at 50% 0%, #fbf4e2 0%, #f3e8cd 55%, #e8d9b6 100%);
  box-shadow:
    inset 0 8px 14px -10px rgba(90, 62, 28, .8),
    inset 0 -8px 14px -10px rgba(90, 62, 28, .8),
    0 18px 40px rgba(0, 0, 0, .45);
  transition: height .95s cubic-bezier(.16, .84, .3, 1);
}
.scroll.is-unfurled .scroll-sheet { height: var(--sheet-h, 240px); }
/* Measured before it is clipped: the height has to be read while the content
   is laid out, and read from a hidden element it is zero. */
.scroll.is-measuring .scroll-sheet { height: auto; visibility: hidden; }

.scroll-inner {
  padding: 26px 30px 30px;
  color: #3a2c17;
  font-family: "Iowan Old Style", "Palatino Linotype", Palatino, "Book Antiqua", Georgia, serif;
  font-size: 16.5px;
  line-height: 1.62;
}

/* The wax seal, sitting where the two halves of the paper meet. It cracks and
   the halves fall apart before the paper moves — the paper cannot open until
   the seal has. */
.wax {
  position: absolute;
  top: 8px;
  left: 50%;
  width: 46px; height: 46px;
  margin-left: -23px;
  z-index: 3;
  pointer-events: none;
}
.wax-half {
  position: absolute;
  top: 0;
  width: 23px; height: 46px;
  background: radial-gradient(70% 60% at 40% 35%, #d0533f 0%, #9e2f22 60%, #741f16 100%);
  box-shadow: inset 0 1px 2px rgba(255, 180, 160, .5);
  transition: transform .5s cubic-bezier(.3, .9, .4, 1), opacity .5s ease;
}
.wax-half.a { left: 0; border-radius: 23px 0 0 23px; }
.wax-half.b { right: 0; border-radius: 0 23px 23px 0; }
.wax.is-cracked .wax-half.a { transform: translate(-26px, 16px) rotate(-38deg); opacity: 0; }
.wax.is-cracked .wax-half.b { transform: translate(26px, 18px) rotate(41deg); opacity: 0; }

/* The writing arrives last, a line at a time, so the letter reads as written
   rather than pasted. --i is set through the CSSOM; CSP has no unsafe-inline. */
.letter-to, .letter-line, .letter-from, .letter-when {
  opacity: 0;
  transform: translateY(6px);
}
.scroll.is-unfurled .letter-to,
.scroll.is-unfurled .letter-line,
.scroll.is-unfurled .letter-from,
.scroll.is-unfurled .letter-when {
  animation: letter-in .5s ease forwards;
  animation-delay: calc(620ms + var(--i, 0) * 130ms);
}
@keyframes letter-in {
  to { opacity: 1; transform: none; }
}

.letter-to {
  margin: 0 0 14px;
  font-size: 12px;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: #8a6a3c;
}
.letter-line { margin: 0 0 12px; }
.letter-from {
  margin: 22px 0 0;
  text-align: right;
  font-size: 21px;
  /* The one place a script face earns itself: a signature. */
  font-family: "Snell Roundhand", "Apple Chancery", "Segoe Script", "Bradley Hand", cursive,
               "Iowan Old Style", Georgia, serif;
  color: #5a4322;
}
.letter-when {
  margin: 4px 0 0;
  text-align: right;
  font-size: 11.5px;
  letter-spacing: .06em;
  color: #9b8156;
}

.letter-actions { display: flex; gap: 10px; }
.letter-btn {
  min-height: var(--tap);
  padding: 0 16px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: rgba(12, 18, 28, .7);
  color: var(--text);
  font-size: 14px;
  cursor: pointer;
}
.letter-btn:hover { border-color: var(--accent); }
.letter-btn.danger { color: var(--danger); }
.letter-btn.danger:hover { border-color: var(--danger); }
.letter-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Reduced motion: the letter is simply open. Nothing here is information, so
   there is nothing to replace the animation with. */
@media (prefers-reduced-motion: reduce) {
  .scroll, .scroll-sheet, .wax-half { transition: none; }
  .scroll.is-unfurled .letter-to,
  .scroll.is-unfurled .letter-line,
  .scroll.is-unfurled .letter-from,
  .scroll.is-unfurled .letter-when { animation: none; opacity: 1; transform: none; }
}

/* Writing one: the same paper, with fields on it. */
.letterfield {
  width: 100%;
  margin-bottom: 12px;
  padding: 9px 11px;
  border: 1px solid rgba(120, 92, 48, .35);
  border-radius: 7px;
  background: rgba(255, 252, 243, .8);
  color: #3a2c17;
  /* 16px or iOS Safari zooms the page when the field takes focus. */
  font: 16px/1.5 "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif;
}
.letterfield::placeholder { color: #a08a63; }
.letterfield:focus { outline: 2px solid #b8763a; outline-offset: 1px; }
.letterarea { resize: vertical; min-height: 120px; }
.letterfield.signature {
  font-family: "Snell Roundhand", "Apple Chancery", "Segoe Script", cursive,
               "Iowan Old Style", Georgia, serif;
  font-size: 19px;
  text-align: right;
  margin-bottom: 0;
}
.letter-btn.primary { color: var(--on-accent); background: var(--accent); border-color: var(--accent); }

.bottlectrl { font-size: 17px; line-height: 1; color: var(--text); }
.bottlectrl.is-on { background: var(--accent); color: var(--on-accent); }
.bottlectrl:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/* ---- games ---- */

/* Same geometry as the plans panel: a side rail on a desktop, docked to the
   bottom of a phone, and closable in both — a panel that covers the map with
   no way out is how the Space and Asteroid views shipped broken. */
.gamepanel {
  position: absolute;
  z-index: 6;
  left: calc(var(--safe-l) + 12px);
  top: calc(var(--safe-t) + 58px);
  width: 320px;
  max-height: calc(100dvh - var(--safe-t) - var(--safe-b) - 76px);
  overflow-y: auto;
  padding: 12px 14px 14px;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--panel);
  box-shadow: 0 10px 30px var(--shadow);
  transition: transform 180ms ease, opacity 180ms ease;
}

.gamepanel.is-collapsed {
  transform: translateX(calc(-100% - 24px));
  opacity: 0;
  pointer-events: none;
}

/* The close control leaves the top of a scrolling panel as soon as you read
   down it — the same bug the detail sheet and both view panels had. */
.gamepanel .panel-head {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--panel-solid);
}

.game-who {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 4px 0 10px;
  font-size: 12px;
  color: var(--muted);
}

.game-who input {
  flex: 1;
  min-width: 0;
  min-height: 30px;
  padding: 0 8px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--bg);
  color: var(--fg);
  font: inherit;
}

.game-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; }

.game-pick {
  width: 100%;
  display: grid;
  gap: 2px;
  padding: 9px 11px;
  text-align: left;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--bg);
  color: var(--fg);
  font: inherit;
  cursor: pointer;
}

.game-pick:hover, .game-pick:focus-visible { border-color: var(--accent); }
.game-pick span { font-size: 12px; color: var(--muted); }

.game-shape {
  display: block;
  width: 100%;
  max-width: 190px;
  margin: 2px auto 8px;
  aspect-ratio: 1;
}

/* The silhouette is the game, so it gets the emphasis a photograph would. */
.game-shape path {
  fill: var(--accent);
  fill-opacity: 0.22;
  stroke: var(--accent);
  stroke-width: 1.1;
  stroke-linejoin: round;
}

.game-route { margin: 2px 0 4px; font-size: 15px; }
.game-hint, .game-left { margin: 2px 0; font-size: 12px; color: var(--muted); }
.game-error { margin: 4px 0; font-size: 12px; color: #ff9b9b; }

.game-guess { display: flex; gap: 6px; margin: 8px 0; }

.game-guess input {
  flex: 1;
  min-width: 0;
  min-height: 34px;
  padding: 0 9px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--bg);
  color: var(--fg);
  font: inherit;
}

.game-guess button {
  min-height: 34px;
  padding: 0 12px;
  border: 0;
  border-radius: 8px;
  background: var(--accent);
  color: var(--on-accent);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}

.game-guess button:disabled { opacity: 0.45; cursor: default; }

.game-guesses { list-style: none; margin: 6px 0 0; padding: 0; display: grid; gap: 4px; }

.game-guesses li {
  display: grid;
  grid-template-columns: 18px 1fr auto;
  gap: 6px;
  align-items: baseline;
  padding: 5px 7px;
  border: 1px solid var(--line);
  border-radius: 8px;
  font-size: 13px;
}

.game-guesses li.is-right { border-color: #39d98a; }
.g-n { color: var(--muted); font-size: 11px; }
.g-meta { color: var(--muted); font-size: 12px; white-space: nowrap; }
.g-clues {
  grid-column: 1 / -1;
  font-size: 11px;
  color: var(--muted);
}
.g-clues em { font-style: normal; opacity: 0.7; }

.game-verdict { margin: 8px 0 4px; font-size: 13px; }
.game-verdict.is-win { color: #39d98a; }
.game-verdict.is-loss { color: #ffb27a; }

.game-actions { display: flex; gap: 6px; margin-top: 6px; }

.game-again, .game-back {
  min-height: 32px;
  padding: 0 11px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--bg);
  color: var(--fg);
  font: inherit;
  cursor: pointer;
}

.game-board { margin-top: 12px; font-size: 12px; }
.game-board summary { cursor: pointer; color: var(--muted); }
.score-block h3 { margin: 8px 0 3px; font-size: 12px; color: var(--muted); }
.score-block ol { list-style: none; margin: 0; padding: 0; display: grid; gap: 2px; }

.score-block li {
  display: grid;
  grid-template-columns: 1fr auto auto;
  gap: 8px;
  padding: 3px 0;
  border-bottom: 1px solid var(--line);
}

.score-plays { color: var(--muted); }
.game-empty { margin: 6px 0; color: var(--muted); }

@media (max-width: 720px) {
  /* Docked to the bottom, like every other panel here — a side rail on a
     phone is a rail across the whole screen. Half the height, so the map it
     draws your guesses on is still visible while you are guessing. */
  .gamepanel {
    left: var(--safe-l);
    right: var(--safe-r);
    top: auto;
    bottom: var(--safe-b);
    width: auto;
    max-height: 58dvh;
    border-radius: 12px 12px 0 0;
  }

  .gamepanel.is-collapsed { transform: translateY(calc(100% + 24px)); }
}

/* ---- games: WhenTaken ---- */

.photo-guess { display: grid; gap: 6px; }

.photo-frame { margin: 0; }

.photo-frame img {
  display: block;
  width: 100%;
  max-height: 34dvh;
  object-fit: contain;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: #000;
}

.photo-year {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  font-size: 12px;
  color: var(--muted);
}

.photo-year output { font-size: 20px; color: var(--fg); font-variant-numeric: tabular-nums; }

.photo-guess input[type="range"] { width: 100%; accent-color: var(--accent); }

.photo-where { margin: 0; font-size: 12px; color: var(--muted); }

.photo-submit {
  min-height: 36px;
  border: 0;
  border-radius: 8px;
  background: var(--accent);
  color: var(--on-accent);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}

.photo-submit:disabled { opacity: 0.45; cursor: default; }

/* Shown at the reveal, not beside the photograph: the title and the author
   line routinely name the year, which is the question. */
.photo-credit {
  display: grid;
  gap: 1px;
  margin-top: 6px;
  padding-top: 6px;
  border-top: 1px solid var(--line);
  font-size: 11px;
  color: var(--muted);
}

.photo-credit a { color: var(--accent); }

/* Shown over the blanked basemap, so an intentionally empty map does not read
   as a broken one. */
.map-hint {
  position: absolute;
  z-index: 4;
  left: 50%;
  top: calc(var(--safe-t) + 64px);
  transform: translateX(-50%);
  margin: 0;
  padding: 5px 11px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--panel);
  color: var(--muted);
  font-size: 12px;
  pointer-events: none;
}

@media (max-width: 720px) {
  /* Below the wrapped top bar, which is two rows on a phone. */
  .map-hint { top: calc(var(--safe-t) + 104px); }
}

/* The leaderboard's day switch. */
.board-span { display: flex; gap: 4px; margin: 6px 0; }

.board-span button {
  flex: 1;
  min-height: 28px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--bg);
  color: var(--muted);
  font: inherit;
  font-size: 12px;
  cursor: pointer;
}

.board-span button.is-on {
  border-color: var(--accent);
  color: var(--fg);
}

.board-note { margin: 6px 0 0; font-size: 11px; color: var(--muted); }

/* ---- bathing water: eleven seasons at a glance ---- */

/* A row of eleven blocks, oldest left. One label says "Excellent"; this says
   whether it has always been, which is the question a swimmer is asking. */
.bathe-history {
  display: flex;
  gap: 2px;
  margin: 8px 0 2px;
}

.bathe-year {
  flex: 1;
  height: 14px;
  border-radius: 2px;
  background: var(--line);
}

/* Matched to the icon colours, so the pin on the map and the strip in the
   panel are obviously the same scale. */
.bathe-year[data-q="Excellent"] { background: #3fc9a5; }
.bathe-year[data-q="Good"] { background: #7ee787; }
.bathe-year[data-q="Sufficient"] { background: #f2c14e; }
.bathe-year[data-q="Poor"] { background: #ff7a7a; }
/* Not a grade — an absence of evidence — so it is deliberately outside the
   scale's hues rather than the worst colour on it. */
.bathe-year[data-q="Not classified"] { background: #46586a; }

.bathe-scale {
  display: flex;
  justify-content: space-between;
  font-size: 11px;
  color: var(--muted);
}

/* ---- WhenTaken: looking closer ---- */

/* The in-panel photograph is a button, because expanding it is a real action
   and has to be reachable from the keyboard. */
.photo-frame {
  display: block;
  width: 100%;
  padding: 0;
  border: 0;
  background: none;
  position: relative;
  cursor: zoom-in;
}

.photo-zoom {
  position: absolute;
  right: 6px;
  bottom: 6px;
  padding: 3px 8px;
  border-radius: 999px;
  background: rgba(2, 17, 28, 0.78);
  color: var(--fg);
  font-size: 11px;
  pointer-events: none;
}

.photo-lightbox {
  position: fixed;
  inset: 0;
  z-index: 40;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: max(12px, var(--safe-t)) 12px max(12px, var(--safe-b));
  background: rgba(2, 12, 20, 0.94);
}

.photo-lightbox img {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  border-radius: 6px;
}

.photo-lightbox-close {
  position: absolute;
  top: max(10px, var(--safe-t));
  right: 12px;
  width: var(--tap);
  height: var(--tap);
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--panel);
  color: var(--fg);
  font-size: 16px;
  cursor: pointer;
}

/* ---- layer filter ---- */

.layer-filter {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 0 14px 8px;
}

.layer-filter input {
  flex: 1;
  min-width: 0;
  min-height: 32px;
  padding: 0 10px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--bg);
  color: var(--fg);
  font: inherit;
  font-size: 13px;
}

.layer-filter input:focus-visible { border-color: var(--accent); outline: none; }

.layer-filter-count {
  font-size: 11px;
  color: var(--muted);
  white-space: nowrap;
}

/* The healthy majority, folded. See renderSourceHealth: 40 rows is four and a
   half screens of "OK" in a panel whose purpose is finding a problem. */
.sources-fold { margin-top: 6px; }

.sources-fold > summary {
  cursor: pointer;
  padding: 6px 0;
  color: var(--muted);
  font-size: 12px;
}

.sources-fold > summary:hover { color: var(--fg); }

/* A pass you could actually watch, versus one that merely happens. Geometry
   puts the ISS overhead every 90 minutes; almost none of those are sightings. */
.pass.is-visible { border-left: 2px solid var(--accent); }

.pass.is-unseen { opacity: 0.5; }

/* Kept rather than filtered — "no passes" and "no passes you could see" are
   different answers — so the reason has to read as an explanation, not a
   measurement. */
.pass.is-unseen .pass-meta { font-style: italic; }

/* The one-sentence answer above the pass list. */
.sky-tonight {
  margin: 4px 0 8px;
  padding: 7px 9px;
  border-left: 2px solid var(--accent);
  border-radius: 0 6px 6px 0;
  background: var(--bg);
  font-size: 12px;
  line-height: 1.45;
  color: var(--fg);
}

.sky-tonight:empty { display: none; }

/* ---- theme toggle ---- */

/* `.panel-head` is `justify-content: space-between`, so adding a second button
   put the heading left, the close button right and the *theme* button dead in
   the middle of the bar. The controls have to group on the right.

   Done by letting the *heading* take the slack, which has no arithmetic to
   get wrong: it fills whatever is left and the buttons sit at the end, and
   `min-width: 0` lets it shrink again when the heading is the thing that does
   not fit.

   Note for whoever measures this next: `.panel-head button` carries a
   deliberate `margin-right: -8px` so the controls sit flush with the panel's
   padding edge, which makes every one of them report as 8px *outside* its own
   head. That is the intended optical alignment, not an overflow — two separate
   theories about it were wrong before the computed margins were read. */
.panel-head h2 { flex: 1 1 auto; min-width: 0; }

.theme-toggle {
  flex: 0 0 auto;
  width: 30px;
  height: 30px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--bg);
  color: var(--muted);
  font-size: 14px;
  line-height: 1;
  cursor: pointer;
}

.theme-toggle:hover, .theme-toggle:focus-visible {
  color: var(--text);
  border-color: var(--accent);
}

/* The basemap is swapped with the theme, and MapLibre re-tiles from scratch
   when it is. A brief flash of empty canvas reads as a crash, so the map is
   held at reduced opacity while the new style loads rather than blanked. */
#map.is-restyling { opacity: 0.35; transition: opacity 120ms ease; }

/* ---- destinations ---- */

.destpanel {
  position: absolute;
  z-index: 6;
  left: calc(var(--safe-l) + 12px);
  top: calc(var(--safe-t) + 58px);
  width: 340px;
  max-height: calc(100dvh - var(--safe-t) - var(--safe-b) - 76px);
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  padding: 12px 14px 14px;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--panel);
  box-shadow: 0 10px 30px var(--shadow-soft);
  transition: transform 180ms ease, opacity 180ms ease;
}

.destpanel.is-collapsed {
  transform: translateX(calc(-100% - 24px));
  opacity: 0;
  pointer-events: none;
}

.destpanel .panel-head {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--panel-solid);
}

/* Adding a place is the point of the view, so it is the first control in it
   and full width — not a 30px square beside the zoom buttons. */
.dest-add {
  width: 100%;
  min-height: 40px;
  margin-bottom: 6px;
  border: 0;
  border-radius: 10px;
  background: var(--accent);
  color: var(--on-accent);
  font: inherit;
  font-weight: 600;
  font-size: 14px;
  cursor: pointer;
}

:root[data-theme="light"] .dest-add { color: #ffffff; }

.dest-hint { margin: 0 0 8px; font-size: 12px; color: var(--muted); }

/* While waiting for the click that places a pin. */
body.is-adding #map { cursor: crosshair; }

.dest-search { display: flex; gap: 6px; margin-bottom: 8px; }

.dest-search input {
  flex: 1;
  min-width: 0;
  min-height: 34px;
  padding: 0 10px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--bg);
  color: var(--text);
  font: inherit;
}

.dest-search button {
  min-width: 36px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--bg);
  color: var(--text);
  font-size: 15px;
  cursor: pointer;
}

.dest-results { display: grid; gap: 4px; margin-bottom: 8px; }

.dest-result {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 2px 8px;
  padding: 7px 9px;
  text-align: left;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--bg);
  color: var(--text);
  font: inherit;
  cursor: pointer;
}

.dest-result:hover { border-color: var(--accent); }
.dest-add-here { grid-row: 1 / 3; align-self: center; font-size: 12px; color: var(--accent); }

.dest-tags { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 8px; }

.dest-tag {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 4px 9px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--bg);
  color: var(--muted);
  font: inherit;
  font-size: 12px;
  cursor: pointer;
}

.dest-tag.is-on { color: var(--text); border-color: var(--accent); }
.dest-count { color: var(--muted); font-size: 11px; }

.dest-dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--c, var(--muted));
  flex: 0 0 auto;
}

.dest-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 4px; }

.dest-row {
  width: 100%;
  display: grid;
  grid-template-columns: 10px 1fr auto;
  gap: 3px 8px;
  padding: 8px 10px;
  text-align: left;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--bg);
  color: var(--text);
  font: inherit;
  cursor: pointer;
}

.dest-row:hover, .dest-row.is-on { border-color: var(--accent); }
.dest-row .dest-dot { margin-top: 5px; }
.dest-name { font-size: 14px; }
.dest-kind { font-size: 11px; color: var(--muted); white-space: nowrap; }
.dest-note {
  grid-column: 2 / -1;
  font-size: 12px;
  color: var(--muted);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.dest-empty { margin: 8px 0; font-size: 12px; color: var(--muted); }

/* The editor is its own panel on the right, so naming a place does not push
   the list you are working through off the screen. */
.dest-editor {
  position: absolute;
  z-index: 7;
  right: calc(var(--safe-r) + 12px);
  top: calc(var(--safe-t) + 58px);
  width: 320px;
  max-height: calc(100dvh - var(--safe-t) - var(--safe-b) - 76px);
  overflow-y: auto;
  display: grid;
  gap: 8px;
  align-content: start;
  padding: 12px 14px 14px;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--panel);
  box-shadow: 0 10px 30px var(--shadow-soft);
}

.dest-input, .dest-textarea {
  width: 100%;
  padding: 8px 10px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--bg);
  color: var(--text);
  font: inherit;
  font-size: 14px;
}

.dest-textarea { resize: vertical; }

.dest-chips { display: flex; flex-wrap: wrap; gap: 4px; }

.dest-chip {
  padding: 4px 9px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--bg);
  color: var(--muted);
  font: inherit;
  font-size: 12px;
  cursor: pointer;
}

.dest-chip.is-on {
  color: var(--text);
  border-color: var(--c, var(--accent));
  box-shadow: inset 0 0 0 1px var(--c, var(--accent));
}

.dest-remove {
  margin-top: 4px;
  min-height: 34px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--bg);
  color: var(--danger);
  font: inherit;
  cursor: pointer;
}

@media (max-width: 720px) {
  /* Both panels dock to the bottom, like every other panel here. The editor
     sits above the list because it is what you are working in. */
  .destpanel, .dest-editor {
    left: var(--safe-l);
    right: var(--safe-r);
    top: auto;
    bottom: var(--safe-b);
    width: auto;
    max-height: 62dvh;
    border-radius: 12px 12px 0 0;
  }

  .destpanel.is-collapsed { transform: translateY(calc(100% + 24px)); }
  .dest-editor { z-index: 8; max-height: 70dvh; }
}

/* ---- calendar ---- */

.calpanel {
  position: absolute;
  z-index: 6;
  left: calc(var(--safe-l) + 12px);
  top: calc(var(--safe-t) + 58px);
  width: 340px;
  max-height: calc(100dvh - var(--safe-t) - var(--safe-b) - 76px);
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  padding: 12px 14px 14px;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--panel);
  box-shadow: 0 10px 30px var(--shadow-soft);
  transition: transform 180ms ease, opacity 180ms ease;
}

.calpanel.is-collapsed {
  transform: translateX(calc(-100% - 24px));
  opacity: 0;
  pointer-events: none;
}

.calpanel .panel-head {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--panel-solid);
}

.cal-who {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 4px 0 8px;
  font-size: 12px;
  color: var(--muted);
}

.cal-who input {
  flex: 1;
  min-width: 0;
  min-height: 30px;
  padding: 0 8px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--field);
  color: var(--text);
  font: inherit;
}

.cal-nav { display: flex; align-items: center; gap: 6px; margin-bottom: 6px; }

.cal-nav button {
  min-width: 30px;
  min-height: 30px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--field);
  color: var(--text);
  font: inherit;
  cursor: pointer;
}

.cal-month { flex: 1; text-align: center; font-size: 14px; font-weight: 600; }
.cal-todaybtn { padding: 0 9px; font-size: 12px; }

.cal-grid { width: 100%; border-collapse: collapse; table-layout: fixed; }

.cal-grid th {
  padding: 2px 0;
  font-size: 11px;
  font-weight: 500;
  color: var(--muted);
}

.cal-grid td { padding: 1px; }

/* A multi-day event, drawn as a bar that joins across the days it covers.
   
   Asked for: a trip entered as the 23rd to the 25th read as three unrelated
   marks, and three counts of "1" say nothing about being one thing.
   
   The bar is square where the run continues and rounded where it stops, and
   it reaches the full width of the cell so two adjacent cells touch. A run
   crossing a Sunday draws as two bars on two rows, which is what every
   calendar does and what the eye expects — the alternative is an absolutely
   positioned overlay spanning table cells, which is a lot of machinery for
   a line. */
.cal-span {
  position: absolute;
  left: -1px;
  right: -1px;
  bottom: 3px;
  height: 3px;
  background: var(--accent);
  opacity: 0.85;
}
.cal-span.is-open { left: 3px; border-top-left-radius: 2px; border-bottom-left-radius: 2px; }
.cal-span.is-close { right: 3px; border-top-right-radius: 2px; border-bottom-right-radius: 2px; }
/* Stacked, so two overlapping trips are two lines rather than one. */
.cal-span + .cal-span { bottom: 7px; }
.cal-span + .cal-span + .cal-span { bottom: 11px; }

/* A run that continues past the last cell of a row, or begins before the
   first, keeps its square end — the bar simply leaves the table. */
.cal-day { position: relative; }

/* Full strength on the selected day. The bar is the owner's colour over the
   selection's, and three pixels at 85% of pink on bright blue is there but
   not obvious — on the one day you are most likely to be looking at. */
.cal-day.is-on .cal-span { opacity: 1; height: 4px; }

.cal-part {
  margin-left: 6px;
  padding: 0 5px;
  border-radius: 999px;
  background: var(--raised);
  font-size: 10px;
  color: var(--muted);
  white-space: nowrap;
}

.cal-day {
  width: 100%;
  aspect-ratio: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1px;
  border: 1px solid transparent;
  border-radius: 8px;
  background: none;
  color: var(--text);
  font: inherit;
  font-size: 13px;
  cursor: pointer;
}

.cal-day:hover { background: var(--raised); }
.cal-day.is-outside { color: var(--muted); opacity: 0.5; }
.cal-day.is-today { border-color: var(--accent); }
.cal-day.is-on { background: var(--accent); color: var(--on-accent); }
.cal-dots { font-size: 8px; line-height: 6px; letter-spacing: 1px; color: var(--satellite); }
.cal-day.is-on .cal-dots { color: var(--on-accent); }

.cal-add {
  width: 100%;
  min-height: 38px;
  margin: 8px 0 6px;
  border: 0;
  border-radius: 10px;
  background: var(--accent);
  color: var(--on-accent);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}

.cal-dayname { margin: 6px 0 4px; font-size: 13px; color: var(--muted); font-weight: 600; }

.cal-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 4px; }

.cal-item {
  width: 100%;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 2px 8px;
  padding: 8px 10px;
  text-align: left;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--field);
  color: var(--text);
  font: inherit;
  cursor: pointer;
}

.cal-item:hover { border-color: var(--accent); }
.cal-time { font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; }
.cal-name { font-size: 14px; }
.cal-whose, .cal-place, .cal-zone, .cal-note {
  grid-column: 2 / -1;
  font-size: 12px;
  color: var(--muted);
}
.cal-whose { color: var(--satellite); }
.cal-empty { margin: 8px 0; font-size: 12px; color: var(--muted); }

/* While waiting for the click that sets an event's place. */
body.is-picking #map { cursor: crosshair; }

.cal-editor {
  position: absolute;
  z-index: 7;
  right: calc(var(--safe-r) + 12px);
  top: calc(var(--safe-t) + 58px);
  width: 320px;
  max-height: calc(100dvh - var(--safe-t) - var(--safe-b) - 76px);
  overflow-y: auto;
  display: grid;
  gap: 8px;
  align-content: start;
  padding: 12px 14px 14px;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--panel);
  box-shadow: 0 10px 30px var(--shadow-soft);
}

.cal-input, .cal-textarea {
  width: 100%;
  padding: 8px 10px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--field);
  color: var(--text);
  font: inherit;
  font-size: 14px;
}

.cal-textarea { resize: vertical; }
.cal-check { display: flex; align-items: center; gap: 8px; font-size: 13px; }
.cal-when { display: flex; gap: 6px; }
.cal-when .cal-input { flex: 1; min-width: 0; }

.cal-placebox {
  display: grid;
  gap: 6px;
  padding: 8px;
  border: 1px dashed var(--line);
  border-radius: 10px;
}

.cal-placed { font-size: 13px; color: var(--text); }
.cal-placeacts { display: flex; gap: 6px; }

.cal-placeacts button, .cal-placesearch button {
  min-height: 30px;
  padding: 0 10px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--field);
  color: var(--text);
  font: inherit;
  font-size: 12px;
  cursor: pointer;
}

.cal-placeacts button:disabled { opacity: 0.45; cursor: default; }
.cal-placesearch { display: flex; gap: 6px; }

.cal-placesearch input {
  flex: 1;
  min-width: 0;
  min-height: 30px;
  padding: 0 9px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--field);
  color: var(--text);
  font: inherit;
  font-size: 13px;
}

.cal-placeresults { display: grid; gap: 4px; }

.cal-placeresult {
  display: grid;
  gap: 1px;
  padding: 6px 8px;
  text-align: left;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--field);
  color: var(--text);
  font: inherit;
  font-size: 13px;
  cursor: pointer;
}

.cal-placeresult:hover { border-color: var(--accent); }

.cal-delete {
  margin-top: 4px;
  min-height: 34px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--field);
  color: var(--danger);
  font: inherit;
  cursor: pointer;
}

@media (max-width: 720px) {
  .calpanel, .cal-editor {
    left: var(--safe-l);
    right: var(--safe-r);
    top: auto;
    bottom: var(--safe-b);
    width: auto;
    max-height: 66dvh;
    border-radius: 12px 12px 0 0;
  }

  .calpanel.is-collapsed { transform: translateY(calc(100% + 24px)); }
  .cal-editor { z-index: 8; max-height: 74dvh; }
}

/* Space and the solar system share one tab, and this switches between them.
   Two renderers rather than one: a globe is geographic and belongs in
   MapLibre, a heliocentric diagram does not. */
.scope-switch {
  display: flex;
  gap: 4px;
  margin: 2px 0 8px;
}

.scope-switch a {
  flex: 1;
  min-height: 30px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--field);
  color: var(--muted);
  font-size: 12px;
  text-decoration: none;
}

.scope-switch a:hover { color: var(--text); }
.scope-switch a.is-on { background: var(--accent); color: var(--on-accent); border-color: var(--accent); }

/* ---- calendar: clearer days, and a phone layout ---- */

/* Three tiny dots said "something is on" and not how much, and were nearly
   invisible at a glance. A count reads at any size. */
.cal-count {
  min-width: 15px;
  padding: 0 3px;
  border-radius: 999px;
  background: var(--who, var(--satellite));
  color: var(--on-accent);
  font-size: 9px;
  line-height: 13px;
  font-weight: 700;
}

.cal-day.is-on .cal-count { background: var(--on-accent); color: var(--accent); }

.cal-field { display: flex; align-items: center; gap: 8px; font-size: 13px; }
.cal-field > span { flex: 0 0 64px; color: var(--muted); font-size: 12px; }
.cal-field .cal-input { flex: 1; min-width: 0; }

.cal-sep {
  margin: 10px 0 2px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
  list-style: none;
}

.cal-none { list-style: none; font-size: 12px; color: var(--muted); padding: 2px 0; }
.cal-at { grid-column: 2 / -1; font-size: 12px; color: var(--muted); }
.cal-repeat { color: var(--satellite); font-size: 11px; }

@media (max-width: 720px) {
  /* A phone is held in one hand and tapped with a thumb, so the grid gets the
     full width and the cells get a real tap target. 44px is the floor the rest
     of this app uses; seven of them plus gaps is 322px, inside a 360px
     handset's usable width. */
  .cal-day { min-height: 44px; font-size: 15px; }
  .cal-count { font-size: 10px; line-height: 15px; min-width: 17px; }
  .cal-grid th { font-size: 12px; padding: 4px 0; }

  .cal-add { min-height: 46px; font-size: 15px; }
  .cal-nav button { min-width: 40px; min-height: 40px; }
  .cal-todaybtn { padding: 0 12px; }

  /* The editor covers the month rather than fighting it for the same corner:
     on a phone they are the same rectangle, and two bottom sheets at once is
     how the games panel and the photo overlay collided. */
  .cal-editor { max-height: 86dvh; }
  .cal-field > span { flex: 0 0 56px; }

  /* A date and a time side by side are each too narrow at 360px. */
  .cal-when { flex-wrap: wrap; }
  .cal-when .cal-input { flex: 1 1 130px; }
}

/* A single owner colours the number through the same property the badge
   reads, so the two cannot disagree and the selected-day rule can override
   both. */
.cal-num { color: var(--who, inherit); }

/* Whose day it is. A single owner colours the number outright; two owners get
   a gradient clipped to the glyphs, because a number cannot be two colours and
   picking one of the two people arbitrarily is worse than showing both. */
.cal-num.is-shared {
  background-clip: text;
  -webkit-background-clip: text;
  color: transparent;
  -webkit-text-fill-color: transparent;
  font-weight: 700;
}

/* The selected day is a filled pill, so an owner colour on the number would
   fight its background. The contrast matters more than the attribution there,
   and the list underneath still says whose it is. */
.cal-day.is-on .cal-num {
  color: var(--on-accent) !important;
  background-image: none !important;
  -webkit-text-fill-color: currentColor;
}

/* ---- trips ---- */

.trippanel {
  position: absolute;
  z-index: 6;
  left: calc(var(--safe-l) + 12px);
  top: calc(var(--safe-t) + 58px);
  width: 360px;
  max-height: calc(100dvh - var(--safe-t) - var(--safe-b) - 76px);
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  padding: 12px 14px 14px;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--panel);
  box-shadow: 0 10px 30px var(--shadow-soft);
  transition: transform 180ms ease, opacity 180ms ease;
}

.trippanel.is-collapsed {
  transform: translateX(calc(-100% - 24px));
  opacity: 0;
  pointer-events: none;
}

/* `--panel-solid`, not `--panel`.
   
   A sticky heading sits over its own panel's scrolling content, and `--panel`
   is `rgba(..., 0.92)` — so 8% of whatever scrolled underneath showed through
   the title. Seen in a screenshot of a long trip: a line of a stop's note was
   legible *behind* the word TRIPS, which reads as a rendering fault rather
   than as translucency.
   
   The panel being translucent over the map is deliberate and stays; the
   heading is a different surface, over the panel rather than over the map.
   `.solarpanel` already had this right, which is why the token exists. */
.trippanel .panel-head { position: sticky; top: 0; z-index: 1; background: var(--panel-solid); }

.trip-ask { display: grid; gap: 6px; margin-bottom: 8px; }

.trip-prompt {
  width: 100%;
  padding: 9px 10px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--field);
  color: var(--text);
  font: inherit;
  font-size: 14px;
  resize: vertical;
}

.trip-go {
  min-height: 42px;
  border: 0;
  border-radius: 10px;
  background: var(--accent);
  color: var(--on-accent);
  font: inherit;
  font-weight: 600;
  font-size: 15px;
  cursor: pointer;
}

.trip-go:disabled { opacity: 0.6; cursor: progress; }

/* A plan takes the better part of a minute, so the wait is narrated. */
.trip-progress { margin: 0; font-size: 12px; color: var(--muted); min-height: 1em; }
.trip-progress:empty { display: none; }
.trip-budget { margin: 0; font-size: 11px; color: var(--muted); }

/* The list scrolls rather than pushing the trip you are reading off the
   screen. It sits *above* the detail, so at 71px a row and eight trips it
   was 571px of chrome to scroll past on every interaction with the plan
   underneath it.
   
   Capped rather than folded, deliberately: folding would make switching
   trips two clicks, and switching is the one thing this list is for. Three
   and a half rows show, which reads as scrollable without a scrollbar
   having to say so. */
.trip-list {
  list-style: none;
  margin: 0 0 8px;
  padding: 0;
  display: grid;
  gap: 4px;
  max-height: 248px;
  overflow-y: auto;
  scrollbar-width: thin;
  /* `flex: 0 0 auto`, because the panel is a column flex container and an
     `overflow: auto` child is free to shrink to nothing in one — measured,
     it collapsed to 4px, which is exactly its own padding with the whole
     list scrolled out of sight. `max-height` only caps; it does not stop a
     flex item from being squeezed below its content. */
  flex: 0 0 auto;
  /* Room for the focus ring on the first and last rows, which a flush
     overflow box would clip. */
  padding: 2px;
}

.trip-row {
  width: 100%;
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 2px 8px;
  padding: 8px 10px;
  text-align: left;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--field);
  color: var(--text);
  font: inherit;
  cursor: pointer;
}

.trip-row:hover, .trip-row.is-on { border-color: var(--accent); }
.trip-name { font-size: 14px; }
.trip-meta { grid-column: 1 / -1; font-size: 12px; color: var(--muted); }

/* Draft and approved must be distinguishable at a glance: one of them has
   written to a shared calendar and the other has not. */
.trip-badge {
  align-self: start;
  padding: 1px 7px;
  border-radius: 999px;
  background: var(--raised-hover);
  color: var(--muted);
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

.trip-badge.is-approved { background: var(--go); color: var(--on-go); }

.trip-detail { border-top: 1px solid var(--line); padding-top: 8px; }
.trip-detailname { margin: 0 0 4px; font-size: 15px; }
.trip-summary { margin: 0 0 6px; font-size: 13px; color: var(--muted); line-height: 1.45; }
.trip-zone { margin: 0 0 8px; font-size: 11px; color: var(--muted); }
.trip-day h4 {
  margin: 10px 0 4px;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
}

/* A day folds. Measured first: a three-day trip was 5,009px of content in a
   608px panel — eight and a quarter screens — of which the days were 3,096.
   The same shape the health and safety panels were already folded for. */
.trip-daysum {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 2px 8px;
  margin: 10px 0 4px;
  padding: 4px 2px;
  cursor: pointer;
  border-radius: 8px;
  list-style: none;                 /* Safari keeps the marker otherwise */
}
.trip-daysum::-webkit-details-marker { display: none; }
.trip-daysum:hover { background: var(--raised); }
.trip-daysum:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.trip-dayname {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text);
  font-weight: 600;
}
/* The twisty, drawn rather than defaulted so it matches in both themes. */
.trip-dayname::before {
  content: "▸";
  display: inline-block;
  margin-right: 6px;
  color: var(--muted);
  transition: transform 120ms ease;
}
.trip-day[open] > .trip-daysum .trip-dayname::before { transform: rotate(90deg); }
@media (prefers-reduced-motion: reduce) {
  .trip-dayname::before { transition: none; }
}
.trip-daysum .trip-daymeta { margin: 0; }

.trip-stops { list-style: none; margin: 0; padding: 0; display: grid; gap: 4px; }

.trip-stop-row {
  display: grid;
  /* 44px, not 22. The first column holds a checkbox *and* the numbered
     circle — 16 + 6 + 20 = 42 — so at 22 the circle was squeezed to **5.7px
     wide**: a purple sliver jammed against the stop name, on every row, at
     every width including the desktop panel. Measured rather than noticed,
     by asking which elements had a scrollWidth wider than their clientWidth.
     44 is also `--tap`, which this control should have been all along: it is
     what includes or excludes a stop, and it was a 22x20 target. */
  grid-template-columns: var(--tap) 1fr auto;
  gap: 2px 8px;
  padding: 7px 9px;
  border: 1px solid var(--line);
  border-radius: 9px;
  cursor: pointer;
}

.trip-stop-row:hover, .trip-stop-row.is-on { border-color: var(--satellite); }

.trip-n {
  /* `flex: 0 0 20px`, not `width`, so a narrow parent can never compress it
     again — a flex item's width is a starting point and its flex-shrink is
     what decides whether it keeps it. */
  flex: 0 0 20px;
  width: 20px; height: 20px;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 50%;
  background: var(--satellite);
  color: var(--on-accent);
  font-size: 11px;
  font-weight: 700;
}

.trip-stopname { font-size: 14px; }
.trip-time { font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; }
/* How long the plan leaves you here. Inside the name cell deliberately:
   `.trip-stop-row` is a three-column grid and a fourth child would wrap into
   the first column, under the checkbox. It also reads better beside the place
   than beside the clock — the time says when, this says how long. */
.trip-dwell {
  display: inline-block;
  margin-left: 6px;
  padding: 0 6px;
  border-radius: 999px;
  background: var(--raised);
  font-size: 11px;
  color: var(--muted);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.trip-nights {
  display: inline-block;
  margin-left: 6px;
  padding: 0 6px;
  border-radius: 999px;
  background: var(--raised);
  font-size: 11px;
  color: var(--muted);
  white-space: nowrap;
}

/* Already committed to something else on this day. Warm rather than red:
   it is a thing to notice, not a thing that is wrong — the plan is fine and
   so is the appointment, and only you know which one moves. */
.trip-warn.is-clash { color: var(--warn); }
/* Muted, not warm: being after dark is a fact about the day rather than a
   mistake — plenty of things are worth seeing lit. */
.trip-warn.is-dark { color: var(--muted); }
.trip-clashflag {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 15px;
  height: 15px;
  border-radius: 50%;
  background: var(--warn);
  color: var(--on-accent);
  font-size: 10px;
  font-weight: 700;
}

/* Where you already are, rather than somewhere you are going that day. */
.trip-staying {
  padding: 3px 9px 3px 52px;
  font-size: 11.5px;
  color: var(--muted);
  font-style: italic;
}

.trip-note { grid-column: 2 / -1; font-size: 12px; color: var(--muted); line-height: 1.4; }

.trip-actions { display: grid; gap: 6px; margin-top: 12px; }

.trip-approve {
  min-height: 42px;
  border: 0;
  border-radius: 10px;
  background: var(--go);
  color: var(--on-go);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}

.trip-approve:disabled { opacity: 0.6; cursor: progress; }

/* One tap to follow the day on a phone. Below the stops, because it is what
   you reach for once you have read them. */
.trip-dayroute {
  display: inline-block;
  margin-top: 8px;
  font-size: 11.5px;
}

.trip-share { margin-top: 12px; }

.trip-copybtn {
  width: 100%;
  min-height: 34px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--raised);
  color: var(--text);
  font: inherit;
  font-size: 12.5px;
  cursor: pointer;
}

.trip-copybtn:hover { border-color: var(--accent); }

.trip-copybtn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* An approved trip that has been edited since. Said where the action is, not
   in the status line, because the fix is a button and the two belong
   together. */
.trip-unapplied {
  padding: 10px;
  border-radius: 8px;
  border: 1px solid var(--warn);
  background: var(--raised);
  display: grid;
  gap: 8px;
}

.trip-unapplied p {
  margin: 0;
  font-size: 12px;
  color: var(--text);
  line-height: 1.45;
}

.trip-delete {
  min-height: 34px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--field);
  color: var(--danger);
  font: inherit;
  cursor: pointer;
}

.trip-fineprint { margin: 6px 0 0; font-size: 11px; color: var(--muted); line-height: 1.45; }

@media (max-width: 720px) {
  .trippanel {
    left: var(--safe-l);
    right: var(--safe-r);
    top: auto;
    bottom: var(--safe-b);
    width: auto;
    max-height: 70dvh;
    border-radius: 12px 12px 0 0;
  }

  .trippanel.is-collapsed { transform: translateY(calc(100% + 24px)); }
  .trip-go, .trip-approve { min-height: 46px; font-size: 15px; }
  /* A full tap target where a thumb is actually in use. Compact above this,
     the same rule the panel heads follow — a 44px row per stop is right on a
     phone and wasteful in a 360px desktop rail. */
  .trip-tick { min-height: var(--tap); }
}

/* ---- trips: editing a plan ---- */

.trip-stop-row { position: relative; }
.trip-stop-row.is-dropped { opacity: 0.55; }
.trip-stop-row.is-dropped .trip-stopname { text-decoration: line-through; }

.trip-tick {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  cursor: pointer;
  /* The whole column, so the tap target is the width of the control rather
     than of the checkbox glyph. */
  min-width: var(--tap);
  min-height: 26px;
}

.trip-tick input {
  accent-color: var(--accent);
  flex: 0 0 auto;
  width: 16px;
  height: 16px;
  margin: 0;
}

.trip-editbtn {
  grid-column: 3;
  align-self: start;
  padding: 2px 8px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: none;
  color: var(--muted);
  font: inherit;
  font-size: 11px;
  cursor: pointer;
}

.trip-editbtn:hover { color: var(--text); border-color: var(--accent); }

.trip-stopedit {
  grid-column: 1 / -1;
  display: grid;
  gap: 6px;
  margin-top: 6px;
  padding-top: 6px;
  border-top: 1px solid var(--line);
}

.trip-stopacts { display: flex; gap: 6px; }

.trip-stopacts button {
  min-height: 30px;
  padding: 0 10px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--field);
  color: var(--text);
  font: inherit;
  font-size: 12px;
  cursor: pointer;
}

.trip-removestop { color: var(--danger) !important; margin-left: auto; }

.trip-refine {
  display: grid;
  gap: 6px;
  margin-top: 12px;
  padding-top: 10px;
  border-top: 1px solid var(--line);
}

.trip-refinego {
  min-height: 38px;
  border: 1px solid var(--accent);
  border-radius: 10px;
  background: none;
  color: var(--accent);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}

.trip-refinego:disabled { opacity: 0.6; cursor: progress; }

/* The walk between two stops. Shown between the rows it joins, because that is
   where the question is asked — two pins 400 m apart and two pins 4 km apart
   look identical at the wrong zoom. */
.trip-walk {
  list-style: none;
  margin: 1px 0 1px 26px;
  padding-left: 12px;
  border-left: 2px dotted var(--line);
  font-size: 11px;
  color: var(--muted);
}

.trip-daymeta { margin: 0 0 5px; font-size: 11px; color: var(--muted); }

/* A stop that its own recorded hours say is shut at the time planned.
   Only ever drawn for a certain `closed` — see src/opening.py for why `open`
   and `unknown` print nothing at all. */
.trip-warn {
  list-style: none;
  margin: 2px 0 2px 26px;
  padding: 5px 9px;
  border-radius: 6px;
  background: var(--raised);
  border-left: 3px solid var(--warn);
  font-size: 11.5px;
  color: var(--text);
}

/* A gap the travel will not fit into. A different colour from the hours
   warning beside it: one says a door will be locked and the other says the
   day is impossible, and they are not the same problem. */
.trip-warn.is-timeline { border-left-color: var(--bad); }

/* The coordinate, per stop and per alternative.
   Keyed on `.mapspin`, which is what `geo.pinHtml` emits. It was keyed on a
   `.trip-pin` wrapper this panel used to build itself, and moving to the
   shared helper orphaned the rule silently: a stop row is a grid, so the
   unstyled block landed in the 44px first column and the coordinate wrapped to
   **one character per line**. Nothing threw, the link was correct, and the
   checks that looked for the element and its href all passed — it was only
   wrong to look at. */
.trip-stop-row .mapspin,
.trip-altlist .mapspin {
  grid-column: 1 / -1;
  margin-top: 4px;
  padding-top: 0;
  border-top: 0;
  gap: 8px;
}

.trip-stop-row .mapspin .linkout,
.trip-altlist .mapspin .linkout { margin-top: 0; font-size: 11.5px; }

.trip-stop-row .mapspin .mapspin-copy,
.trip-altlist .mapspin .mapspin-copy { font-size: 11px; padding: 3px 6px; }

/* Folded shut on purpose: the answer to "where are we eating" should not be a
   list of six places. It is there for when the first one is closed. */
.trip-alts {
  list-style: none;
  margin: 3px 0 6px 26px;
  font-size: 11.5px;
}

.trip-alts summary {
  cursor: pointer;
  color: var(--muted);
  padding: 3px 0;
}

.trip-alts summary:hover { color: var(--text); }

.trip-altlist {
  list-style: none;
  margin: 4px 0 0;
  padding: 0 0 0 10px;
  border-left: 2px solid var(--line);
  display: grid;
  gap: 8px;
}

.trip-altname { display: block; color: var(--text); }
.trip-altmeta { display: block; color: var(--muted); font-size: 11px; }

/* Other people's published words, quoted. Never a score of our own. */
.trip-safety {
  margin: 14px 0 6px;
  padding: 12px;
  border-radius: 8px;
  background: var(--raised);
  border: 1px solid var(--line);
}

/* Level 3 and 4 are "reconsider travel" and "do not travel". Those earn a
   colour; 1 and 2 are the ordinary state of most of the world and would
   cry wolf. */
.trip-safety[data-level="3"] { border-color: var(--warn); }
.trip-safety[data-level="4"] { border-color: var(--bad); }

.trip-safety h4 {
  margin: 0 0 8px;
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
}

.trip-safety h5 {
  margin: 10px 0 4px;
  font-size: 11.5px;
  color: var(--text);
}

.trip-advisory { margin-bottom: 10px; }

.trip-advsrc { font-size: 11.5px; color: var(--text); font-weight: 600; }

.trip-advlevel {
  margin-left: 6px;
  padding: 1px 6px;
  border-radius: 999px;
  background: var(--raised-hover);
  font-size: 10.5px;
  color: var(--muted);
}

/* Which country an advisory is about, shown only on a trip that crosses a
   border — otherwise it repeats the heading directly above it. */
.trip-advwho {
  margin-left: 6px;
  font-size: 11px;
  color: var(--muted);
}

.trip-advhead { display: block; font-size: 12px; color: var(--text); margin-top: 2px; }
.trip-advalerts { margin: 4px 0 0; font-size: 11.5px; color: var(--text); }
.trip-advsum { margin: 4px 0 0; font-size: 11px; color: var(--muted); line-height: 1.45; }

.trip-advmore summary {
  cursor: pointer;
  font-size: 11px;
  color: var(--muted);
  padding: 3px 0;
}

.trip-advmore summary:hover { color: var(--text); }
.trip-advwhen { margin: 4px 0 0; font-size: 10.5px; color: var(--muted); }
.trip-advnote { margin: 6px 0 0; font-size: 11.5px; color: var(--muted); line-height: 1.45; }

.trip-hazards ul { margin: 0; padding-left: 16px; font-size: 11.5px; color: var(--text); }
.trip-hazards li { margin: 2px 0; }

.trip-advfine {
  margin: 10px 0 0;
  padding-top: 8px;
  border-top: 1px solid var(--line);
  font-size: 10.5px;
  color: var(--muted);
  line-height: 1.45;
}

.trip-kind {
  padding: 1px 6px;
  border-radius: 999px;
  background: var(--raised-hover);
  color: var(--muted);
  font-size: 10px;
  text-transform: lowercase;
}

/* A stop with no reason is worth pointing at: the plan is supposed to explain
   itself, and silence there is a gap rather than a style. */
.trip-nowhy { font-style: italic; opacity: 0.7; }

/* Back to the trip list, in the sticky panel head.
 *
 * `.panel-head button` is a `var(--tap)` square with a negative right margin,
 * which is right for a single glyph pinned to the right edge and wrong for a
 * labelled control on the left — it would crush "← All trips" into 44px and
 * pull it off the edge. Scoped by `.panel-head .trip-back` (two classes) so it
 * outranks that rule without reaching for `!important`, which in this file is
 * reserved for the `[hidden]` override.
 *
 * The heading takes the slack rather than the head relying on
 * `space-between`: with four children that would spread them evenly and leave
 * "Trips" adrift in the middle. An auto margin absorbs the free space, so
 * back+heading group left and theme+close group right — and with the back
 * control hidden the head lays out exactly as it did before. */
.trippanel .panel-head h2 { margin-right: auto; }

.panel-head .trip-back {
  width: auto;
  height: auto;
  margin-right: 8px;
  padding: 6px 8px 6px 0;
  font-size: 13px;
  font-weight: 600;
  color: var(--accent);
  white-space: nowrap;
}

.panel-head .trip-back:hover,
.panel-head .trip-back:focus-visible { color: var(--text); }

/* ------------------------------------------------------------ training view

   Seven tabs is one past what the bar was measured for — 332.1px of nav
   against the 336px a 360px handset offers, and the seventh takes it over. So
   the strip scrolls sideways rather than pushing the status text, the bests
   button and sign-out off the right edge, which is the order they collapse in
   (`.status` has flex: 1 and goes first). A scrollbar there would sit on top
   of the tabs, so it is hidden and the overflow is the only cue — which is
   why the active tab is scrolled into view on every page, in theme.js. */
.viewnav { max-width: 100%; overflow-x: auto; scrollbar-width: none; }
.viewnav::-webkit-scrollbar { display: none; }

.trainpanel {
  position: absolute;
  left: 12px; top: 64px; bottom: 12px;
  width: min(560px, calc(100vw - 24px));
  display: flex; flex-direction: column; gap: 10px;
  padding: 12px;
  overflow-y: auto;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--panel);
  backdrop-filter: blur(12px);
}
/* Over its own scrolling content, so it has to be opaque — a translucent
   heading lets a stop's note show through the word TRAINING, which reads as a
   rendering fault rather than as translucency. */
.trainpanel .panel-head {
  position: sticky; top: -12px; z-index: 2;
  margin: -12px -12px 0; padding: 12px;
  background: var(--panel-solid);
}

/* The phase takes its own line.
 *
 * It was `flex: 1` between the two arrows, which was fine until the week
 * picker joined the row: at 500px "Week 4 · Base build · about 20-24 miles"
 * was crushed into a ribbon four words tall. A sentence competing with three
 * controls for one row loses, so it stops competing. */
.tr-weeknav { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.tr-weeknav button {
  flex: 0 0 auto; width: 36px; height: 36px;
  border: 1px solid var(--line); border-radius: 8px;
  background: var(--raised); color: var(--text); cursor: pointer;
}
.tr-phase {
  flex: 1 1 100%; order: -1; margin: 0;
  text-align: center; font-size: 13px; color: var(--muted);
}
/* Sized, not greedy: `flex: 1 1 auto` let the picker eat the row and pushed
   the forward arrow onto a line of its own, so the two arrows sat on
   different rows. */
#tr-week { flex: 0 1 150px; min-width: 0; }

.tr-days { display: flex; flex-direction: column; gap: 8px; }
.tr-day {
  border: 1px solid var(--line); border-radius: 10px;
  background: var(--raised); overflow: hidden;
}
.tr-day.is-today { border-color: var(--accent); }
.tr-day > summary {
  display: flex; align-items: center; gap: 8px;
  padding: 10px 12px; cursor: pointer; list-style: none;
}
.tr-day > summary::-webkit-details-marker { display: none; }
.tr-dayname { font-weight: 650; }
.tr-daytitle { color: var(--muted); font-size: 13px; }
.tr-count { margin-left: auto; color: var(--muted); font-size: 12px; font-variant-numeric: tabular-nums; }

.tr-items { list-style: none; margin: 0; padding: 0 12px 12px; display: flex;
            flex-direction: column; gap: 8px; }
.tr-item {
  display: grid; gap: 2px;
  padding: 8px; border-radius: 8px; background: var(--field);
}
.tr-item.is-done .tr-name { text-decoration: line-through; color: var(--muted); }
.tr-tick { display: flex; align-items: center; gap: 8px; cursor: pointer; }
/* A tap target you can actually hit with a thumb. */
.tr-tick input { width: 20px; height: 20px; flex: 0 0 20px; accent-color: var(--accent); }
.tr-name { font-weight: 600; }
.tr-target { font-size: 13px; color: var(--text); }
.tr-basis { font-size: 12px; color: var(--muted); font-style: italic; margin: 0; }
.tr-note { font-size: 12px; color: var(--muted); }
.tr-actual { font-size: 12px; color: var(--accent); }
.tr-actions { display: flex; gap: 8px; align-items: center; margin-top: 4px; }
.tr-actions button {
  border: 1px solid var(--line); border-radius: 6px; background: var(--raised);
  color: var(--accent); font-size: 12px; padding: 4px 8px; cursor: pointer;
}
.tr-fixed { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em; }

.tr-editor { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 6px; }
.tr-editor label { display: flex; align-items: center; gap: 4px; font-size: 12px; color: var(--muted); }
.tr-editor input {
  width: 92px; padding: 6px 8px;
  border: 1px solid var(--line); border-radius: 6px;
  background: var(--panel-solid); color: var(--text);
}
.tr-editor input[data-f="note"] { width: 100%; }

.tr-bests {
  position: absolute; right: 12px; top: 64px; bottom: 12px;
  width: min(420px, calc(100vw - 24px));
  padding: 12px; overflow-y: auto;
  border: 1px solid var(--line); border-radius: 14px;
  background: var(--panel-solid);
}
.tr-goals { list-style: none; margin: 0 0 12px; padding: 0;
            display: flex; flex-direction: column; gap: 6px; }
.tr-goal { display: grid; grid-template-columns: 1fr auto auto; gap: 8px;
           align-items: center; font-size: 13px; }
.tr-goal.is-unknown, .tr-goalnote { color: var(--muted); }
.tr-goalnow { font-variant-numeric: tabular-nums; font-weight: 650; }
.tr-goalwant { color: var(--muted); font-size: 12px; }
.tr-goal.is-hit .tr-goalnow { color: var(--accent); }
.tr-bar { grid-column: 1 / -1; height: 4px; border-radius: 2px; background: var(--line); }
.tr-bar i { display: block; height: 100%; border-radius: 2px; background: var(--accent); }

@media (max-width: 720px) {
  .trainpanel { left: 8px; right: 8px; width: auto; top: 108px; }
  .tr-bests { left: 8px; right: 8px; width: auto; top: 108px; }
}

/* A down week says so. Fewer sets with no explanation reads as the plan
   losing its nerve, which is the opposite of what it is doing. */
.tr-easing {
  margin: 0 12px 8px; padding: 6px 8px;
  border-radius: 6px; background: var(--field);
  font-size: 12px; color: var(--accent);
}

/* The four headline PRs. A strip, not a table: four across on a desktop and
   two on a phone, because eight lines of numbers above the week is something
   you scroll past rather than read. */
.tr-headline {
  list-style: none; margin: 0; padding: 0;
  display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px;
}
.tr-pr {
  display: flex; flex-direction: column; gap: 1px;
  padding: 8px 6px; border-radius: 8px;
  background: var(--raised); border: 1px solid var(--line);
  text-align: center;
}
.tr-prlabel { font-size: 11px; color: var(--muted); }
.tr-prvalue { font-size: 15px; font-weight: 650; font-variant-numeric: tabular-nums; }
.tr-prwant { font-size: 10px; color: var(--muted); }
.tr-pr.is-hit { border-color: var(--accent); }
.tr-pr.is-hit .tr-prvalue, .tr-pr.is-hit .tr-prwant { color: var(--accent); }
.tr-pr.is-empty .tr-prvalue { color: var(--muted); }

@media (max-width: 560px) {
  .tr-headline { grid-template-columns: repeat(2, 1fr); }
}
