/* ═══════════════════════════════════════════════════════════════════════════
   flycatcherclub — World Weather redesign
   The map keeps its scientific palettes; the CHROME retheme is brand green only.
   ═══════════════════════════════════════════════════════════════════════════ */
:root {
  --bg: #070f1c;            /* page canvas, behind the framed app */
  --ocean: #0a1626;         /* MapLibre ocean (unchanged) */
  --panel-solid: #101a2a;
  --fill: rgba(255, 255, 255, 0.06);
  --fill-hover: rgba(255, 255, 255, 0.12);
  --border: rgba(255, 255, 255, 0.09);
  --border-mid: rgba(255, 255, 255, 0.12);
  --border-strong: rgba(255, 255, 255, 0.16);
  --text: #eaf1fb;
  --muted: #93a6c0;
  --muted2: #8fa8c4;
  --faint: #5b7da3;
  --ink2: #c3d2e4;

  /* Brand green — the only accent change from the old blue. */
  --accent: #3fbf85;
  --accent-hover: #7ad9ad;
  --accent-ink: #04121f;
  --accent-16: rgba(63, 191, 133, 0.16);
  --accent-13: rgba(63, 191, 133, 0.13);
  --accent-10: rgba(63, 191, 133, 0.10);

  --glass: rgba(11, 19, 33, 0.74);
  --glass-strong: rgba(11, 19, 33, 0.95);
  --shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
  --shadow-drawer: 0 -30px 80px rgba(0, 0, 0, 0.55);
  --radius: 14px;
  --radius-sm: 9px;

  /* One family for everything. Figures come from font-variant-numeric below, not
     from a monospace — see the note on the deleted font links in index.html. The
     --serif/--mono aliases that carried the older rules through the redesign are
     gone with the last rule that named one. */
  --sans: 'Helvetica Neue', Helvetica, Arial, sans-serif;

  --city-band: 40vh;   /* map band height in the city view */
  --body-left: 24px;   /* left edge for section titles and category labels */

  /* ── redesign tokens ──────────────────────────────────────────────────────
     The "no header at all" pass (design_handoff_flycatcher_chrome). These are
     added alongside the tokens above rather than replacing them: the phases land
     one surface at a time, and a surface that has not been rebuilt yet must keep
     reading its old values. Phase 9i deletes whatever is left unread.

     Data palettes are NOT here and never will be — src/lib/variables.ts stops and
     src/lib/colors.ts ramps stay the single source of truth for anything the map
     paints. These are chrome only. */
  --fc-page: #04070a;             /* frame behind the app */
  --fc-brief: #070a0d;            /* the briefing document's page */
  --fc-panel: rgba(9, 13, 17, 0.90);
  --fc-overlay: rgba(9, 13, 17, 0.95);
  --fc-blur: blur(22px);
  --fc-panel-radius: 16px;
  --fc-panel-shadow: 0 20px 50px rgba(0, 0, 0, 0.5);

  /* Scrims: the map reads through them, so the chrome needs no bar to sit on. */
  --fc-scrim-top: linear-gradient(180deg, rgba(4, 7, 10, 0.78), rgba(4, 7, 10, 0));
  --fc-scrim-bottom: linear-gradient(0deg, rgba(4, 7, 10, 0.86), rgba(4, 7, 10, 0));

  --fc-hair-row: rgba(255, 255, 255, 0.07);   /* between rows */
  --fc-hair-rule: rgba(255, 255, 255, 0.09);  /* section rules */
  --fc-hair-ruler: rgba(255, 255, 255, 0.2);  /* the ruler's baseline */
  --fc-hair-month: rgba(255, 255, 255, 0.13); /* month dividers */

  --fc-fill: rgba(255, 255, 255, 0.06);       /* quiet buttons and fields */
  --fc-fill-strong: rgba(255, 255, 255, 0.08);
  --fc-outline: rgba(255, 255, 255, 0.2);
  --fc-outline-strong: rgba(255, 255, 255, 0.28);

  --fc-text: #eef2f5;         /* primary */
  --fc-text-2: #c3cdd6;       /* secondary */
  --fc-muted: #8d9aa6;        /* muted — still ≥ 4.5:1 at 12px */
  --fc-ruler-month: #7d8b97;
  --fc-sep: #33414c;          /* the sentence's "·" separators */

  /* Verdicts. Distinct from the SCORE_STOPS ramp on purpose: that ramp colours
     data, these colour a written judgement. */
  --fc-ideal: #7ad9ad;
  --fc-marginal: #f4c05a;
  --fc-outside: #f2765c;

  --fc-accent-20: rgba(63, 191, 133, 0.20);
  --fc-accent-16: rgba(63, 191, 133, 0.16);
  --fc-accent-14: rgba(63, 191, 133, 0.14);
  --fc-accent-10: rgba(63, 191, 133, 0.10);

  --fc-gutter: 44px;          /* the map's outer gutter */
  --fc-gutter-brief: 72px;    /* the briefing's side padding */
  --fc-gutter-phone: 22px;

  /* How much map the landing block leaves below itself. Named because two things
     depend on it: the block's own bottom padding, and the aside that drops back
     THROUGH that padding to sit on the page's bottom edge. */
  --fc-landing-reserve: clamp(96px, 33vh, 300px);

  /* Twins of CHART_THEME in src/lib/city-view.ts, mirrored BY HAND. The charts
     are built as SVG strings so their unit test can read them without a browser,
     which means their colours cannot come from here — but the surfaces around
     them have to agree with them. Same convention as breakpoints.ts against the
     media queries. */
  --fc-chart-grid: #18232e;
  --fc-chart-axis: #8d9aa6;
  --fc-chart-line: #eef2f5;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  height: 100%;
  overflow: hidden;
  background: var(--bg);
  color: var(--text);
  font-family: var(--sans);
  -webkit-font-smoothing: antialiased;
  /* Every figure on the site is one a reader compares against another — a score
     against a score, a column of readings, a ruler of dekads. Lining figures do
     that job; the monospace this replaces was only ever a way to buy them. */
  font-variant-numeric: tabular-nums;
}
a { color: var(--accent); text-decoration: none; }
a:hover { color: var(--accent-hover); }

/* ── redesign primitives ────────────────────────────────────────────────────
   The two shapes every summoned surface in the redesign is made of. Declared
   here, ahead of the phases that use them (the lens menu, the peaks panel, the
   recipe editor), because all three want the identical surface and none of them
   should own it. */
.panel {
  background: var(--fc-panel);
  border: 1px solid var(--fc-hair-row);
  border-radius: var(--fc-panel-radius);
  box-shadow: var(--fc-panel-shadow);
  backdrop-filter: var(--fc-blur);
  -webkit-backdrop-filter: var(--fc-blur);
}
/* One row shape for every list in the redesign: a label, then a right-hand hint.
   Rows are baseline-aligned, not centred — the hint is smaller than the label and
   the two must sit on one reading line. */
.row {
  display: flex;
  width: 100%;
  align-items: baseline;
  gap: 14px;
  padding: 14px;
  border: 0;
  border-radius: 11px;
  background: none;
  color: var(--fc-text-2);
  font: inherit;
  font-size: 17px;
  text-align: left;
  cursor: pointer;
}
.row > span:first-child { white-space: nowrap; }
.row:hover { background: rgba(255, 255, 255, 0.05); }
.row.active { color: var(--fc-text); font-weight: 600; background: var(--fc-accent-16); }
.row-hint { margin-left: auto; padding-left: 14px; font-size: 13.5px; color: var(--fc-muted); white-space: nowrap; }

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

.glass {
  background: var(--glass);
  backdrop-filter: blur(20px) saturate(1.3);
  -webkit-backdrop-filter: blur(20px) saturate(1.3);
}

/* Scale only — the ring box is already centred on the dot by the -13px margins on
   .city-ring, so an extra translate(-50%,-50%) here would offset it up-and-left. */
@keyframes cityPulse {
  0% { transform: scale(0.6); opacity: 0.9; }
  100% { transform: scale(2.6); opacity: 0; }
}
/* The tooltip's transform is its cursor position (see positionTip), so its
   entrance can only animate opacity — a translate here would override the inline
   positioning for the animation's duration and flash the tooltip at 0,0 first. */
@keyframes tipIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes fadeUp { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: translateY(0); } }
/* The briefing rising into the space the map vacates.
   Going city → map, the map box FLIP-animates from the band back to full-bleed
   (animateMapBox in ui/layout.ts) and that reads as the briefing sliding away.
   The other direction had no counterpart: #cityview goes display:none → block,
   so the document simply appeared, already occupying its half of the screen.
   This is that missing half — same 420ms, and cubic-bezier(.215,.61,.355,1) is
   easeOutCubic, which is the curve the map box is on, so the two move together
   rather than merely at the same time. */
@keyframes briefingRise {
  from { transform: translateY(calc(100vh - var(--city-band))); }
  to { transform: translateY(0); }
}

/* ═══════════════ THE CORNER — what replaced the header ═══════════════════════
   No bar, no panel, no background. A wordmark, an eyebrow and a sentence, over
   a scrim, in the top-left corner of the map.

   The column is BOUNDED — top:30px to bottom:150px — and the menus hanging off
   it are flex children with min-height:0. That pair is the whole reason a picker
   can never grow down into the ruler: the column cannot exceed its box, so the
   menu scrolls instead. */
#corner {
  position: fixed;
  top: 30px;
  left: var(--fc-gutter);
  bottom: 150px;
  /* 46, above the ruler's 45. The menus hang off this element, and a positioned
     ancestor with a z-index is a stacking CONTEXT — a child cannot climb out of
     it, so raising #lens-menu on its own does nothing. At equal z-index the
     ruler won on DOM order and painted over an open picker. */
  z-index: 46;
  display: none;
  flex-direction: column;
  align-items: flex-start;
  gap: 22px;
  pointer-events: none; /* the map keeps the space between the pieces */
}
#corner > * { pointer-events: auto; }
body.view-map #corner, body.view-city #corner, body.view-landing #corner { display: flex; }
/* The landing keeps only the wordmark — it has its own headline and there is no
   map state to describe yet. The BRIEFING keeps the sentence: the rating and the
   date it names are what the whole page is about, and a reader comparing ratings
   for a place should not have to leave it to change one. */
body.view-landing #showing { display: none; }

/* The scrim the corner reads against. A fixed element rather than a property of
   #corner, because it has to span the full width whatever the sentence's length. */
#top-scrim {
  position: fixed;
  top: 0; left: 0; right: 0; height: 230px;
  z-index: 20;
  background: var(--fc-scrim-top);
  pointer-events: none;
  display: none;
}
body.view-map #top-scrim { display: block; }
/* The briefing needs it too — the corner sits over the map band, and without a
   scrim the basemap's own labels read through it. It stays put while the page
   scrolls beneath, so it also has to back the corner over the document. */
body.view-city #top-scrim { display: block; }

/* ── the wordmark ── */
.brand {
  position: static; /* it is a row of #corner now, not a floating fixture */
  display: flex;
  align-items: center;
  gap: 8px;
  background: transparent;
  border: none;
  padding: 0;
  cursor: pointer;
  font-family: var(--sans);
}
.brand-dot {
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--accent);
}
.brand-name { font-size: 14.5px; font-weight: 600; letter-spacing: -0.01em; color: var(--fc-text); }
.brand-tld { color: var(--accent); font-weight: 600; }
.brand:hover .brand-name { color: #fff; }
/* The landing and the briefing put the wordmark where their own layouts want it. */
body.view-landing #corner, body.view-city #corner { top: 34px; }

/* ── the sentence ── */
.sn-eyebrow {
  font-size: 12.5px; font-weight: 500; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--fc-muted);
}
/* Laid out as TEXT, not as flex items with a gap. It is a sentence, so the space
   between its words should be the font's own space at this size — a 10px flex
   gap is a number that happens to look close, and it stops looking close the
   moment the size or the family changes. Inline layout also gives baseline
   alignment for free (the words differ in weight and colour) and wraps at the
   spaces on a phone, which is what the zero-height break element was faking. */
.sentence {
  margin-top: 9px;
  font-size: 30px;
  font-weight: 400;
  letter-spacing: -0.02em;
  white-space: nowrap;
}
.sentence > * { vertical-align: baseline; }
.sn-br { display: none; }
.sn-lead { color: var(--fc-muted); }
.sn-lead[hidden] { display: none; }

/* Pulled in on both sides. Between two words there is a space, a "·" and
   another space — three things, so at a full space each the WORDS end up about
   23px apart and the sentence reads as a list rather than a line. The dot wants
   a thin space, the way a mid-sentence punctuation mark does. */
.sn-sep { color: var(--fc-sep); margin: 0 -4px; }
/* Every word is a button, and none of them looks like one. The underline is the
   only affordance: solid accent under the rating (this is the choice the map is
   making), dashed under the two time words (these are adjustable too). Reading
   as editable TEXT rather than as a toolbar is the entire point. */
.sn-word {
  font: inherit;
  font-size: 30px;
  letter-spacing: -0.02em;
  color: var(--fc-text-2);
  background: none;
  border: 0;
  border-bottom: 1px dashed rgba(255, 255, 255, 0.26);
  border-radius: 0;
  padding: 0 0 2px;
  cursor: pointer;
  transition: color 0.15s ease, border-color 0.15s ease;
}
.sn-word:hover, .sn-word.open { color: var(--fc-text); border-bottom-color: var(--accent); }
.sn-lens {
  font-weight: 500;
  color: var(--fc-text);
  border-bottom: 2px solid var(--accent);
}
/* A measurement is not a judgement: the word keeps the dashed underline the two
   time words have rather than the accent one a rating earns. */
.sn-lens-raw { font-weight: 400; color: var(--fc-text-2); border-bottom: 1px dashed rgba(255, 255, 255, 0.26); }
.sn-helper { max-width: 420px; margin-top: 10px; font-size: 14.5px; color: var(--fc-muted); }
.sn-helper[hidden] { display: none; }

/* ── the top-right words ── */
#corner-right {
  position: fixed;
  top: 34px;
  right: var(--fc-gutter);
  z-index: 45;
  display: none;
  align-items: baseline;
  gap: 26px;
}
body.view-map #corner-right, body.view-city #corner-right { display: flex; }
/* The peaks panel takes this corner while it is open. Two sets of words in the
   same 44px of screen is exactly the crowding the redesign removes, and the
   panel is the thing the reader just asked for. */
body.peaks-open #corner-right { display: none; }
.corner-word {
  font: inherit;
  font-size: 15px;
  color: var(--fc-text-2);
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  transition: color 0.15s ease;
}
.corner-word-quiet { color: var(--fc-muted); }
.corner-word:hover, .corner-word.open { color: var(--fc-text); }

/* ═══════════════ THE SUMMONED SURFACES ═══════════════════════════════════════
   One shape for all of them (.panel, declared with the tokens). Each hangs under
   the word that opens it — sentence.ts measures the offset, so a longer rating
   name moves the menu with it. */
.menu {
  width: 396px;
  max-width: calc(100vw - 2 * var(--fc-gutter));
  padding: 12px;
  /* The flex child half of the bounded-column contract above. */
  min-height: 0;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
}
.menu[hidden] { display: none; }
.menu-list { display: flex; flex-direction: column; }
.menu-rule { height: 1px; margin: 10px 14px; background: var(--fc-hair-row); }
.menu-caveat { padding: 12px 14px 4px; font-size: 13.5px; line-height: 1.5; color: var(--fc-muted); }
.menu-back {
  align-self: flex-start;
  margin: 2px 0 8px;
  padding: 6px 14px;
  font: inherit; font-size: 14.5px;
  color: var(--fc-muted);
  background: none; border: 0; border-radius: 9px;
  cursor: pointer;
}
.menu-back:hover { color: var(--fc-text-2); background: rgba(255, 255, 255, 0.05); }

/* Early / Mid / Late, above the twelve months: two decisions, kept apart. */
.seg { display: flex; gap: 4px; padding: 2px 2px 10px; }
.seg-btn {
  flex: 1;
  padding: 9px 0;
  font: inherit; font-size: 14.5px;
  color: var(--fc-muted);
  background: var(--fc-fill);
  border: 0; border-radius: 9px;
  cursor: pointer;
}
.seg-btn:hover { color: var(--fc-text-2); }
.seg-btn.active { color: var(--fc-text); font-weight: 600; background: var(--fc-accent-16); }

/* ── the Layers panel ── */
.layers-menu {
  position: fixed;
  top: 72px;
  right: var(--fc-gutter);
  z-index: 46;
  min-width: 210px;
  display: flex; flex-direction: column; gap: 8px;
  padding: 14px;
  background: var(--fc-panel);
  border: 1px solid var(--fc-hair-row);
  border-radius: var(--fc-panel-radius);
  box-shadow: var(--fc-panel-shadow);
  backdrop-filter: var(--fc-blur);
  -webkit-backdrop-filter: var(--fc-blur);
}
.layers-menu[hidden] { display: none; }
.layers-menu .pill-toggle { justify-content: flex-start; }
.layers-menu .field-opacity { margin-left: 0; padding-top: 4px; border-top: 1px solid var(--fc-hair-row); }
.layers-menu .field-opacity input[type="range"] { width: 96px; }


/* ── "My kind of day" — step 2 of the lens menu ─────────────────────────────
   Not a popover any more: it is MOVED into #lens-menu, which owns its position
   and its bounded height. So it has no fixed positioning, no width and no
   surface of its own — the menu is the surface. */
.ce { display: flex; flex-direction: column; gap: 16px; padding: 2px 2px 6px; }
.ce[hidden] { display: none; }
.ce.dragover { outline: 1px dashed var(--accent); outline-offset: 6px; }
.ce-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.ce-title { font-size: 19px; font-weight: 500; letter-spacing: -0.01em; }
.ce-sub { font-size: 14.5px; color: var(--fc-text-2); }
.ce-hint { font-size: 13.5px; color: var(--fc-muted); }
.ce-note { color: var(--fc-muted); font-size: 13px; line-height: 1.5; }

/* A quiet text button — "Change", "or gate it on wind instead". */
.ce-textbtn {
  font: inherit; font-size: 14.5px;
  color: var(--fc-muted);
  background: none; border: 0; padding: 0;
  cursor: pointer;
}
.ce-textbtn:hover { color: var(--fc-text-2); }

/* "Copy a recipe from Sunbathing ▾" — seeds the editor, does not switch lens. */
.ce-copy-wrap { position: relative; }
.ce-chip {
  font: inherit; font-size: 14px;
  color: var(--fc-text-2);
  background: var(--fc-fill);
  border: 0; border-radius: 9px;
  padding: 7px 12px;
  cursor: pointer;
}
.ce-chip:hover { color: var(--fc-text); background: var(--fc-fill-strong); }
.ce-copy-menu {
  position: absolute; top: calc(100% + 6px); right: 0; z-index: 2;
  min-width: 190px; padding: 6px;
  background: var(--fc-overlay);
  border: 1px solid var(--fc-hair-row);
  border-radius: 12px;
  box-shadow: var(--fc-panel-shadow);
}
.ce-copy-menu[hidden] { display: none; }
.ce-copy-menu .row { font-size: 15px; padding: 10px 12px; }

/* The gate as a sentence, not two buttons: it is one decision about what a good
   day even is, not a pair of modes to be in. */
.ce-gate-line { font-size: 14.5px; line-height: 1.7; color: var(--fc-text-2); }
.ce-inline {
  font: inherit; font-size: 14.5px; font-weight: 600;
  color: var(--fc-text);
  background: none; border: 0; border-bottom: 1px dashed rgba(255, 255, 255, 0.26);
  padding: 0 0 1px; cursor: pointer;
}
.ce-inline:hover { border-bottom-color: var(--accent); }
.ce-gate-swap { display: block; margin-top: 4px; font-size: 13.5px; }

/* Rows are divs, not labels (see the note in index.html). */
.ce-slider { display: flex; align-items: center; gap: 12px; }
.ce-slider[hidden] { display: none; }
.ce-name { flex: 0 0 92px; color: var(--fc-text-2); font-size: 14.5px; }
.ce-slider output { flex: 0 0 58px; text-align: right; font-size: 14px; color: var(--fc-text); }
.ce-slider input[type="range"] {
  flex: 1; min-width: 0; height: 16px; margin: 0;
  appearance: none; -webkit-appearance: none;
  background: none; cursor: pointer;
}
.ce-slider input[type="range"]::-webkit-slider-runnable-track {
  height: 4px; border-radius: 2px; background: rgba(255, 255, 255, 0.16);
}
.ce-slider input[type="range"]::-moz-range-track {
  height: 4px; border-radius: 2px; background: rgba(255, 255, 255, 0.16);
}
/* A 16px thumb with an ink ring. This is what replaced the −/+ steppers: they
   existed because an 11px thumb was hard to hit, and a bigger thumb is the
   actual fix rather than two buttons apologising for a small one. */
.ce-slider input[type="range"]::-webkit-slider-thumb {
  appearance: none; -webkit-appearance: none;
  width: 16px; height: 16px; border-radius: 50%;
  background: var(--accent); border: 2px solid var(--accent-ink);
  margin-top: -6px;
}
.ce-slider input[type="range"]::-moz-range-thumb {
  width: 16px; height: 16px; border-radius: 50%;
  background: var(--accent); border: 2px solid var(--accent-ink);
}

/* ── the ranking ── */
.ce-ranks-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.ce-ranks { display: flex; flex-wrap: wrap; gap: 8px; }
.ce-rank {
  display: inline-flex; align-items: baseline; gap: 8px;
  padding: 8px 13px;
  border-radius: 9px;
  background: var(--rank-tint, rgba(255, 255, 255, 0.05));
  font-size: 14.5px;
  cursor: grab;
  user-select: none;
}
.ce-rank:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.ce-rank.dragging { opacity: 0.45; cursor: grabbing; }
.ce-rank-n { font-size: 12px; color: var(--fc-muted); }

/* ── the seeded day ── */
.sd-row {
  display: flex; align-items: center; gap: 12px;
  padding: 12px 14px;
  border-radius: 12px;
  background: var(--fc-accent-10);
}
.sd-body { min-width: 0; flex: 1; }
.sd-title { font-size: 14.5px; }
.sd-thumb { width: 42px; height: 42px; flex: none; object-fit: cover; border-radius: 8px; }
.sd-thumb[hidden] { display: none; }
.sd-fields { display: flex; flex-direction: column; gap: 10px; }
.sd-fields[hidden] { display: none; }

/* ── The wind-arrow key, inside the Layers menu ── */
.legend-arrows { margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--border); }
.legend-arrows[hidden] { display: none; }
.legend-arrows-title {
  display: flex; justify-content: space-between; align-items: baseline;
  font-size: 11px; font-weight: 600; margin-bottom: 6px;
}
.legend-arrows-title .unit { color: var(--muted); font-weight: 500; font-size: 10px; }
.legend-arrows-row { display: flex; align-items: flex-end; justify-content: space-between; gap: 3px; }
.legend-arrow { display: flex; flex-direction: column; align-items: center; gap: 3px; }
.legend-arrow img { display: block; }
.legend-arrow span { font-size: 9px; color: var(--muted); white-space: nowrap; }

/* ═══════════════ THE RULER — the year, on the screen's bottom edge ═══════════
   Not a card floating above the edge: the edge itself. The block spans the full
   width and sits ON the bottom, over a scrim, so the map runs underneath it
   uninterrupted — which is the whole reason the old header could go. */
#ruler-block {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  /* 45, with the corner chrome — not 28. #cityview is 35 and covers the whole
     viewport, so at 28 the ruler laid out correctly and painted BEHIND the
     briefing: present in the DOM, invisible on screen. The ruler and the corner
     are one layer (the fixed chrome); only the summoned menus, at 46, go over
     them. */
  z-index: 45;
  padding: 0 var(--fc-gutter) 26px;
  display: none;
  pointer-events: none; /* the scrim area is the map's; the controls opt back in */
  /* Entering or leaving a briefing moves the ruler between the screen's bottom
     edge and the map band's. The body class flips in one frame, so without this
     the ruler JUMPED while the map box was still animating under it. Same 420ms
     and same easeOutCubic as animateMapBox, so the two arrive together.

     `bottom` only — onCityScroll writes an inline transform on this element
     every scroll frame, and transitioning that would make the band lag the
     page. */
  transition: bottom 420ms cubic-bezier(0.215, 0.61, 0.355, 1);
}
@media (prefers-reduced-motion: reduce) {
  #ruler-block { transition: none; }
}
/* The ruler stays in the briefing too — a briefing is about one place at one
   time of year, and the ruler is how the time of year is changed.
   
   It belongs to the MAP, so in this view it sits on the map band's bottom edge
   rather than the screen's, and scrolls away with the band (onCityScroll). A
   ruler pinned to the bottom of a document reads as that document's own scale;
   this one is the map's. `bottom` in percent resolves against the viewport for a
   fixed element, so this is the band's lower edge without restating 40vh. */
body.view-map #ruler-block, body.view-city #ruler-block { display: block; }
body.view-city #ruler-block { bottom: calc(100% - var(--city-band)); }
/* The ruler now owns the band's lower edge, so the band's own controls step up
   over it. They are absolutely positioned inside .city-band, so this offset is
   measured from the band's bottom — the same edge the ruler sits on. */
body.view-city .cb-topright, body.view-city .cb-foot { bottom: 128px; }
/* The scrim is the block's own ::before so nothing else has to own a gradient
   div: it reaches well above the ruler and fades out, so light coastlines never
   swallow the ticks. */
#ruler-block::before {
  content: "";
  position: absolute; left: 0; right: 0; bottom: 0; height: 170px;
  background: var(--fc-scrim-bottom);
  pointer-events: none;
}
#ruler-key, #ruler-row { position: relative; pointer-events: auto; }

/* ── the key row ── */
/* Only the key. "Where it peaks" moved up to the corner words: sitting here it
   read as a caption for the ruler rather than as the door to a list of places. */
#ruler-key {
  display: flex; align-items: center;
  gap: 16px; margin-bottom: 14px; flex-wrap: wrap;
}
.rk-eyebrow {
  font-size: 12.5px; font-weight: 500; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--fc-muted); white-space: nowrap;
}
.rk-scale { display: flex; align-items: center; gap: 16px; min-width: 0; flex-wrap: wrap; }
/* Four words and four swatches — the whole legend, for a rating. */
.rk-band {
  display: inline-flex; align-items: center; gap: 7px;
  font-size: 12.5px; color: var(--fc-muted); white-space: nowrap;
}
.rk-band::before {
  content: ""; width: 9px; height: 9px; border-radius: 2px;
  background: var(--swatch); box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.25);
}
/* …and the clipped ramp, for a measurement, where four words cannot do the job. */
.rk-ramp { display: flex; align-items: center; gap: 8px; font-size: 12.5px; color: var(--fc-muted); }
.rk-ramp-label { white-space: nowrap; }
.rk-ramp-end { white-space: nowrap; }
.rk-ramp-bar {
  width: 132px; height: 9px; border-radius: 2px;
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.25);
}

/* ── play + ruler ── */
#ruler-row { display: flex; align-items: flex-end; gap: 26px; }
.play {
  flex: none;
  width: 44px; height: 44px;
  margin-bottom: 2px;
  border-radius: 50%;
  border: 0;
  background: var(--accent);
  color: var(--accent-ink);
  font-size: 13px;
  cursor: pointer;
  transition: background 0.15s ease;
}
.play:hover { background: var(--accent-hover); }

.ruler { position: relative; flex: 1; min-width: 0; }
.ru-cells { display: flex; align-items: flex-end; }
/* One cell per month, all equal — a month is a month regardless of its length,
   because the unit on this scale is the dekad and every month has three. */
.ru-month { flex: 1; min-width: 0; }
/* Every section the same size. They were briefly drawn at a height proportional
   to the rating there — the handoff's idea — and it was dropped after use: 36
   bars at 36 heights read as a chart rather than as a control, and the short
   ones did not look like something you could aim at. A uniform row is a scale
   you can hit, and each section is visibly one ten-day step. */
/* The sections tile the ruler EXACTLY: no padding, no gaps, no margins. Three
   separate things assume they do — the marker sits at (i + 0.5) / 36 of the
   ruler's width, the hover preview maps a cursor x back through the same
   fraction, and the invisible range input divides its own width into 36 equal
   steps. Any padding or gap in here and all three drift from what is drawn,
   which is why the marker looked centred on some dekads and not others.

   The visible gap is a transparent border INSIDE each section's box, with
   background-clip keeping the fill off it. Split evenly left and right so the
   fill is centred in the box the arithmetic is about. */
.ru-ticks { display: flex; align-items: stretch; height: 22px; }
.ru-tick {
  flex: 1 1 0; min-width: 0;
  border-left: 1.5px solid transparent;
  border-right: 1.5px solid transparent;
  background-clip: padding-box;
  border-radius: 3px;
  background-color: rgba(255, 255, 255, 0.15);    /* still to come */
  transition: background-color 120ms;
}
.ru-tick.on { background-color: rgba(63, 191, 133, 0.4); }  /* already gone by */
.ru-tick.cur { background-color: var(--accent); }
.ru-tick.hov { background-color: rgba(255, 255, 255, 0.5); }
.ru-tick.on.hov, .ru-tick.cur.hov { background-color: var(--accent-hover); }
.ru-line { height: 1px; background: var(--fc-hair-ruler); }
.ru-label {
  border-left: 1px solid var(--fc-hair-month);
  padding: 9px 0 0 7px;
  font-size: 12px; color: var(--fc-ruler-month); white-space: nowrap;
}
.ru-label.cur { color: var(--accent); }

/* The marker: one playhead through the scale. It was a dot sitting under the
   sections with a hairline rising from it, and the dot read as slightly off
   whatever it pointed at — it had its own baseline, below the thing it was
   marking. A line crosses the sections it is naming, so there is nothing to
   misread. -1px is half its width, so `left` stays the true centre of the dekad
   rather than being pre-compensated in JS. */
.ru-marker { position: absolute; top: 0; margin-left: -1px; pointer-events: none; }
.ru-marker[hidden] { display: none; }
.ru-mline {
  position: absolute; top: -3px; left: 0;
  width: 2px; height: 31px;   /* the 22px sections, the hairline, and a little past it */
  border-radius: 1px;
  background: var(--accent);
}

/* The real range input rides invisibly over the ticks: drag and keyboard for
   free. It reaches past the ticks' 34px so the whole ruler is draggable, and it
   is what receives the hover the ticks cannot (see wireRuler). */
.ru-range {
  position: absolute; left: 0; right: 0; top: -6px; height: 46px;
  width: 100%; margin: 0; opacity: 0; cursor: pointer; z-index: 1;
}
.ru-range::-webkit-slider-runnable-track { background: transparent; height: 46px; }
.ru-range::-moz-range-track { background: transparent; height: 46px; }
.ru-range:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; opacity: 1; }

/* Time-of-day as a single-value select (one option visible). */
.bucket-select {
  flex: none;
  font: inherit; font-size: 12px; font-weight: 500;
  color: var(--text);
  background: var(--fill);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 6px 30px 6px 11px;
  cursor: pointer;
  appearance: none; -webkit-appearance: none;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'><path d='M1 1l4 4 4-4' fill='none' stroke='%2393a6c0' stroke-width='1.4'/></svg>");
  background-repeat: no-repeat;
  background-position: right 11px center;
}
.bucket-select:hover { background-color: var(--fill-hover); border-color: var(--border-strong); }
.bucket-select:focus { outline: none; border-color: var(--accent); }
.bucket-select option { background: var(--panel-solid); color: var(--text); }

.pill-toggle {
  font: inherit; font-size: 11px;
  color: var(--muted);
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 5px 11px;
  cursor: pointer;
  display: flex; align-items: center; gap: 7px;
  transition: all 0.15s ease;
}
.pill-toggle .dot {
  width: 7px; height: 7px; border-radius: 50%;
  border: 1px solid var(--muted); background: transparent; transition: all 0.15s ease;
}
.pill-toggle.active { color: var(--text); background: var(--fill-hover); border-color: var(--border-strong); }
.pill-toggle.active .dot { border-color: var(--accent); background: var(--accent); box-shadow: 0 0 6px var(--accent); }
.field-opacity { display: inline-flex; align-items: center; gap: 6px; color: var(--muted); font-size: 13px; margin-left: 2px; }
.field-opacity input[type="range"] { width: 66px; accent-color: var(--accent); cursor: pointer; }

/* Shared range styling (thumbs green). */
input[type='range'] {
  -webkit-appearance: none; appearance: none;
  height: 4px; border-radius: 3px;
  background: rgba(255, 255, 255, 0.16);
  outline: none; cursor: pointer;
}
input[type='range']::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 14px; height: 14px; border-radius: 50%;
  background: var(--accent); border: 2px solid var(--accent-ink);
  box-shadow: 0 0 0 1px var(--accent);
}
input[type='range']::-moz-range-thumb {
  width: 13px; height: 13px; border-radius: 50%;
  background: var(--accent); border: 2px solid var(--accent-ink);
}

/* ═══════════════ LANDING ═══════════════ */
/* Bottom-anchored, not centred. The map is the subject of this screen, and a
   block floating in the middle of it covers the part a reader is looking at. */
/* The bottom padding is not breathing room, it is the suggestion list's seat.
   The list hangs below the field in the ordinary way, so the block has to sit
   far enough off the bottom edge for a full one (320px, plus its 8px gap, plus
   the ~40px of "Or rate the whole world…" below the field) to land on screen.
   Capped in vh so a short window shrinks the reserve rather than pushing the
   92px headline off the top. */
#landing {
  position: fixed; inset: 0; z-index: 40;
  display: flex; align-items: flex-end;
  padding: 0 var(--fc-gutter) var(--fc-landing-reserve);
}
/* A vertical scrim rather than a radial vignette: the type sits at the bottom,
   so that is where the darkness needs to be. */
.landing-vignette {
  position: absolute; inset: 0;
  background: linear-gradient(180deg, rgba(4, 7, 10, 0.55) 0%, rgba(4, 7, 10, 0.82) 55%, rgba(4, 7, 10, 0.94) 100%);
}
.landing-block {
  position: relative;
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: 60px; width: 100%;
}
.landing-col { display: flex; flex-direction: column; align-items: flex-start; max-width: 820px; }
.landing-eyebrow {
  font-size: 13px; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--accent);
}
.landing-headline {
  font-size: 92px; font-weight: 400; line-height: 0.98; letter-spacing: -0.02em;
  margin: 22px 0 0; text-wrap: balance;
}
.landing-row { display: flex; gap: 14px; margin-top: 46px; align-items: stretch; }
.search-wrap { position: relative; width: 540px; max-width: min(540px, 60vw); }
/* The primary action sits INSIDE the field: one object that takes a city and
   gives back a year, rather than a field and a button to be paired up. */
.search-field {
  display: flex; align-items: center; gap: 10px;
  background: rgba(255, 255, 255, 0.08);
  border: 1px solid var(--fc-outline);
  border-radius: 14px;
  padding: 9px 9px 9px 24px;
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
}
.search-field:focus-within { border-color: var(--accent); }
#search-input {
  flex: 1; min-width: 0;
  background: transparent; border: none; outline: none;
  color: var(--fc-text); font-size: 19px; font-family: inherit;
}
#search-input::placeholder { color: var(--fc-muted); }
.search-btn {
  flex: none;
  border: none; background: var(--accent); color: var(--accent-ink);
  font-weight: 600; font-size: 16px; border-radius: 10px; padding: 15px 26px; cursor: pointer;
  font-family: inherit; white-space: nowrap;
}
.search-btn:hover { background: var(--accent-hover); }
.ghost-btn {
  display: flex; align-items: center; gap: 9px;
  background: transparent;
  border: 1px solid var(--fc-outline-strong);
  border-radius: 14px;
  padding: 16px 26px;
  color: var(--fc-text); font-size: 16px; font-weight: 500; cursor: pointer;
  font-family: inherit; white-space: nowrap;
}
.ghost-btn:hover { border-color: var(--accent); color: var(--accent-hover); }
/* The third route reads as a sentence, not a third button: it is the least
   obvious way in and should not compete with the two above it. */
.landing-third { margin-top: 20px; font-size: 15px; color: var(--fc-muted); }
.landing-link {
  font: inherit; font-size: 15px;
  color: var(--accent);
  background: none; border: 0; padding: 0;
  border-bottom: 1px solid rgba(63, 191, 133, 0.5);
  cursor: pointer;
}
.landing-link:hover { color: var(--accent-hover); }
/* The right-hand column of the landing block: what the numbers are, and the one
   line about measuring the audience.
   It does not sit on the headline's baseline like the rest of the block — it
   drops out of flow and rides the bottom edge of the page, below the reserve the
   block keeps clear. Two columns bottom-aligned to each other read as one
   paragraph in two halves; this reads as what it is, a footnote to the whole
   screen. Positioned against .landing-block (the nearest positioned ancestor),
   so `right: 0` is already the gutter and only the vertical needs the reserve
   subtracted back out. */
.landing-aside {
  position: absolute; right: 0;
  bottom: calc(var(--fc-gutter) - var(--fc-landing-reserve));
  width: 300px;
}
.landing-note {
  margin: 0;
  font-size: 15px; line-height: 1.65; color: var(--fc-muted);
}
.landing-legal { margin: 10px 0 0; font-size: 13px; line-height: 1.5; }
.landing-legal a {
  color: var(--fc-muted);
  text-decoration: none;
  border-bottom: 1px solid rgba(255, 255, 255, 0.18);
}
.landing-legal a:hover { color: var(--accent-hover); border-bottom-color: rgba(63, 191, 133, 0.5); }

.suggestions {
  position: absolute; top: calc(100% + 8px); left: 0; right: 0;
  background: rgba(14, 23, 38, 0.98);
  border: 1px solid var(--border-strong);
  border-radius: 12px; padding: 6px;
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.6);
  z-index: 6;
  text-align: left;
  max-height: 320px; overflow-y: auto;
}
.suggestions[hidden] { display: none; }
.suggestion {
  display: flex; width: 100%; justify-content: space-between; align-items: center; gap: 12px;
  background: transparent; border: none; color: var(--text);
  padding: 9px 11px; border-radius: 8px; cursor: pointer; text-align: left;
  font: inherit; font-size: 13px;
}
.suggestion:hover, .suggestion.active { background: var(--accent-10); }
.suggestion .s-country { color: var(--muted2); font-size: 11px; }

/* ═══════════════ CITY VIEW ═══════════════ */
/* pointer-events:none on the whole overlay so the map band behind stays interactive;
   the header and body re-enable events for their own controls. */
#cityview { position: fixed; inset: 0; z-index: 35; display: none; pointer-events: none; }
body.view-city #cityview { display: block; }
/* In the city view the map becomes a band at the TOP OF THE PAGE — there is no
   header above it any more — and the scroll container sits over it, transparent
   at the band so the real map shows through and scrolls away. */
body.view-city #map { top: 0; left: 0; right: 0; height: var(--city-band); bottom: auto; }

/* The scroll container spans the page and sits over the fixed map band. It's
   pointer-transparent so drags/clicks/zoom fall through to the real map in the band;
   the masthead and body re-enable pointer events for themselves. Wheeling over
   the opaque part still scrolls the container.

   Keep this selector in step with the element's class. The masthead used to be
   .city-header; when it was renamed the rule stopped matching, and the whole
   block went pointer-transparent — no wheel, no text selection, and drags
   landing on the map band behind it. */
#cityscroll {
  position: absolute; inset: 0;
  overflow-y: auto;
  overflow-x: hidden;
  pointer-events: none;
}
.masthead, .city-body { pointer-events: auto; }
/* ── the same container, under a finger ──
   A wheel scrolls whatever element it lands on and then walks UP to the nearest
   scroller, so pointer-events:none above costs a mouse nothing. A touch is not
   resolved that way. WebKit builds a scroller's touch event region from the
   SCROLLER'S OWN pointer-events, not its descendants' — so a pointer-transparent
   scroller has no region, and a pan over it finds no scrollable node at all.
   Measured on an iPhone (iOS 18.7): the box overflowed 3302px into 695px, took a
   scripted scrollTop:200 exactly, the finger landed inside it, nineteen
   touchmoves fired, not one was defaultPrevented — and it never moved a pixel.
   The masthead and .city-body being pointer-events:auto does not help; the region
   is the scroller's. (The one place a finger DID scroll the briefing was the
   calendar strips and the chart slots, which declare touch-action:pan-y and so
   land in the region by another route. That is the tell, if this ever regresses.)

   So where the pointer is coarse, the scroller takes its own events back. The
   cost is the band's own map gestures — pan, and tapping a dot to switch city —
   which is the right way round: a briefing that will not scroll is broken, and ⤢
   still gives the map a whole screen to be panned on. The band's controls are the
   scroller's own descendants, so they keep working. */
@media (hover: none) and (pointer: coarse) {
  #cityscroll { pointer-events: auto; }
  /* The hint named the two gestures the band just gave up. The credits behind "+"
     are unaffected — they are the same element wearing .is-src. */
  .cb-foot-text:not(.is-src) { display: none; }
}
/* Both, in lockstep — they are siblings in flow, so the same transform on each
   moves them as one block. The animation is on the ELEMENTS, not their contents,
   so re-rendering the masthead on calendar hover does not restart it. */
body.view-city .masthead,
body.view-city .city-body { animation: briefingRise 420ms cubic-bezier(0.215, 0.61, 0.355, 1); }
@media (prefers-reduced-motion: reduce) {
  body.view-city .masthead, body.view-city .city-body { animation: none; }
}
/* A pre-rendered city page opens ON the briefing — it is what the server sent,
   painted before app.js ran. There is no transition into it to animate, and
   rising a page the reader is already looking at is just a delay with a curve on
   it. The class is set by build-pages and dropped by the client the first time
   the reader navigates away, so every later open still rises. */
body.prerendered .masthead,
body.prerendered .city-body { animation: none; }
/* Transparent spacer that reveals the fixed map band beneath. pointer-events pass
   through to the map except on the controls. */
.city-band {
  position: relative;
  height: var(--city-band);
  pointer-events: none;
}
.city-band-ctrls { position: absolute; inset: 0; }
.city-band-ctrls > * { pointer-events: auto; }
/* Anything the band carries scrolls away with it (onCityScroll), so nothing
   from the map hangs over the briefing below. */
.ch-explore {
  font: inherit; font-size: 12px; color: var(--text);
  background: var(--fill); border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
  padding: 9px 14px; cursor: pointer;
}
/* Top-right map controls: maximise (→ full map view), then zoom +/−. */
/* The band's own controls sit at its BOTTOM-right. The top-right of the screen
   belongs to "Search a city" / "Layers" now — those are fixed and ride the band
   as it scrolls, so two sets of controls were landing in the same corner. */
.cb-topright { position: absolute; bottom: 14px; right: 16px; display: flex; align-items: flex-end; gap: 8px; }
.cb-max {
  width: 34px; height: 34px;
  border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
  background: var(--glass); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
  color: var(--text); font-size: 16px; line-height: 1; cursor: pointer;
}
.cb-max:hover { border-color: var(--accent); }
.cb-zoom {
  display: flex; flex-direction: column; overflow: hidden;
  border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
  background: var(--glass); backdrop-filter: blur(12px);
}
.cb-zbtn { width: 32px; height: 30px; border: none; background: transparent; color: var(--text); font-size: 16px; cursor: pointer; }
.cb-zbtn:hover { background: var(--fill-hover); }
.cb-zsep { height: 1px; background: var(--border-mid); }
/* Bottom-left footer: a +/- toggle then either the "click a dot" hint or the data
   sources (swapped in JS) — so the credits no longer sit under the hint. */
.cb-foot {
  position: absolute; bottom: 12px; left: 16px;
  display: flex; align-items: center; gap: 6px;
  max-width: calc(100% - 132px); /* clear the band's controls at bottom-right */
}
.cb-src-toggle {
  flex: 0 0 auto;
  width: 22px; height: 22px;
  display: flex; align-items: center; justify-content: center; font-size: 13px; line-height: 1; color: var(--muted2);
  background: rgba(11, 19, 33, 0.72); border: none; border-radius: 7px; cursor: pointer;
}
.cb-src-toggle:hover { color: var(--text); }
.cb-foot-text {
  min-width: 0; font-size: 10px; letter-spacing: 0.04em; color: var(--muted2);
  background: rgba(11, 19, 33, 0.72); padding: 5px 9px; border-radius: 7px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.cb-foot-text.is-src { white-space: normal; overflow: visible; line-height: 1.5; }
/* The custom +/- toggle owns the credits in the city view, so hide MapLibre's own. */
body.view-city .maplibregl-ctrl-attrib { display: none; }
/* Map view: the same collapsed-behind-"+" credits, fixed bottom-left, clearing
   the legend at bottom-right. Shown only on the full map. */
#map-foot {
  position: fixed; bottom: 16px; left: 16px; z-index: 28;
  display: none; align-items: flex-start; gap: 6px;
  max-width: min(560px, calc(100vw - 40px));
}
body.view-map #map-foot { display: flex; }

/* ── The masthead: two blocks of the same shape ────────────────────────────
   Left is the place and what its year does; right is the verdict and what drove
   it. They MIRROR — same headline size, same rule, same four rows — because
   that mirroring is the page's argument: the score and its reasons are equally
   load-bearing, so they get equal weight.

   It scrolls with the document. The old header pinned itself to the top, which
   is the bar this redesign deleted, reintroduced one page later. */
.masthead {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 44px;
  align-items: start;
  padding: 46px var(--fc-gutter-brief) 30px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.1);
  background: var(--fc-brief);
  position: relative; /* the close button hangs off this, out of the flow */
}
.ms-right { text-align: right; }
/* Absolute, not a first child in the flow. In the flow it pushed the score down
   by its own height and the two headlines stopped sharing a baseline — and the
   masthead's whole argument is that its two halves are the same shape. It sits
   in the block's top padding instead, clear of both. */
/* A glyph in a square, not a word. The box is 36px so it is a real target on a
   touch screen, but the ✕ inside it is what the eye aligns to — hence the
   negative offset: half the box is pulled back past the gutter so the glyph's
   centre sits on the same line the briefing's right edge does, rather than the
   button's edge sitting there and the mark floating 18px inside it. */
.ms-close {
  position: absolute;
  top: 10px; right: calc(var(--fc-gutter-brief) - 18px);
  display: grid; place-items: center;
  width: 36px; height: 36px;
  font: inherit; font-size: 17px; line-height: 1;
  color: var(--fc-muted);
  background: none; border: 0; padding: 0;
  border-radius: 50%;
  cursor: pointer;
  transition: color 120ms ease, background-color 120ms ease;
}
.ms-close:hover { color: var(--fc-text); background: rgba(255, 255, 255, 0.08); }
/* Keyboard focus gets the same affordance as hover plus a ring — the glyph is
   small and has no label of its own to fall back on. */
.ms-close:focus-visible {
  color: var(--fc-text);
  background: rgba(255, 255, 255, 0.08);
  outline: 1px solid rgba(255, 255, 255, 0.35);
  outline-offset: 1px;
}
.ms-name {
  margin: 0;
  font-size: 86px; font-weight: 400;
  letter-spacing: -0.03em; line-height: 0.94;
  text-wrap: balance;
}
.ms-score { font-size: 86px; font-weight: 400; letter-spacing: -0.03em; line-height: 0.94; }
.ms-band { font-size: 38px; font-weight: 400; margin-left: 14px; }
.ms-sub { margin-top: 10px; font-size: 16.5px; color: var(--fc-muted); }
.ms-right .ms-sub { font-size: 17px; color: var(--fc-text-2); }
/* The rule that starts both columns' rows, 20px above them, on both sides. */
.ms-rows { margin-top: 20px; border-top: 1px solid rgba(255, 255, 255, 0.09); }
.ms-row {
  display: flex; align-items: baseline; gap: 12px;
  padding: 11px 0;
  border-bottom: 1px solid var(--fc-hair-row);
  font-size: 14.5px;
}
.ms-row:last-child { border-bottom: 0; }
.ms-row-label { flex: 1; color: var(--fc-text-2); }
.ms-row-value { flex: 0 0 auto; color: var(--fc-muted); }
/* Only the drivers are coloured: they are verdicts, and the colour IS the
   verdict. The records beside them are measurements and stay quiet. */
.ms-right .ms-row.drv-good .ms-row-value { color: var(--fc-ideal); }
.ms-right .ms-row.drv-mid .ms-row-value { color: var(--fc-marginal); }
.ms-right .ms-row.drv-bad .ms-row-value { color: var(--fc-outside); }
.ms-right .ms-row.drv-na .ms-row-value { color: var(--fc-muted); opacity: 0.55; }
/* The left column's records go through the same row shape (recordsHtml still
   emits .record-card), so the two sides cannot drift apart. */
.ms-left .record-card { border-top: 0; border-bottom: 1px solid var(--fc-hair-row); }
.ms-left .record-card:last-child { border-bottom: 0; }

/* ── Briefing body: full-width diagrams aligned to the header progress track ──
   The gutter left of each chart (up to the track's left edge) holds a big "value on
   this day" callout; the chart itself spans the track's width. */
/* Extra bottom room so a section-title link can scroll its section right up to just
   under the pinned header (there'd otherwise not be enough content below the lower
   sections to bring them to the top). */
.city-body { padding: 44px 0 30vh; background: var(--fc-brief); }
/* Diagram grid: "best for" full-width on top, then the wide (short) temperature/
   feels-like diagram with an "at a glance" summary of the selected time beside it,
   and the four smaller diagrams in a single row below. */
.diagram-grid {
  display: grid;
  /* The design's own grid: two chart columns and a 300px reading column that
     spans both rows. "best" and "temp" run the full width above them. */
  grid-template-columns: 1fr 1fr 300px;
  grid-template-areas:
    "best best best"
    "temp temp temp"
    "d1   d2   sum"
    "d3   d4   sum";
  gap: 44px 48px;
  padding: 0 var(--fc-gutter-brief);
}
.a-best { grid-area: best; }
.a-temp { grid-area: temp; }
.a-sum  { grid-area: sum; }
.a-d1   { grid-area: d1; }
.a-d2   { grid-area: d2; }
.a-d3   { grid-area: d3; }
.a-d4   { grid-area: d4; }
.dcard { display: flex; flex-direction: column; min-width: 0; }
/* Wide, short temperature chart (viewBox 1000×220) so it sits low beside the summary. */
.dcard.a-temp .chart-svg { aspect-ratio: 1000 / 220; }
/* "At a glance" summary — a labelled measurement per row for the selected section. */
.sum-eyebrow {
  font-size: 12.5px; font-weight: 500; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--fc-muted);
  margin-bottom: 6px;
}
.sum-caveat { margin: 18px 0 0; font-size: 14.5px; line-height: 1.6; color: var(--fc-muted); }
.summary { display: flex; flex-direction: column; }
.sum-row {
  display: flex; justify-content: space-between; align-items: baseline; gap: 10px;
  padding: 12px 0; border-bottom: 1px solid var(--fc-hair-row);
}
.sum-row:last-child { border-bottom: 0; }
.sum-label { font-size: 15px; color: var(--fc-muted); min-width: 0; }
.sum-val { font-size: 15.5px; color: var(--fc-text); white-space: nowrap; }
.dcard-head { display: flex; align-items: baseline; gap: 9px; }
/* The four measurement cards. */
.dcard-head .sec-title { font-size: 23px; font-weight: 400; letter-spacing: -0.01em; }
/* The two that head a whole band of the page get the section size. */
.a-best .sec-title, .a-temp .sec-title { font-size: 30px; }
/* Section titles that scroll the map away read as links. */
/* These two are also links that scroll their section to the top. They read as
   headings first: the affordance is on hover, because a permanently accent-
   coloured heading claims to be more important than the one beside it. */
.sec-title.sec-scroll { cursor: pointer; }
.sec-title.sec-scroll:hover { color: var(--accent); }
/* Compact current-section value, right-aligned in the card header. */
.dcard-val { margin-left: auto; font-size: 18px; color: var(--fc-text); white-space: nowrap; }
.dcard-val span { color: var(--fc-muted); font-size: 14px; }
.dcard-unit { font-size: 13.5px; color: var(--fc-muted); margin: 3px 0 12px; }
.dcard .chart-slot { width: 100%; }

.sec-title { font-size: 23px; font-weight: 400; }
.sec-unit { font-size: 13.5px; color: var(--fc-muted); }

/* Charts fill their slot's reserved aspect box (below) so text/strokes never squash. */
.chart { width: 100%; height: 100%; display: block; }
/* Hover cursor + readout overlay: hover reads that day's value, click scrubs to it. */
.chart-slot { position: relative; cursor: crosshair; }
/* Reserve each chart's height via aspect-ratio so re-rendering its SVG every frame
   can't momentarily collapse the container (which would clamp the page scroll). */
.chart-svg { display: block; }
/* All four grid charts share one compact aspect box (viewBox 340×220). */
.dcard .chart-svg { aspect-ratio: 340 / 220; }
/* A pre-rendered page carries the lead diagram at BOTH widths, because a static
   file cannot know the viewport, and an SVG drawn at the wrong viewBox for its
   box is letterboxed inside it — a third-width chart floating in a full-width
   box until the client redraws it. This picks the one whose viewBox matches the
   box the rules above just reserved, at the same 1180px line (the narrow one is
   switched on in that media query). Once the client renders, one SVG with
   neither class replaces both, and these rules stop applying. */
.chart-atnarrow { display: none; }
.chart-cursor { position: absolute; top: 0; bottom: 14px; width: 1px; background: var(--accent); opacity: 0.85; pointer-events: none; }
.chart-cursor[hidden] { display: none; }
.chart-readout {
  position: absolute; top: 2px; pointer-events: none; font-size: 10px; color: var(--text);
  background: rgba(11, 19, 33, 0.92); border: 1px solid var(--border);
  border-radius: 5px; padding: 2px 6px; white-space: nowrap;
}
.chart-readout[hidden] { display: none; }

/* The records: five rows, not cards. Boxing each extreme made five equal
   objects out of what is really one list of readings — and the masthead the
   briefing is becoming wants them mirrored against the drivers column, which
   only works if both are rows of the same shape. */
.records-grid { display: flex; flex-direction: column; }
.record-card {
  display: flex; align-items: baseline; gap: 12px;
  padding: 11px 0;
  border-top: 1px solid var(--fc-hair-row);
  font-size: 14.5px;
}
.record-card .rc-label { flex: 1; color: var(--fc-text-2); }
.record-card .rc-value { color: var(--fc-text); }
.record-card .rc-sub { flex: 0 0 auto; min-width: 78px; text-align: right; color: var(--fc-muted); font-size: 13.5px; }

/* The dates that go somewhere — a record's "Mid Aug", the calendar's peak month.
   Styled as the editable text the sentence in the corner is, not as buttons: a
   dashed underline that solidifies and brightens under the pointer. Everything
   else about them (size, colour, alignment) stays exactly as the static label it
   replaced, so the layout does not shift now that they are controls. */
.rc-goto, .cal-goto {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  color: inherit;
  cursor: pointer;
  text-decoration: underline dashed currentColor 1px;
  text-underline-offset: 3px;
  transition: color 120ms ease, text-decoration-color 120ms ease;
}
.rc-goto:hover, .cal-goto:hover,
.rc-goto:focus-visible, .cal-goto:focus-visible {
  color: var(--fc-text);
  text-decoration-style: solid;
}
.rc-goto { min-width: 78px; text-align: right; }

/* "When this place is good" takes the full width. The detail panel that used to
   sit beside it said what the masthead now says — the same score, the same band,
   the same drivers — and two of those on one screen is one too many. Hovering a
   cell states it up there instead. */
.goodfor { margin: 14px 0 4px; }
/* Activity calendar — 36 section cells that flex to fill the card, grouped 3/month. */
.calendar { margin-top: 2px; }
.cal-row { display: flex; align-items: center; gap: 24px; margin-bottom: 4px; }
.cal-label { width: 176px; flex: 0 0 176px; font-size: 16px; color: var(--fc-text-2); }
/* The rating the map is showing is the one the reader came in on. */
.cal-row.active .cal-label { font-weight: 600; color: var(--fc-text); }
.cal-cells { display: flex; gap: 8px; flex: 1; min-width: 0; }   /* gap between month groups */
.cal-monthgroup { display: flex; gap: 3px; flex: 1; min-width: 0; } /* the three dekads within a month */
.cal-cell { flex: 1 1 0; min-width: 0; height: 30px; border-radius: 3px; cursor: pointer; }
.cal-cell.cur { box-shadow: inset 0 0 0 1.5px rgba(238, 242, 245, 0.85); }
.cal-cell.hov { box-shadow: inset 0 0 0 1.5px var(--accent); }
.cal-cell.pinned { box-shadow: inset 0 0 0 2px var(--fc-text); }
/* Where this rating peaks HERE — the answer the row exists to give, instead of
   thirty-six cells to compare by eye. */
.cal-peak { width: 96px; flex: 0 0 96px; font-size: 14.5px; color: var(--fc-text-2); text-align: left; }
.cal-peak-none { color: var(--fc-muted); }
/* The month scale repeats under the rows as the axis, inset past the name column
   (176 + 24) and the peak column (96 + 24) so it lines up with the cells. */
.cal-months { display: flex; gap: 8px; margin: 10px 0 0 0; padding-left: 200px; padding-right: 120px; }
.cal-month { flex: 1 1 0; min-width: 0; text-align: center; font-size: 12px; color: var(--fc-ruler-month); }
/* One row per factor: the MEASUREMENT right-aligned in a fixed column, its ASSESSMENT
   immediately after — a constant small gap, so verdict sits close to its number and
   the columns line up. Colour-coded: green ideal, amber marginal, red outside. */
.cal-drivers { display: flex; flex-direction: column; gap: 4px; }
.cd-row { display: flex; align-items: baseline; font-size: 11.5px; }
/* ── the score details, in their two states ── */
/* Collapsed, the line and its toggle share a row; expanded, the toggle sits
   under the rows. Both fall out of one flex-wrap column-less rule. */
.why { margin-top: 8px; display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 12px; }
.why .cal-drivers { flex: 1 1 100%; }
.why-toggle {
  font: inherit; font-size: 12.5px;
  color: var(--fc-muted);
  background: none; border: 0; padding: 4px 0 0;
  cursor: pointer;
}
.why-toggle:hover { color: var(--fc-text-2); }
.why-lim { font-size: 13px; }
.why-lim.drv-good { color: var(--fc-ideal); }
.why-lim.drv-mid { color: var(--fc-marginal); }
.why-lim.drv-bad { color: var(--fc-outside); }
/* In the peaks list the details belong to the row above them, so they sit in
   the row's own indent rather than starting at the panel edge. */
.pk-row .why { margin: 0 0 12px; }
.cd-measure { flex: 0 0 92px; text-align: right; color: var(--ink2); font-size: 11px; white-space: nowrap; }
.cd-verdict { padding-left: 12px; white-space: nowrap; color: var(--muted); }
.cd-verdict.drv-good { color: var(--accent); }
.cd-verdict.drv-mid { color: #f4c05a; }
.cd-verdict.drv-bad { color: #f2765c; font-weight: 600; }
.cd-verdict.drv-na { color: var(--muted); opacity: 0.55; }
/* Extras row */
.extras { margin: 44px var(--fc-gutter-brief) 0; padding-top: 30px; border-top: 1px solid var(--fc-hair-rule); }
.extras-grid { display: grid; grid-template-columns: 1.55fr 1fr; gap: 34px; align-items: start; }
.extras-label {
  font-size: 10px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--muted);
  font-weight: 600; margin-bottom: 11px;
}
.extras-desc { margin: 0; font-size: 13.5px; line-height: 1.7; color: var(--ink2); text-wrap: pretty; }
/* The city body's transient states — "Loading the briefing…" / "Couldn't load" —
   centred in the empty body rather than hugging the left edge. */
.city-wait { padding: 40px 0; color: var(--muted); text-align: center; }
.advice-list { display: flex; flex-direction: column; gap: 18px; }
.advice { max-width: 46em; }
.advice .adv-title { font-size: 14.5px; font-weight: 600; margin-bottom: 4px; }
.advice .adv-text { font-size: 14.5px; color: var(--fc-muted); line-height: 1.6; }

/* ═══════════════ Hover ring + tooltip ═══════════════ */
.city-ring {
  position: fixed; z-index: 58; width: 26px; height: 26px;
  margin-left: -13px; margin-top: -13px;
  pointer-events: none;
}
.city-ring[hidden] { display: none; }
.city-ring span {
  position: absolute; inset: 0; border-radius: 50%;
  border: 2px solid var(--accent);
  animation: cityPulse 1.3s ease-out infinite;
}
.city-ring span:last-child { animation-delay: 0.65s; }

.tooltip {
  position: fixed; top: 0; left: 0; z-index: 60;
  pointer-events: none;
  min-width: 168px;
  background: rgba(16, 26, 42, 0.97);
  backdrop-filter: blur(10px);
  border: 1px solid var(--border-strong);
  border-radius: 11px;
  padding: 11px 13px;
  font-size: 12px;
  box-shadow: var(--shadow);
  will-change: transform;
  animation: tipIn 0.16s ease;
}
.tooltip[hidden] { display: none; }
.tt-name { font-weight: 600; font-size: 15px; margin-bottom: 1px; }
.tt-country { font-size: 11px; color: var(--muted2); margin-bottom: 8px; }
.tt-readings { display: flex; gap: 16px; font-size: 12px; margin-bottom: 9px; }
.tt-readings span { color: var(--muted2); }
.tt-open { font-size: 11px; font-weight: 600; color: var(--accent); letter-spacing: 0.03em; }
.tooltip .tt-sep { height: 0; border-top: 1px solid var(--border); margin: 8px 0 6px; }
.tt-act { display: flex; justify-content: space-between; gap: 12px; font-size: 12px; }
.tt-act span { color: var(--muted2); }
.tt-drivers { display: flex; flex-wrap: wrap; gap: 2px 10px; font-size: 10.5px; line-height: 1.5; margin-top: 4px; max-width: 230px; }
.tt-drivers .drv { white-space: nowrap; color: var(--muted); }
.tt-drivers .drv-mid { color: #f4c05a; }
.tt-drivers .drv-bad { color: #f2765c; font-weight: 600; }
.tt-drivers .drv-na { color: var(--muted); opacity: 0.55; }

/* ═══════════════ View-state visibility ═══════════════ */
body.view-map #landing,
body.view-city #landing { display: none; }

/* ═══════════════ Loading ═══════════════ */
.loading {
  position: fixed; inset: 0; z-index: 80;
  display: flex; align-items: center; justify-content: center;
  background: var(--bg); color: var(--muted); font-size: 14px; letter-spacing: 0.3px;
}
.loading[hidden] { display: none; }

/* MapLibre control theming */
.maplibregl-ctrl-group { background: var(--glass) !important; backdrop-filter: blur(10px); }
.maplibregl-ctrl-group button { filter: invert(0.9) hue-rotate(180deg); }
.maplibregl-ctrl-attrib { background: rgba(10, 22, 38, 0.6) !important; color: var(--muted); }
.maplibregl-ctrl-attrib a { color: var(--muted); }
/* Hide the map's own zoom control everywhere — the chrome (city band zoom, scroll
   zoom) covers it, and it would collide with the bottom-right legend. */
.maplibregl-ctrl-bottom-right { display: none; }

/* ═══════════════ Responsive (desktop-first; a graceful narrow fallback) ═══════ */
@media (max-width: 1180px) {
  /* Below the feature-grid width, stack every diagram in one column; the wide
     temperature chart reverts to the standard aspect. The cards' grid-area names
     must be released too — with grid-template-areas:none they'd all resolve to
     the same implicit line and pile onto each other. */
  .diagram-grid { grid-template-columns: 1fr; grid-template-areas: none; }
  .a-best, .a-temp, .a-sum, .a-d1, .a-d2, .a-d3, .a-d4 { grid-area: auto; }
  .dcard.a-temp .chart-svg { aspect-ratio: 340 / 220; }
  /* …and with it, the baked chart that was drawn for this box. */
  .chart-atwide { display: none; }
  .chart-atnarrow { display: block; }
}
@media (max-width: 900px) {
  .city-grid, .extras-grid { grid-template-columns: 1fr; gap: 22px; }
  /* Stack the calendar over its assessment panel when there's no room beside it. */
  .goodfor { grid-template-columns: 1fr; }
  .landing-headline { font-size: 38px; }
  .search-wrap { width: 100%; max-width: none; }
  .landing-row { flex-direction: column; width: 100%; max-width: 452px; }
  .ghost-btn { justify-content: center; }
}

/* ── Compact-desktop / tablet tier (721–1180px) ─────────────────────────────
   Between the phone layout (≤720px, which rearranges the DOM into a chip rail +
   bottom bar) and the multi-column desktop (>1180px, where the diagram grid also
   goes multi-column), the header used to overflow: the ~745px activity-chip rail
   spilled over the time-of-day select and cog, the timeline's full month names
   collided, and the six briefing stat-cards crushed into an unreadable row. This
   keeps the desktop STRUCTURE but compacts it in place — no DOM moves, unlike the
   phone layer. Shares the 1180 boundary with the single-column diagram grid (and
   the temp chart's narrow viewBox); kept off ≤720 by the min-width. */
@media (min-width: 721px) and (max-width: 1180px) {
  /* The sentence is the widest thing in the corner; give it a little back
     before the phone block takes over. */
  .sentence, .sn-word { font-size: 26px; }
  /* The six briefing records wrap to as many columns as fit, stacked under the
     city name, instead of being crushed into one row beside it. */
  .city-header { flex-direction: column; align-items: stretch; gap: 14px; }
  .ch-left { max-width: none; }

}

/* ═══════════════ "MY KIND OF DAY" — the seeder's fields + the peaks panel ════
   Two things that turn a rating from a colour into an answer: the seeder inside
   the editor (name a day, get a recipe) and the panel beside the map (where
   that rating peaks).

   The seeder's SURFACE moved up into the .ce block above — one tinted row. What
   is left here is the fields behind "Change", which are the fallback for when
   there is no photo to drop. */
#sd-where {
  flex: 1; min-width: 0; padding: 10px 12px;
  font: inherit; font-size: 14.5px; color: var(--fc-text);
  background: var(--fc-fill); border: 1px solid var(--fc-hair-rule); border-radius: 10px;
  outline: none;
}
#sd-where:focus { border-color: var(--accent); }
.sd-where-wrap { position: relative; display: flex; align-items: center; gap: 10px; }
.sd-sugg { position: absolute; top: calc(100% + 5px); left: 0; right: 0; z-index: 2; }
.sd-when { display: flex; gap: 6px; }
.sd-when select {
  flex: 1; min-width: 0; padding: 9px 8px;
  font: inherit; font-size: 13.5px; color: var(--fc-text);
  background: var(--fc-fill); border: 1px solid var(--fc-hair-rule); border-radius: 10px;
  cursor: pointer; outline: none;
}
.sd-when select:focus { border-color: var(--accent); }
.sd-when select option { background: var(--panel-solid); color: var(--fc-text); }
.sd-apply-row { display: flex; align-items: center; gap: 10px; }
.sd-apply {
  flex: 1; padding: 11px;
  font: inherit; font-size: 14.5px; font-weight: 600; color: var(--accent-ink);
  background: var(--accent); border: 0; border-radius: 10px;
  cursor: pointer; transition: background 0.15s ease;
}
.sd-apply:hover:not(:disabled) { background: var(--accent-hover); }
.sd-apply:disabled { opacity: 0.4; cursor: default; }
.sd-note { color: var(--fc-muted); font-size: 13px; line-height: 1.45; margin-top: 2px; }
.sd-note b { color: var(--fc-text); font-weight: 500; }
.sd-saved { display: flex; flex-wrap: wrap; gap: 6px; }
.sd-chip {
  padding: 6px 11px;
  font: inherit; font-size: 13px; color: var(--fc-muted);
  background: var(--fc-fill); border: 0; border-radius: 99px;
  cursor: pointer;
}
.sd-chip:hover { color: var(--fc-text); background: var(--fc-fill-strong); }

/* ── "Where it peaks" — a panel, asked for ──────────────────────────────────
   It used to be a rail permanently down the right edge, with a collapsed tab
   and a --rail-inset every other right-anchored control had to add to its own
   offset. Being always there is what made all that necessary. Summoned, it is
   just a panel in the corner: nothing steps aside for it, because nothing has
   to share the edge with it for longer than a reader wants it. */
.peaks {
  position: fixed;
  /* Above #cityview (35) so the list survives opening a match, below the corner
     chrome and its menus (45/46). */
  z-index: 36;
  top: 30px;
  right: var(--fc-gutter);
  width: 352px;
  max-height: calc(100vh - 210px); /* never down into the ruler */
  display: flex; flex-direction: column;
  padding: 26px 26px 14px;
}
.peaks[hidden] { display: none; }
.peaks-head { flex: none; }
.peaks-headline { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; }
.peaks-eyebrow {
  font-size: 12.5px; font-weight: 500; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--fc-muted);
}
.peaks-title { font-size: 26px; font-weight: 400; letter-spacing: -0.01em; margin-top: 4px; }
.peaks-sub { margin-top: 10px; font-size: 13.5px; line-height: 1.5; color: var(--fc-muted); }
/* scrollbar-gutter, not padding: the scores are right-aligned at the panel's
   edge, and an overlay scrollbar was sitting on top of them. Stable gutter
   reserves the lane whether or not the list is long enough to scroll, so the
   column does not shift when a re-rank changes the row count. */
.peaks-list { overflow-y: auto; margin-top: 6px; min-height: 0; scrollbar-gutter: stable; padding-right: 4px; }
.pk-note { padding: 16px 0; color: var(--fc-muted); font-size: 13.5px; line-height: 1.6; }

/* A row is three facts in the order a sentence would put them: the place, when
   it is good there, how good. No rank numeral (the order says that) and no
   condition dots (a swatch says "this number is that colour" and stops). */
.pk-row { border-top: 1px solid var(--fc-hair-row); }
.pk-row.active .pk-main { background: var(--fc-accent-10); }
.pk-main {
  display: flex; align-items: baseline; gap: 14px; width: 100%;
  padding: 15px 0;
  font: inherit; color: inherit; text-align: left;
  background: none; border: 0; cursor: pointer;
  border-radius: 8px;
}
.pk-main:hover { background: rgba(255, 255, 255, 0.04); }
.pk-body { min-width: 0; flex: 1; }
.pk-place { display: block; font-size: 17px; font-weight: 500; }
.pk-country { color: var(--fc-muted); font-weight: 400; font-size: 14px; margin-left: 7px; }
.pk-when { display: block; color: var(--fc-muted); font-size: 13.5px; margin-top: 3px; }
.pk-score { flex: none; font-size: 17px; }
.pk-score.ideal { color: var(--fc-ideal); }
.pk-score.good { color: #a6d96a; }
/* The panel belongs to the map surface, so it ends where the map does. In the
   city view the map is only a band, so the list is bounded by the band and
   scrolls away with it (layout.ts mirrors the band's transform) rather than
   running down over the briefing. */
body.view-city .peaks { max-height: calc(var(--city-band) - 28px); }

/* ═══════════════ MOBILE ≤720px ═══════════════════════════════════════════════
   The one mobile block (the desktop-regression firewall: every mobile rule lives
   here). It no longer has a JS twin that MOVES anything: the ruler is already on
   the bottom edge at every width, so the phone needs the same DOM in smaller
   type rather than a second arrangement of it. Phase 9i finishes the job. */
/* The second half of the condition is a phone HELD SIDEWAYS. A 15 Pro in
   landscape is 852px wide, so a width-only tier hands it the compact-desktop
   layout — a phone being treated as a small desktop because it was rotated. What
   separates the two is not width: it is a touch device whose screen is short.
   Every phone in landscape is under 440px tall (the largest Pro Max is 440); the
   smallest tablet in landscape is 744. 520px sits between them with room on both
   sides, and the pointer terms keep a short desktop window out of here entirely.
   breakpoints.ts mirrors this by hand — change one and change the other. */
@media (max-width: 720px), (hover: none) and (pointer: coarse) and (max-height: 520px) {
  :root {
    --city-band: 30vh;
    /* What the bottom-edge ruler occupies, for the things that must clear it:
       the key row and its margin, the ticks, the hairline and the month labels,
       plus the block's own bottom padding. Measured, not guessed — anything
       shorter and the peaks sheet sits on the key. */
    --ruler-h: calc(132px + env(safe-area-inset-bottom));
    --body-left: 16px;
  }
  @supports (height: 1dvh) {
    :root { --city-band: 30dvh; }
  }

  /* ── the corner, at phone size ── */
  #corner { top: 18px; left: var(--fc-gutter-phone); bottom: 132px; gap: 14px; }
  #corner-right { top: 20px; right: var(--fc-gutter-phone); gap: 16px; }
  .corner-word { font-size: 14px; }
  .sn-eyebrow { font-size: 11px; }
  /* The sentence is the one thing that cannot simply shrink: at 390px "Best for
     Active outdoors · mid July · midday" will not fit on one line at any size
     worth reading. So it wraps HERE and only here — the lead word and the
     rating on one line, the two time words on the next — and the separators go
     with it, because a "·" at the start of a line is worse than none. */
  .sentence { font-size: 24px; line-height: 1.25; }
  .sn-word { font-size: 24px; }
  /* Two lines: the rating, then the time. Breaking anywhere else would leave a
     "·" at the end of a line — which is why this is a deliberate break rather
     than white-space:normal letting it fall where it may. */
  .sentence { white-space: normal; }
  .sn-br { display: inline; }
  .sn-sep-first { display: none; }
  .sn-helper { font-size: 13.5px; max-width: none; }
  /* The menus become full-width sheets under the sentence rather than 396px
     boxes hanging off a word that may itself have wrapped. */
  .menu { width: auto; align-self: stretch; margin-left: 0 !important; }

  /* ── the ruler, at phone size ── */
  #ruler-block { padding: 0 var(--fc-gutter-phone) calc(14px + env(safe-area-inset-bottom)); }
  /* The bands ARE the key; the eyebrow is a caption for them, and a caption is
     what a phone can spare. (The peaks opener that used to crowd this row has
     moved up to the corner words.) */
  #ruler-key { gap: 12px; margin-bottom: 12px; }
  .rk-eyebrow { display: none; }
  .rk-band { font-size: 11px; }
  .rk-scale { gap: 14px; }
  #ruler-row { gap: 14px; }
  .ru-ticks { height: 18px; }
  .ru-tick { border-left-width: 0.75px; border-right-width: 0.75px; }
  .ru-label { font-size: 9.5px; padding: 7px 0 0 4px; }
  .ru-mline { top: -3px; height: 26px; } /* the phone's shorter 18px sections */
  /* A finger needs a taller (still invisible) scrub target than a mouse.
     (input.ru-range, not .ru-range: the shared input[type='range'] height rule
     out-specifies a lone class.) */
  #ruler input.ru-range { top: -10px; height: 54px; touch-action: none; }

  /* ── the layers popover becomes a bottom sheet (cog + time-of-day) ── */
  .layers-menu {
    top: auto; left: 0; right: 0; bottom: 0;
    min-width: 0;
    border-radius: 16px 16px 0 0;
    border-bottom: none;
    padding: 16px 18px calc(18px + env(safe-area-inset-bottom));
    gap: 9px;
    box-shadow: var(--shadow-drawer);
  }
  .layers-menu .pill-toggle { padding: 10px 14px; font-size: 13px; }
  .layers-menu .field-opacity { display: flex; padding-top: 10px; }
  .layers-menu .field-opacity input[type="range"] { width: auto; flex: 1; }

  /* The editor has no sheet of its own any more — it is inside #lens-menu,
     which is already a full-width sheet at this size. All it needs is fingers. */
  .ce-slider input[type="range"] { height: 30px; }
  .ce-rank { padding: 11px 14px; }

  #map-foot { bottom: 8px; left: 8px; max-width: calc(100vw - 16px); }
  body.view-map #map-foot { bottom: calc(var(--ruler-h) + 8px); } /* clear the ruler */

  /* ── the briefing ── */
  /* No sentence and no ruler over a briefing at this width: 390px has room for
     the document or for the chrome, not both. The corner rides the band away
     (onCityScroll) and the map view is where the two get changed. */
  body.view-city #showing { display: none; }
  body.view-city #ruler-block { display: none; }
  /* And with the ruler gone, the 128px the band's controls step up to clear it is
     128px of nothing: at 30dvh that lands the zoom buttons, the maximise and the
     credits at the TOP of the band, in the brand and the corner words. They go
     back to the band's own bottom edge — the values they carry at every other
     width, restated because the city-view rule that displaced them still wins on
     specificity. */
  body.view-city .cb-topright { bottom: 14px; }
  body.view-city .cb-foot { bottom: 12px; }
  /* The masthead stops mirroring side-by-side and stacks: two 86px headlines
     cannot share 390px, and stacked they still read in the same order — place,
     then verdict, each with its own rows under it. */
  /* The top padding is deeper than the 26px the stack would otherwise want,
     because the close button lives in it: at this width the name is 52px and
     runs the full column, so a long one ("Santiago de los Caballeros") would
     pass under the ✕ if the two shared any vertical space. 44px is the button's
     36px plus its 6px offset, and the name starts below it. */
  .masthead { grid-template-columns: 1fr; gap: 26px; padding: 44px var(--fc-gutter-phone) 22px; }
  .ms-close { top: 6px; right: calc(var(--fc-gutter-phone) - 18px); }
  .ms-right { text-align: left; }
  .ms-name { font-size: 52px; }
  .ms-score { font-size: 52px; }
  .ms-band { font-size: 28px; margin-left: 10px; }
  .ms-sub { font-size: 14px; }
  .ms-right .ms-sub { font-size: 15.5px; }
  .city-body { padding: 26px 0 30vh; }
  .diagram-grid { gap: 30px 0; padding: 0 var(--fc-gutter-phone); }
  /* Calendar rows keep their name column — the peak column is what goes. At this
     width "Jul · 97" costs a quarter of the strip the cells need, and the strip
     is the thing the row exists to show. */
  .cal-row { gap: 12px; margin-bottom: 7px; }
  .cal-label { width: 104px; flex: 0 0 104px; font-size: 13px; }
  .cal-peak { display: none; }
  .cal-cell { height: 19px; }
  .cal-cells { gap: 4px; }
  .cal-monthgroup { gap: 1.5px; }
  .cal-months { padding-left: 116px; padding-right: 0; font-size: 9.5px; }
  .cal-month { font-size: 9.5px; }
  /* Horizontal drags scrub these strips; vertical drags stay page scrolls. */
  .chart-slot, .cal-cells { touch-action: pan-y; }
  /* Shrink the band chrome so the ~30dvh band keeps room for the map itself. */
  .cb-foot { max-width: calc(100% - 108px); }
  .extras { margin: 30px var(--fc-gutter-phone) 0; }
  .sum-caveat { font-size: 13.5px; }

  /* ── landing ── */
  #landing { padding: 0 var(--fc-gutter-phone) 44px; }
  /* The block stops being a row: the prose column moves under the buttons. Its
     flex-basis has to be reset with it — 300px is a WIDTH in a row and a HEIGHT
     in a column, and left alone it padded 200px of nothing under the text. */
  .landing-block { flex-direction: column; align-items: stretch; gap: 22px; }
  /* Back into the flow: the block is a column here, so the aside is simply the
     last thing in it and has no bottom edge of its own to ride. */
  .landing-aside { position: static; width: auto; flex: 0 0 auto; }
  .landing-note { font-size: 14px; }
  .landing-eyebrow { font-size: 12px; letter-spacing: 0.12em; }
  .landing-headline { font-size: 46px; margin-top: 16px; }
  /* The field becomes its own row and both buttons go full width beneath it: a
     540px shell with a button tucked inside has nowhere to go at 390px, and the
     primary action is the last thing that should be squeezed. */
  .landing-row { flex-direction: column; gap: 12px; margin-top: 30px; }
  .search-wrap { width: auto; max-width: none; }
  .search-field { flex-direction: column; align-items: stretch; gap: 12px; padding: 14px 18px; }
  .search-btn { padding: 17px; font-size: 17px; }
  .ghost-btn { justify-content: center; padding: 17px; font-size: 17px; }
  #search-input { font-size: 17px; } /* ≥16px, or iOS zooms on focus */
  /* On a phone the list still opens UPWARD. Desktop makes room for it below the
     field by sitting the block well off the bottom edge; there is no such room
     here — the stacked block already fills most of the screen, and reserving
     320px would push the headline off the top. */
  #suggestions { top: auto; bottom: calc(100% + 8px); }

  /* ── "my kind of day" ── */
  #sd-where { font-size: 16px; } /* <16px would trigger iOS focus zoom */
  /* The panel becomes a bottom sheet above the ruler: the map keeps the screen
     and the list stays one thumb away. It is dismissed by name ("Close") now
     rather than collapsed to a handle — with the panel summoned in the first
     place, a permanent lip on a phone screen is exactly the thing the redesign
     was removing. */
  .peaks {
    top: auto; left: 0; right: 0;
    bottom: calc(var(--ruler-h) + 8px);
    width: auto;
    padding: 18px var(--fc-gutter-phone) 6px;
    border-radius: var(--fc-panel-radius) var(--fc-panel-radius) 0 0;
    box-shadow: var(--shadow-drawer);
  }
  /* A FIXED height, not a max: the row count changes as a rating re-ranks (and
     the "Ranking…" placeholder is a single line), and a content-sized sheet
     jumped up and down each time. */
  .peaks { height: 46dvh; }
  /* Undo the desktop city-view band clamp that would otherwise squeeze it. */
  body.view-city .peaks { max-height: none; }
  .peaks-title { font-size: 22px; }
}
