/* =====================================================================================
   THE TWO FACES — self-hosted, no CDN (this is an offline piece).
   Space Grotesk speaks; Space Mono counts. The woff2 files are the same ones the author's
   other project ships (docs/TYPOGRAPHY_WC.md §A); they live in ./fonts and are declared
   here with font-display:swap so a cold load paints text immediately.
   ===================================================================================== */
/* SPACE GROTESK IS ONE VARIABLE FONT, not four weights.
   The four files copied over (spacegrotesk-400/500/600/700.woff2) are byte-identical —
   md5 87c506d8… all four — because they are four copies of the same variable master:
   `wght` axis 300–700, DEFAULT INSTANCE 300 (the file's own name is "Space Grotesk Light").
   Declared the WC way, as four single-value faces, every one of them would pin the axis to
   its own default and the whole piece would render Light with synthetic bolding on top.
   So it is declared ONCE across the real axis range: genuine 400/500/600/700, and one
   22 KB download instead of four. The other three files are left in fonts/ untouched. */
@font-face {
  font-family: 'Space Grotesk';
  font-style: normal;
  font-weight: 300 700;
  font-display: swap;
  src: url('fonts/spacegrotesk-400.woff2') format('woff2');
}
/* Space Mono ships as two real static cuts (Regular 400 / Bold 700, no fvar) — declared
   as two faces, which is what it actually is. */
@font-face { font-family: 'Space Mono'; font-style: normal; font-weight: 400; font-display: swap; src: url('fonts/spacemono-400.woff2') format('woff2'); }
@font-face { font-family: 'Space Mono'; font-style: normal; font-weight: 700; font-display: swap; src: url('fonts/spacemono-700.woff2') format('woff2'); }

:root {
  /* ---------- Type tokens ----------
     The strict role split, and the only two faces in the piece:
       --grot  every WORD: the display title, headings, UI, body copy, labels
       --mono  every NUMBER: years, dates, magnitudes, percentages, counts, axis ticks
     Numbers always carry font-variant-numeric: tabular-nums so digits never shift width. */
  --grot: 'Space Grotesk', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --mono: 'Space Mono', ui-monospace, 'SFMono-Regular', Consolas, monospace;

  /* LEGACY ALIASES. src/personal.js, src/skyview.js and src/audio.js write
     `font-family:var(--serif)` / `var(--sans)` INLINE on elements this sheet does not own
     (globe city names, skyline landmark names, the sound toggle). Those files belong to
     other hands, so the two old names survive here as aliases and their elements come
     across to Grotesk for free. Nothing in this sheet uses them any more. */
  --serif: var(--grot);
  --sans: var(--grot);

  /* DAY INVERSION: the app RESTS in a warm sepia day, so the chrome defaults to dark warm
     INK on the light ground. src/main.js interpolates these same variables toward the
     light-on-dark night palette from coronaK, keeping the chrome legible when the scene
     darkens into totality. See docs/DAY_INVERSION_DESIGN.md §5.
     The ink LADDER is the hierarchy (WC's --ink -> --ink-2 -> --dim -> --faint -> --fainter):
       --ink        the voice
       --ink-soft   body / secondary
       --ink-faint  quiet labels and index marks
       --line       the ONE hairline tone — rules, never boxes */
  --ink: rgb(58, 47, 36);          /* primary text — warm dark ink on the sepia day */
  --ink-soft: rgb(106, 92, 74);    /* secondary text */
  --ink-faint: rgb(138, 124, 104); /* tertiary */
  --line: rgba(92, 74, 52, 0.28);
  --panel-bg: rgba(236, 228, 212, 0.82);
  --panel-edge: rgba(120, 100, 74, 0.20);
  --ground: rgba(234, 224, 205, 1);   /* current sky colour — for glyph "holes" punched to it */
  --scrim: rgba(245, 238, 225, 0.72); /* theme-aware halo for plateless labels (light in day) */
  --total: #fff6da;
  --annular: #ff9a3c;
  --hybrid: #ffc98e;
  --partial: #6b86a8;
  --gold: rgb(184, 146, 70);       /* lighter amber (director 27 Jul: «жёлтый посветлее») */
  /* The clean warm amber the type glyphs are cut in — one rule, so the legend chips, the
     reading guide, the per-type diagrams, the tooltip and the city card can never drift. */
  --amber: color-mix(in srgb, var(--gold) 72%, #ffd08a);
}

* { box-sizing: border-box; }

/* ---------- Numbers ----------
   The one class that carries the number face into running prose. index.html already marks
   its numerals with it ("11,898 eclipses", "2000 BCE to 3000 CE"); see the follow-up note
   at the foot of this file for the JS-generated strings that still need it. */
.num {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
  letter-spacing: -0.01em;   /* Mono runs wide; pull it back inside a Grotesk line */
}

html, body {
  margin: 0;
  padding: 0;
  height: 100%;
  overflow: hidden;
  background: #eae0cd;      /* DAY INVERSION: warm sepia ground (the in-scene backdrop dome
                              paints the real day->night sky over the opaque canvas) */
  color: var(--ink);
  font-family: var(--grot);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* Warm sepia day ground behind the (now opaque) canvas — a coherent fallback before the
   in-scene backdrop dome draws. The dome carries the actual day->night darkening. */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  background:
    radial-gradient(120% 90% at 50% 42%, #f2ead9 0%, #ece2cf 45%, #e4d8c1 78%, #dcceb2 100%);
  z-index: -2;
}

#scene {
  position: fixed;
  inset: 0;
  width: 100vw;
  height: 100vh;
  display: block;
  touch-action: none;
}

/* ---------- Loader ---------- */
.loader {
  position: fixed;
  inset: 0;
  display: grid;
  place-items: center;
  background: #eae0cd;   /* DAY INVERSION: sepia loading ground, matching the resting day */
  z-index: 40;
  transition: opacity 1.1s ease;
}
.loader.hidden { opacity: 0; pointer-events: none; }
.loader-inner { text-align: center; }
.loader-dot {
  width: 9px; height: 9px; margin: 0 auto 20px;
  border-radius: 50%;
  /* DAY INVERSION: a dark warm-ink dot (the shadow) reads on the sepia loading ground. */
  background: #3a2f24;
  box-shadow: 0 0 22px 5px rgba(58, 47, 36, 0.28);
  animation: pulse 2.1s ease-in-out infinite;
}
@keyframes pulse {
  0%, 100% { transform: scale(0.7); opacity: 0.5; }
  50% { transform: scale(1.15); opacity: 1; }
}
.loader-text {
  font-family: var(--grot);
  font-size: 17px;
  font-weight: 500;
  letter-spacing: -0.005em;
  color: var(--ink-soft);
}

/* ---------- Masthead ----------
   The World Cup header, in three parts: an EYEBROW (a 34px tick rule + a wide-tracked
   uppercase mono edition line), the tight display TITLE, and the STANDFIRST. Nothing here
   is a plate; the whole block is one column of type in the ink ladder.
   HEIGHT IS LOAD-BEARING: the city column (#panel) starts directly below this block, so
   the sizes below are chosen against that budget and the two are measured against each
   other in dev/type_audit.mjs. Do not grow the title without moving .panel's top. */
.titleblock {
  position: fixed;
  top: 26px; left: 34px;
  z-index: 10;
  width: min(430px, 42vw);
  pointer-events: none;
}
/* Eyebrow: a short tick rule, then the edition line in the number face, uppercase and
   widely tracked. Baseline-aligned as one row so it costs a single line of height. */
.eyebrow {
  display: flex;
  align-items: center;
  gap: 11px;
  margin: 0 0 11px;
  font-family: var(--mono);
  font-size: 13px;
  font-weight: 400;
  line-height: 1;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--ink-faint);
  white-space: nowrap;
}
.eyebrow::before {
  content: "";
  flex: 0 0 auto;
  width: 34px; height: 1px;
  background: var(--line);
}
.titleblock h1 {
  font-family: var(--grot);
  font-weight: 600;
  font-size: clamp(27px, 2.45vw, 35px);
  line-height: 0.96;
  letter-spacing: -0.026em;    /* the display rule: the bigger the type, the tighter */
  margin: 0;
  color: var(--ink);
  text-wrap: balance;
}
.subtitle {
  margin: 11px 0 0;
  font-size: 14px;
  font-weight: 400;
  line-height: 1.5;
  letter-spacing: 0;
  color: var(--ink-faint);
  text-wrap: balance;      /* two even lines, never one word left stranded */
}

/* ---------- View tabs: Globe / City Sky ----------
   The piece's two top-level views. Quiet type, no plate, no border, no rounding —
   the active one is carried by ink weight + colour + a single gold hairline, the
   same language the city picker and the sky CTA already use. Centred at the top so
   it never crowds the title (left) or the legend (right). z above the sky overlay
   (40) so the viewer can always step back out to the globe. */
/* RELOCATED (27 Jul 2026): was centred at top:26/left:50% over the globe; now sits at the
   head of the LEFT column, directly above the city picker, left-aligned with it (34px) so
   the column reads as one panel: tabs -> picker -> card. Kept as its OWN fixed-position
   element (a sibling of #panel in the markup, not nested inside it) rather than folded
   into #panel's box: body.sky-open fades #panel to opacity:0 once the sky is actually
   entered, so the card disappears in City Sky mode, but the tabs must stay lit and
   clickable there — they are
   the only way back to Globe (see main.js's setView/skyviewApi wiring). z-index unchanged
   (45, above the sky overlay's 40) for exactly that reason. The matching left offset and
   the small gap below (margin-bottom) are what make the two independent fixed boxes read
   as a single continuous column instead of two coincidentally-aligned ones. */
/* REMOVED (30 Jul 2026): .viewtabs / .vt-btn — the whole [Globe | City Sky] tab bar and its
   filled-button exception to the no-plates rule. Director: "кнопка Сити Скай вообще не нужна.
   выбор города и все". src/main.js no longer builds or binds the bar and its #viewTabs markup
   is out of index.html; City Sky has its own way out (.cmb-leave) and the way IN is the card's
   "Step into <city>'s sky". Recoverable from git history. */

/* REMOVED (26 Jul 2026): the CHROME OF THE PARKED FLAT MODES — .modeswitch/.ms-btn (the
   old Globe/Catalogue/Spiral tab bar), .pano-labels and its .pl/.pl-title/.pl-tick axis
   marks, the whole .orient "you are here" marker, and .nowline-label. src/panorama.js,
   src/orient.js and src/nowline.js have been deleted along with their state machine in
   main.js and the aPanorama/aSpiral attributes in points.js, so nothing could ever have
   populated any of these again. ~130 lines, and their markup went from index.html with
   them. Recover the whole feature — modules, attributes, DOM and CSS, all four layers —
   from git tag `parked-flat-modes`; see docs/PROCESS.md §40.
   NOT REMOVED, and not related despite the shared prefix: `.personal-labels .pl-year`,
   which is the LIVE city layer's year label on the globe. */

/* ---------- Legend ---------- */
/* A real COLUMN with a left edge (B5), sized to its own CONTENT — the reading-guide brick
   that once forced a bounded, scrolling column is gone, so the box no longer runs down to
   the scrubber and no longer shares a bounding box with the corner ♪ toggle below it. The
   column is pointer-transparent so it never steals a drag from the globe; every child opts
   back in. */
.legend {
  position: fixed;
  top: 30px; right: 34px; bottom: auto;
  z-index: 10;
  width: 214px;
  display: flex;
  flex-direction: column;
  align-items: stretch;         /* left-aligned, deliberate rhythm (was flex-end) */
  gap: 0;
  text-align: left;
  pointer-events: none;
}
.legend > * { pointer-events: auto; }
/* The header: TYPE on the left, the collapse toggle on the right, on one baseline. */
.legend-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 14px;
  margin-bottom: 16px;
}
/* B6: "How to read" HIDES the legend body (a declutter toggle); the TOGGLE ITSELF stays so
   it can be brought back. main.js flips data-guide on click; the empty #legendGuide it also
   toggles is inert.
   "TYPE" GOES WITH IT (director, 30 Jul 2026: "когда убираем how to read, слово тип, тоже
   нужно убирать") — .legend-title lives in .legend-head, a SIBLING of .legend-body, so
   collapsing the body alone left "Type" behind with nothing under it. Collapsed now hides
   both; only the "How to read" control itself is left on the header row. */
.legend[data-guide="true"] .legend-body,
.legend[data-guide="true"] .legend-title { display: none; }
/* The collapsible body is the column's rhythm: type key, hairline, WHEN, hairline, About. */
.legend-body {
  display: flex;
  flex-direction: column;
  align-items: stretch;
}
/* The section label, WC's: Grotesk 600, uppercase, wide tracking, in the quiet ink. */
.legend-title {
  font-family: var(--grot);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

/* "How to read" toggle — WC's back-link treatment: uppercase, tracked, dim, brightening. */
.legend-help {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--ink-faint);
  font-family: var(--grot);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  cursor: pointer;
  transition: color 0.25s;
}
.legend-help:hover { color: var(--ink-soft); }
.legend[data-guide="true"] .legend-help { color: var(--gold); }
.lh-caret {
  width: 0; height: 0;
  border-left: 4px solid transparent;
  border-right: 4px solid transparent;
  border-top: 5px solid currentColor;
  opacity: 0.85;
  transition: transform 0.28s ease;
}
.legend[data-guide="true"] .lh-caret { transform: rotate(180deg); }

/* The reading-guide brick is GONE (B6). It duplicated the About page word for word — the
   "what it is / how to read" prose and the full sources list both live on about.html now —
   so it was deleted rather than moved. #legendGuide survives as an empty, hidden node only
   so main.js's toggle keeps binding to it; the collapse is driven by the legend's
   data-guide state (see above). No .legend-guide box, no scrollbar, nothing over the field. */
.legend-guide { display: none; }

/* =====================================================================================
   THE PHONE SHELL — a legend BUTTON and two BOTTOM SHEETS (30 Jul 2026)
   Director, after the first test on a real phone: "надо упрощать интерфейс. на тел. легенда
   сразу кнопкой, и на плашке выезжает. а то не видно." / "городу нужна карточка которая
   будет снизу на плашке. пару строк видно сразу. график хотя бы полных затмений. остальное,
   если вывести плашку наверх."

   Everything in this section is INERT above the phone breakpoint: the two new elements are
   display:none here and are switched on only inside the <=620px block far below, and the two
   utility classes are written by src/timeline.js's mountSheet, which only ever mounts at phone
   width. The desktop legend column and the desktop city card are untouched by any of it.

   THE PLATE. A bottom sheet is a plate, and the director asked for exactly that here ("на
   плашке"). It obeys the standing rules regardless: NO border, NO rounded top edge, NO shadow,
   nothing under 13px. It is separated from the globe by TONE — an opaque sepia surface built
   from the sheet's own two existing tokens (see the .panel-body / .legend sheet rules) — never
   by a rule or an edge.
   ===================================================================================== */

/* The legend's button. Phone only; see index.html's own note by #legendBtn. */
.legend-btn { display: none; }

/* The grip: the strip a finger grabs, and the line that says where the sheet goes. Phone only.
   No pill, no handle bar, no chrome — a caret of the same family as the legend's .lh-caret, and
   words. Its HEIGHT is the touch target (44px), bought with spacing, not with a box. */
.sheet-grip { display: none; }

/* The transition is on the TRANSFORM only, and only while settling: during a drag the class is
   off, so the sheet tracks the finger frame for frame with no easing between. */
.sheet-anim { transition: transform 280ms cubic-bezier(0.22, 0.61, 0.36, 1); }

/* ---------- WHEN: the time encoding, older -> recent ---------- */
/* B8: ~20 OVERLAPPING DISCS in the warm monochrome ramp the glyphs use, not a gradient bar.
   Set off from the type key above by one hairline and real air. */
.lg-when {
  margin-top: 18px;
  padding-top: 15px;
  border-top: 1px solid var(--line);
}
.lg-subhead {
  font-family: var(--grot);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-bottom: 11px;
}
.lt-discs {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
}
.lg-ramp-caps {
  display: flex;
  justify-content: space-between;
  margin-top: 8px;
}

/* ---------- The type key: four rows on two rails ----------
   B5: swatch + name on the LEFT rail, a quiet "i" on the RIGHT rail. The rows share one
   rhythm, the swatches line up, the "i"s line up — a deliberate column, not a heap. */
.lg-types {
  display: flex;
  flex-direction: column;
}
.lg-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}
.chip {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 5px 2px;
  border: 0;              /* house rule: no outlines, and no rounded pill */
  background: transparent;
  color: var(--ink-soft);
  font-family: var(--grot);
  font-size: 13.5px;
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  cursor: pointer;
  transition: color 0.25s, background 0.25s, opacity 0.25s;
  user-select: none;
}
.chip:hover { color: var(--ink); background: transparent; }
/* Clicking a type ISOLATES it: the pressed chip is the one type on screen, every other
   chip reads as switched off. Both states are carried by aria-pressed, so the visual
   and the accessible state can never drift apart. */
.chip[aria-pressed="true"] {
  color: var(--gold);          /* isolated type: carried by ink + weight, never a pill */
  font-weight: 700;
  background: transparent;
}
.chip[data-off="true"] { opacity: 0.34; }
.chip[data-off="true"] .sw { opacity: 0.3; }
.chip:focus-visible { outline: none; color: var(--gold); }

/* ---------- The per-type "i" (opens its card over the legend, on CLICK) ----------
   REVERSED (director, 27 Jul 2026): was hover-to-open; hover now does NOTHING but the usual
   colour tell that the mark is interactive — a click opens the card, a second click on the
   same "i", the card's own × , clicking anywhere else, or Escape all close it (see the inline
   script at the foot of the legend in index.html). A single lowercase mono "i" on the row's
   right rail. NO circle, NO ring, NO rounding — but the "i" keeps its HAIRLINE OUTLINE (a
   sanctioned exception to the no-borders rule) so it still reads as a control worth pressing.
   Dim until touched, gold (ink + box) on hover/focus/open. */
.lg-i {
  flex: 0 0 auto;
  margin: 0;
  padding: 4px 6px;
  border: 1px solid var(--ink-faint);
  border-radius: 0;
  background: none;
  font-family: var(--mono);
  font-size: 13px;
  font-weight: 700;
  line-height: 1;
  color: var(--ink-faint);
  cursor: pointer;
  transition: color 0.2s ease, border-color 0.2s ease;
}
.lg-i:hover,
.lg-i:focus-visible,
.lg-i[aria-expanded="true"] { color: var(--gold); border-color: var(--gold); outline: none; }

/* ---------- B3 + B4: the per-type explanation CARDS over the legend ----------
   REVERSED (director, 27 Jul 2026): now CLICK to open (not hover), and now ON A PLATE — a
   real filled sepia panel, a sanctioned one-off exception to the no-plates house rule (he
   asked for it "на плашках" explicitly), using the same --panel-bg token the filled view
   tabs already use so the fill stays inside the piece's own palette. Still no loud border,
   still no shadow. Open/closed state is a plain class (.is-open) toggled by the small inline
   script at the foot of #legend in index.html — :hover/:has() could open on hover but could
   not cleanly do "click outside to close", so this one interaction gets the few lines of JS
   the task calls for. One card open at a time; the legend body clears while one is open. */
.lg-cards {
  position: absolute;
  top: 30px; left: 0; right: 0;    /* overlay the body region, just under the header */
  z-index: 4;
  pointer-events: none;
}
/* THE CARD IS WIDER THAN ITS COLUMN (2 Aug 2026, third pass). The legend is 214px, which left the
   drawing 162 — and at 162 the Sun–Moon–Earth cone was three blobs and two dashed lines, which is
   why the second pass cut it. The director sent it back («а почему из карточек пропало главное??
   схема солнца, луны и земли»), and he is right: the geometry is the only thing on the card that
   says why there are four kinds at all. So the fault is fixed where it actually was — the plate
   unpins from the column's left edge and grows LEFTWARD over the globe to 324px, which gives the
   drawing 272: the same width the phone sheet already gives it, so one figure is drawn at one
   size on both surfaces. It stays anchored to the legend's right edge, so it still reads as that
   column's card, and the globe behind it is exactly what the plate is for. */
.lg-card {
  position: absolute;
  top: 0; left: auto; right: 0;
  width: 324px;
  text-align: left;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity 0.18s ease, visibility 0s linear 0.18s;
  background: var(--panel-bg);
  padding: 16px 34px 18px 18px;   /* extra right padding clears the × */
}
.lg-card.is-open {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transition: opacity 0.18s ease, visibility 0s;
}
/* A SHORT WINDOW MUST NOT LET THE CARD RUN OFF THE BOTTOM (2 Aug 2026, with the drawn
   explainer: the card grew from ~220px of prose to a drawing plus prose; with the cone back as
   the main figure it runs 423–445). The plate is absolutely positioned from the legend's own
   top, so on a short viewport it would simply continue past the fold with no way to reach the
   end of it. It scrolls instead. At 900px — the size this is judged at — the cap is 804px
   against a tallest card of 445 (measured), so nothing scrolls and nothing about the desktop
   reading changes. */
.lg-card {
  max-height: calc(100vh - 96px);
  overflow-y: auto;
  overscroll-behavior: contain;
}
/* The close × — plain mono glyph, no circle/plate of its own (the card's plate is enough). */
.lg-card-close {
  position: absolute;
  top: 8px; right: 8px;
  width: 22px; height: 22px;
  display: flex; align-items: center; justify-content: center;
  padding: 0;
  border: 0;
  background: none;
  font-family: var(--mono);
  font-size: 15px;
  line-height: 1;
  color: var(--ink-faint);
  cursor: pointer;
  transition: color 0.2s ease;
}
.lg-card-close:hover,
.lg-card-close:focus-visible { color: var(--gold); outline: none; }
/* While any card is open, clear the legend body so the card reads alone.
   THE ABOUT LINK GOES WITH IT (2 Aug 2026). It used to sit below the card's old ~220px height
   and never met it; the drawn explainer is twice that, and the plate is deliberately a
   TRANSLUCENT sepia (--panel-bg, 0.82) — so "ABOUT THIS PROJECT" read straight through the
   middle of the card's own prose. It is the same declutter the body already gets, for the same
   reason, and it comes back the instant the card closes. (The link moved OUT of .legend-body on
   31 Jul so that collapsing "How to read" would not hide it — that is a different state, and
   this rule is scoped to data-card-open, so the two do not interfere.) */
.legend[data-card-open="true"] .legend-body,
.legend[data-card-open="true"] .lg-about { opacity: 0; pointer-events: none; }

/* The GEOMETRY diagram: Sun (with its light rays) · Moon · Earth, the umbra/penumbra shadow
   cones drawn to the Earth, all in the piece's own sepia/amber ink. Lines are content here,
   not chrome, so they are allowed — but no photo, no frame, no rounding, no shadow. Every
   tone is mixed from the theme tokens, so the diagram inverts with the day↔night scene.

   IT IS THE CARD'S MAIN FIGURE AGAIN (2 Aug 2026, third pass). The second pass cut it from the
   legend's "i" cards; the director sent it straight back — «а почему из карточек пропало
   главное?? схема солнца, луны и земли» — and the diagnosis was wrong the first time. At 162 CSS
   px this drawing is three circles and two dashed lines; at 272, which the widened plate now
   gives it, the Sun is 45px across and the cone is a cone. The idea was never the problem, the
   room was. Both surfaces draw it now, at 272 on the card and a 200px column on About.
   The rules below live in this sheet rather than in src/explain-figures.css because the
   d-* alphabet is shared property and must not fork — see the note over .ex-swatch. */
.geo {
  display: block;
  width: 100%;
  max-width: 272px;
  height: auto;
  margin-bottom: 4px;
  color: var(--amber);
  overflow: hidden;
}
.d-sun   { fill: currentColor; }
.d-earth { fill: color-mix(in srgb, var(--ink) 20%, transparent); }
.d-moon  { fill: var(--ink); }
.d-umbra { fill: color-mix(in srgb, var(--ink) 30%, transparent); }
.d-anti  { fill: color-mix(in srgb, var(--amber) 20%, transparent); }
.d-ray   { stroke: currentColor; stroke-width: 1.4; }
.d-pen   { stroke: color-mix(in srgb, var(--amber) 58%, transparent); stroke-width: 1; stroke-dasharray: 3 2.5; }
.d-spot  { fill: var(--ink); }
/* ---- THE PENUMBRA AS A REGION, AND BOTH REGIONS NAMED (3 Aug 2026) --------------------------
   Director: «в схемах ну умбру и пенумбру можно на схемах изобразить??» The cone was already
   here and the prose already carried the whole idea — the shadow cone, the dark tip that reaches
   the ground, the corridor it draws — but the drawing distinguished none of its parts, so the
   words `umbra` and `penumbra` in the text had nothing to point at. The umbra was filled; the
   penumbra was two dashed edges with nothing between them, which is not a region, it is a hint.
   IT IS LIGHTER THAN THE UMBRA AND IT HAS TO STAY THAT WAY, because that difference is the only
   thing the figure is saying: 12% amber against the umbra's 30% ink. Same family as .d-anti, one
   step down — the antumbra is where the cone has REOPENED and light is getting back in, so the
   two belong to the amber side of the alphabet and the umbra alone to the ink side. */
.d-penumbra { fill: color-mix(in srgb, var(--amber) 12%, transparent); }
/* The names, set ON the drawing. 11 viewBox units, and that is a floor calculation rather than a
   taste one: `.geo` is `width:100%; max-width:272px` and MEASURED (dev/explainer_size.mjs) it is
   272 CSS px at 1280x900, 576x1280, 390x844 and 360x640 alike — the plate never gives it less —
   so one unit is 1.259 px at every width the card is ever drawn at and 11 units land at 13.85 px,
   over the piece's 13px floor with room to spare. Letter-spaced small caps, the same treatment
   .lg-plate-name already uses, so a label reads as a label and not as data. */
.d-lab  { fill: var(--ink-faint); font-family: var(--grot); font-size: 11px; font-weight: 600;
          letter-spacing: 0.09em; }
.d-lead { stroke: var(--ink-faint); stroke-width: 0.9; stroke-dasharray: 2.5 2.5; }
/* ---- THE MARK BESIDE THE CONE: A MOON IN FRONT OF A SUN ------------------------------------
   «изображение солнца надо более понятно. а не кружочек и линии вокруг (не читается как затмение
   вообще). переделывай» — and he is right about the old one: it was a ring of straight spokes
   with PAPER in the middle, because the house rule at the time was that the Moon is the paper and
   there is no black field anywhere. That rule was written to keep photographic black out of a
   piece drawn on warm paper, and it survived into a mark whose entire job is to say "something is
   in front of the Sun". With nothing in the middle, nothing is.
   SO THE MOON IS DRAWN, in `--ink` — the SAME fill the Moon already takes in the cone figure
   directly above it (.d-moon), not a new black and not a photograph's black. One body, one ink,
   in both halves of the card. .d-sundisc is the Sun's own amber underneath it, which is what
   makes the annular's ring and the partial's bite read as one disc over another rather than as a
   shape cut out of a disc. */
.d-moondisc { fill: var(--ink); }
.d-sundisc  { fill: var(--amber); }
/* The inner corona, hugging the limb — the low, bright collar every photograph of totality has
   under the streamers. It is what stops the rays reading as spokes on a hub. */
.d-inner    { fill: color-mix(in srgb, var(--amber) 42%, transparent); }

/* ---- WHAT LANDED, marked on the Earth's own limb -------------------------------------------
   ONE alphabet across all four diagrams, and it is categorical rather than tonal — a footprint
   mark says WHICH shadow arrived, never how dark it was (that claim belongs to the band figure
   below, which carries the piece's real numbers):
     solid, heavy   the UMBRA landed          (total; the hybrid's middle)
     dashed, heavy  the ANTUMBRA landed       (annular; the hybrid's two ends)
     thin, light    only the PENUMBRA landed  (partial — nothing dark reaches the ground at all)
   Drawn as strokes along the globe's near limb, so the mark is on the surface rather than
   floating beside it. */
.d-foot-tot { fill: none; stroke: var(--ink); stroke-width: 3.2; }
.d-foot-ann { fill: none; stroke: var(--ink); stroke-width: 3.2; stroke-dasharray: 2.2 2; }
/* The penumbra's is a WASH over the part of the globe it actually covers, not a line along the
   limb: the region is most of the visible face (the penumbra's lower edge cuts the disc near its
   centre), and drawn as a stroke it read as an OUTLINE around the Earth — which is both the one
   thing the house rules forbid and a claim about a boundary the shadow does not have. */
.d-foot-pen { fill: var(--ink); fill-opacity: 0.17; stroke: none; }

/* =====================================================================================
   THE SWATCH, AND BESIDE IT THE SUN — the row that sits under the cone on a legend card.
   (director, 2 Aug 2026: «тень можно просто квадратиком где-то в углу, чтобы человек понял,
   что такое этот паттерн» — the shadow can be just a little square somewhere in a corner, so
   a person understands what that pattern is.)

   THE BAND IS NOT AN EXPLANATION HERE, IT IS A KEY. The cone above it explains; this chip only
   has to make a corridor met later on the globe RECOGNISABLE. So it is a plain rectangle of
   this type's own ink — no ground, no curve, no corridor — at the tone the shipped shader gives
   it (BAND_PATTERN_GLSL, src/paths.js), never a tone invented for the drawing:
     · total    solid, full ink                    the umbra reached the ground
     · annular  filled FLAT at its own obscuration  flat, never graded and never open along the
                (median 0.907; across the           axis: obscuration is constant inside a path
                catalogue's 3,956 annulars the      of annularity (1994 May 10, off this piece's
                ceiling runs 0.824..1.000)          own elements: 0.88950 axis, 0.88930 limit)
     · hybrid   solid: its ends sit at 0.98..1.00,  so the change lives in the SKY, not the band,
                which is not a visible difference   and the card says so in as many words
     · partial  ONE 1-1 dot screen, its ink turned  the faintest of the four by construction —
                down to the shipped quiet factor    the pattern says which kind of shadow, the
                0.522 (screenQuiet, shadowfill.js)  weight says how much it mattered

   WHY THE CHIP IS SIZED IN ABSOLUTE PIXELS, and this is the one thing on the card that must not
   be allowed to drift. The partial's key IS its grain; shrink it until the cells go sub-pixel
   and the whole thing collapses into a flat grey smudge that teaches nothing and is worse than
   no key at all. An SVG <pattern>'s cell is an ATTRIBUTE, not a CSS property, so it cannot
   follow a percentage box — the old figure had to accept 2 CSS px on the desktop card and
   1.26x that in the phone sheet. Here the svg carries width/height attributes equal to its
   viewBox (52x22), so 1 user unit is 1 CSS px on EVERY surface, the cell is exactly the shipped
   BAND_DOT_CSS of 2, and 52x22 buys 26x11 whole cells — measured to survive at DPR 1, 2 and 3
   (dev/explain_shots.mjs). shape-rendering:crispEdges is on the svg for the same reason: it
   stops a plate landing on a fractional pixel from antialiasing the checker into porridge.

   THE SUN IS DRAWN IN INK AND ABSENCE, NEVER ON A BLACK FIELD. The rejected version made the
   Moon a solid ink disc, and in a piece that is warm sepia daylight from end to end that reads
   as a hole punched in the page. So: what is LEFT of the Sun is amber, and what the Moon took
   is the paper. The four cases are the director's own three options, one apiece —
     total    свечение   the corona alone; the disc it surrounds is the page
     annular  кольцо     an amber ring at the catalogue's median annular magnitude 0.9523
     hybrid   both       the glow (mid-track) beside the ring (the two ends)
     partial  закрытое солнце не до конца — the Sun's amber disc with the Moon masked out of
              it as paper, at the catalogue's median partial magnitude 0.4728
   — which is also what makes the four legible at a glance: a spiked halo, a closed thin ring,
   a pair, and a bitten disc are four different silhouettes before they are four categories. */

/* The row under the cone: the key in the bottom-LEFT corner of the figure block, the Sun pushed
   to the far right by `margin-left:auto`, and the air between them doing the separating. Both
   are sized absolutely rather than by percentage — the chip because its grain depends on it (see
   above), the Sun because the two must not drift apart as the card's width changes. */
.ex-row {
  display: flex;
  align-items: center;
  gap: 16px;
  margin: 10px 0 14px;
  max-width: 272px;
}
.ex-swatch {
  display: block;
  flex: 0 0 auto;
  /* No width/height here ON PURPOSE: the attributes on the element are the contract. */
}
.ex-sun {
  display: block;
  flex: 0 0 auto;
  /* IT USED TO BE `margin-left: auto`, pushing the Sun to the far right with the band chip in the
     left corner and air between them. The chip is gone (3 Aug 2026, «чёрный квадратик всё-таки
     убираем»), so the row holds one figure — and one figure pushed to the right of an empty row
     reads as a thing that has drifted. It sits under the cone's own left edge now, which is where
     the drawing above it starts. */
  height: 66px;
  width: auto;
  color: var(--amber);
  overflow: visible;
}
.d-ground  { fill: none; stroke: var(--line); stroke-width: 1; }
.d-band    { fill: var(--ink); }
.d-band-ceil { fill: var(--ink); fill-opacity: 0.907; }   /* the median annular's own ceiling */
/* The screen's cells. The OPACITY IS THE SHIPPED QUIET FACTOR, not a look: screenQuiet() in
   src/shadowfill.js derives 0.522 from the live age ramp's own ends so that the heaviest
   possible partial still lands under the lightest possible total, and bandPattern() multiplies
   the 1-1 grid by exactly that. Drawing the cells at full ink — which is what shipped here
   before — made the explainer's partial twice the weight of the globe's, and the loudest of
   the four instead of the quietest.
   STILL USED BY about.html's own three-figure chain, which is why it stays exactly as it was.
   The LEGEND's swatch no longer uses it — see .d-grain below. (about.html's figure is now the
   last checker left in the piece and will want the same treatment; it is another file.) */
.d-scr     { fill: var(--ink); fill-opacity: 0.522; }
/* THE GRAIN, in the legend's partial key. Its opacity is PER CELL and lives on each rect in
   index.html, because the band it stands for is no longer a 1-1 grid at one tone: every cell
   carries ink, at 0.34-0.86 of what the band is handed. The fence above is still the whole of it —
   each cell is that 0.522 times its own grain value, i.e. 0.1775 + 0.2714*g, 0.178 to 0.449. So
   this rule carries the INK and nothing else: a fill-opacity here would beat the presentation
   attribute on every rect (a CSS declaration outranks a presentation attribute) and flatten the
   grain back to one flat tone, which is the exact fault it exists to end. */
.d-grain   { fill: var(--ink); }
/* The reader, and their line of sight, on the About page's three-figure chain only — the cards
   have no line to draw now that the two marks sit side by side. Gold, because it is the one
   thing in the figure that is a person rather than a body: it reads over a total's solid ink. */
.d-tick    { stroke: var(--gold); stroke-width: 1.6; }
.d-sight   { fill: none; stroke: var(--gold); stroke-width: 1; stroke-dasharray: 3 2.5; }
/* THE SUN, in the piece's own daylight. An ILLUSTRATION of the corona, exactly as the About page
   already says of the piece's own — a ring of spokes, no photograph, nothing claimed about a
   particular eclipse's shape. There is deliberately NO class for the Moon: the Moon is the paper
   (a mask, or simply the middle of the ring), which is what took the black out of these figures. */
/* 1.4 viewBox units, not 1: the card renders this drawing at about 0.77 (an 86-unit box at the
   66px .ex-sun height), so a 1-unit spoke was well under a CSS pixel and the whole corona went
   wispy at 1x — the one place where the compact surface forced a number on the shared alphabet.
   1.4 lands at 1.07 CSS px on the card and 1.3 on About, and the opacity carries the rest. */
/* THE OPACITY IS PER RAY NOW, AND SO IT MUST NOT BE SET HERE (3 Aug 2026). It was a flat 0.55 for
   every spoke, which is what made the old mark read as a gear wheel rather than a corona: a real
   corona is not one tone, it is bright in the equatorial streamers and thin over the polar holes,
   and that unevenness is the whole of what makes it legible as a corona instead of a halo. Each
   line now carries its own `stroke-opacity` from the same profile that sets its length (see
   dev/build_explainer_figs.mjs, which quotes src/corona.js's own terms). A declaration here would
   BEAT those presentation attributes — a CSS rule outranks a presentation attribute — and flatten
   the lot back to one tone, which is exactly the fault this exists to end. The same trap is
   already documented over .d-grain; this is the second element to fall into it. */
.d-corona  { stroke: currentColor; stroke-width: 1.4; }
.d-sky     { fill: currentColor; }      /* what is left of the Sun */
.d-ring    { fill: none; stroke: currentColor; }   /* stroke-width per instance: it IS the ring */

/* The one honest caveat the drawing owes. THE CONE IS BACK, so the DISTANCE caveat is back with
   it: any diagram of this kind is out by about 25× (About carries the arithmetic — 107.5 solar
   diameters and 110.7 lunar, against 4.5 and 4.8 as drawn). What is NOT schematic is the ink in
   the chip, which is the globe's own. So the line says exactly that
   and no more. */
.ex-scale {
  margin: 10px 0 0;
  font-family: var(--grot);
  font-size: 13px;
  line-height: 1.5;
  color: var(--ink-faint);
}

.lg-plate-name {
  font-family: var(--grot);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--gold);
  padding-bottom: 8px;
  border-bottom: 1px solid var(--line);   /* the one hairline: a rule, never a box */
  margin-bottom: 10px;
}
.lg-plate-text {
  margin: 0;
  font-family: var(--grot);
  font-size: 13.5px;
  font-weight: 400;
  line-height: 1.62;
  letter-spacing: 0;
  color: var(--ink);
}
.lg-plate-text b { color: var(--gold); font-weight: 600; }
.lg-plate-text i { font-style: italic; }

.legend-filter-note {
  font-size: 13px;
  line-height: 1.5;
  letter-spacing: 0;
  color: var(--ink-soft);
  text-align: left;
  margin-top: 10px;
}

/* B9: the About link, set CLEAR of the WHEN block above it — a hairline and real air, so it
   no longer reads as tacked onto the ramp. (Its type/colour treatment lives in about.css.)
   SELECTOR WIDENED from `.legend-body .lg-about` to `.legend .lg-about` (31 Jul 2026, the
   collapse-follows-collapse fix): the link moved OUT of .legend-body in index.html so B6's
   collapse (which hides .legend-body) no longer swallows it — see index.html's own note by
   .lg-about. It is still the last thing in the column, just one level shallower, so this rule
   still finds it and nothing about its look changes. */
.legend .lg-about {
  display: block;
  margin-top: 22px;
  padding-top: 16px;
  border-top: 1px solid var(--line);
}

/* ---------- B10: the ambient-sound toggle ----------
   WHAT WAS WRONG, ROUND ONE. This block used to reveal the control only `body:not(.intro-play)`
   and force `opacity: 0; pointer-events: none` under BOTH `body.intro-play` and `body.sky-open`.
   So the one moment a visitor most needs to kill the music — the opening, which starts playing
   at boot — was the one moment the control was deliberately unreachable, and it vanished again
   inside City Sky. Measured live before that fix (dev/sound_probe.mjs, 1280x900): during the
   intro `.eclipse-sound` was `opacity 0 / pointer-events none`, `elementFromPoint` at its own
   centre returned the canvas. Directors' notes 26 Jul ("the sound mute icon is missing") and
   31 Jul ("кнопку выключения звука надо добавить, причем где-то уже на заставке") are both this.
   Fixed: the control is present and live from first paint to last frame, through the loader,
   the whole opening, the resting globe, a chosen city and City Sky. Nothing hides it any more.

   WHAT WAS WRONG, ROUND TWO (still 31 Jul). Once it existed it was a bare 30x30 speaker glyph —
   "кнопка музыки что с ней? стоит странно. нужно Music on off, очень заметно, думаю в районе
   легенды." A glyph alone does not say what it does or what state it is in; --ink-soft at rest
   read as a decoration, not a control worth pressing.
   FIXED NOW: the control carries its state in WORDS ("Music on" / "Music off", `.es-label`,
   `#soundLabel`), promoted to the piece's full-voice `--ink` (not a utility tone), and moved to
   sit directly under the legend column, sharing its exact box. "Very visible" is bought with
   placement, size and tone only, per house rule — no border, no plate, no rounding were added.

   WHERE, AND WHY THERE. `right: 34px; width: 214px` is the LEGEND'S OWN BOX (see `.legend`
   above), reused verbatim rather than centred independently — the control's left and right edges
   land exactly on the legend's, so it reads as the next row of that same rail, not a floating
   icon somewhere else on the page.

   TWO TOP VALUES, NOT ONE (31 Jul 2026, third pass — director: "когда how to read сворачиваем,
   about the project и music, они должны как бы подтянуться туда наверх... если мы его
   сворачиваем, то Music тоже поднимается вместе с About the Project"). Collapsing "How to read"
   (B6) now hides `.legend-body` and lets `.lg-about` ride up to sit right under the header (see
   index.html and `.legend .lg-about` above) — so the column has two real heights, and Music has
   to know which one it is following. This rule is the EXPANDED value (the default, and what the
   intro's own frozen `data-guide="false"` always shows — see below); the COLLAPSED value lives in
   its own rule right after this one, keyed by `#legend[data-guide="true"] ~ .eclipse-sound`.
   THAT SELECTOR IS WHY THIS CONTROL IS A SIBLING OF #legend, NOT ITS CHILD — see index.html's
   own note on `#soundToggle` for the two reasons nesting it was rejected (the intro's opacity
   fade, the phone sheet's transform). Being a sibling means there is no layout relationship CSS
   can use directly (a `position: fixed` box does not flow after anything) — but `#legend` and
   `#soundToggle` are both direct children of `<body>`, and this one comes AFTER `#legend` in the
   markup, so the plain CSS general-sibling combinator (`~`) can read the legend's OWN
   `data-guide` attribute and switch the fixed offset accordingly. No JavaScript measures
   anything; the two numbers are measured once, by hand, and hard-coded, same as `right: 34px`
   itself always has been.
   Measured, not guessed (dev/legend_collapse_probe.mjs):
     desktop 1280x900   .legend expanded    [1032, 30, 214, 298]  -> bottom edge 328
                        .legend collapsed   [1032, 30, 214,  90]  -> bottom edge 120
                        .intro-skip  [1189, 840, 63, 32]   (opening only, bottom-right)
                        .cmb-locator bottom 102px + 132px  -> top edge 666 in City Sky
       => top: 344px (expanded) puts the control 16px under the legend; top: 136px (collapsed)
          puts it the same 16px under the shorter column. Both leave hundreds of pixels clear of
          the locator, and neither ever gets near .intro-skip.
     phone 390x844: kept on the existing top rail beside the Legend button, entirely unaffected —
       see the phone media query below for that geometry (the label there is necessarily shorter:
       real estate is the 390px frame itself, not a choice). The legend's collapse state does not
       exist on a phone (data-guide is ignored there — see the phone "(B) phone" block), so there
       is only ever one phone position, same as before this pass.
   Z-INDEX 46. It has to clear the City Sky overlay (40), that overlay's loading plate, the phone
   `.panel` lift inside City Sky (45) and the boot loader (40, whose ground is the same #eae0cd
   sepia this ink is drawn for). Nothing else in the sheet is higher.

   TONE. `--ink` at rest — the voice, the same weight the legend's own "TYPE" and hero numerals
   carry, promoted from the old --ink-soft specifically because "очень заметно" asked for more
   than a utility reads as. `--gold` on hover/focus-visible like every other control here. The
   caveat line (see below) stays one rung down, `--ink-faint`, so the state word is always the
   loudest thing in the control. No plate, no border, no rounding, no shadow. currentColor
   carries the mark; main.js's day->night interpolation of --ink and --ink-faint carries both
   the mark and the words together, exactly as it carries the rest of the type. */
.eclipse-sound {
  position: fixed;
  right: 34px;
  top: 344px;
  width: 214px;
  z-index: 46;
  display: flex;
  align-items: center;
  gap: 11px;
  margin: 0;
  padding: 6px 0;
  border: 0;
  border-radius: 0;
  background: none;
  color: var(--ink);
  cursor: pointer;
  text-align: left;
  -webkit-user-select: none;
  user-select: none;
  transition: color 0.3s ease;
}
/* COLLAPSED: About's own height (header + About, body gone) is much shorter than the full
   column, so Music has to sit much higher to stay "under this block" instead of leaving the gap
   where the type key and WHEN ramp used to be. See the comment above `.eclipse-sound` for why
   this is a sibling combinator and not a JS-measured value.
   DESKTOP ONLY, EXPLICITLY (`min-width: 621px`, the same cutoff the phone "(B) phone" block
   itself uses). `#legend[data-guide]` is an ID + attribute selector — high enough specificity
   that, unscoped, it would beat the phone media query's own `.eclipse-sound { top: 60px }` rule
   regardless of source order, EVEN THOUGH data-guide can never be toggled by a phone visitor
   (`.legend-help` is `display: none` there). Caught live: `data-guide="true"` left over from a
   desktop session (or set by a headless test) still matched on a 390px reload and picked this
   rule over the phone one, because an id+attribute selector always outranks a single class no
   matter which one comes later in the file. Scoping this to the same breakpoint the phone block
   uses removes the possibility entirely rather than relying on source order. */
@media (min-width: 621px) {
  #legend[data-guide="true"] ~ .eclipse-sound {
    top: 136px;
  }
}
/* A CARD IS OPEN: Music STEPS DOWN rather than being buried (2 Aug 2026). The explanation cards
   became drawings and grew to ~430px, so at the resting 344px this control ended up inside the
   card's own text — and at z-index 46 it painted OVER it, which was the worst of both. It moves
   to just under the tallest card instead, by the SAME sibling-combinator mechanism the collapse
   offset above uses (no JS measuring anything), and moves back the moment the card closes.
   RE-MEASURED 2 Aug, third pass: with the Sun–Moon–Earth cone back as the card's main figure the
   tallest card is 445 from its own top of 60, i.e. it ends at 505 — so 500 put this control back
   inside the caveat line it was moved out of. 520 clears it with air.
   Music therefore stays present and hittable on this screen too, which is the standing
   requirement for this control — see its own comment and dev/sound_probe.mjs.
   THE HEIGHT GUARD is the point of the second query: below ~600px of viewport there is no room
   to put it under the card at all, so it simply stays where it is and the card (which is capped
   and scrolls at that size, see .lg-card) may overlap it. That is the honest trade at a window
   height the piece is not laid out for; above it, nothing overlaps. */
@media (min-width: 621px) and (min-height: 600px) {
  #legend[data-card-open="true"] ~ .eclipse-sound {
    top: 520px;
  }
}
.eclipse-sound:hover,
.eclipse-sound:focus-visible { color: var(--gold); outline: none; }
.es-mark {
  display: block;
  width: 27px;
  height: 27px;
  flex: 0 0 auto;
}
.es-cone { fill: currentColor; }
.es-waves { stroke: currentColor; }
.es-slash { stroke: currentColor; display: none; }

/* THE WORDS. `.es-label` is the state, always the loudest line in the control — bigger and
   heavier than the legend's own "TYPE"/"How to read" (13px/600) precisely so it reads as more
   than a utility. `.es-caveat` is the pre-gesture honesty line (see index.html and audio.js's
   _reflectControl): present only while sound is "on" but the AudioContext has not actually been
   allowed to run yet, gone the instant it has (or whenever sound is "off" — there is nothing to
   qualify about silence). `:empty` removes it from flow entirely rather than leaving a blank
   line, so the row is exactly one line tall once the caveat clears — no layout jump for anything
   fixed-positioned below it, because nothing else on this rail is. */
.es-text {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
  min-width: 0;
}
.es-label {
  font-family: var(--grot);
  font-size: 15px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  line-height: 1.15;
  color: currentColor;
}
.es-caveat {
  font-family: var(--grot);
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 0;
  text-transform: none;
  color: var(--ink-faint);
  line-height: 1.25;
  text-wrap: balance;
}
.es-caveat:empty { display: none; }

/* SOUND OFF — the state is carried by the mark itself, never by a caption or a colour alone:
   the waves go, the slash appears, and the mask cuts a channel for it through the cone so the
   strike reads as a strike instead of merging into the ink it crosses. `data-sound` is written
   on <html> by index.html's own inline script (before first paint, from localStorage) and kept
   there by src/audio.js — so a visitor who muted last time sees the struck mark in the first
   painted frame, with no flash of the wrong state. */
html[data-sound="off"] .es-waves { display: none; }
html[data-sound="off"] .es-slash { display: block; }
html[data-sound="off"] .es-cut { stroke: #000; }
/* Type is encoded by SHAPE, never by colour. ONE clean warm amber carries every swatch —
   the legend chips, the reading guide, the tooltip and the city card all draw from this
   single rule, so the card can never drift to its own (muddier) tone.
   Each shape is CUT WITH A MASK, not drawn with a border or a gradient blend:
     - no outlines anywhere (house rule),
     - holes are genuinely transparent, never a plug painted in the ground colour,
     - a PARTIAL is a BARE crescent with NO enclosing circle — the same alphabet the
       timeline lanes and the WebGL globe points already speak. */
.sw {
  width: 14px; height: 14px;
  display: inline-block;
  box-sizing: border-box;
  flex: 0 0 auto;
  border: none;
  border-radius: 0;
  /* Clean warm amber. Lifted off the raw --gold (which reads brown-grey at swatch size on
     the sepia day ground) toward a brighter, warmer ink, while still riding the day->night
     interpolation of --gold so it stays right in both themes. */
  background: var(--gold);
  background: var(--amber);
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  -webkit-mask-position: center;  mask-position: center;
  -webkit-mask-size: 100% 100%;   mask-size: 100% 100%;
}
/* TOTAL — a solid filled disc: the Sun is gone. */
.sw-total {
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'%3E%3Cpath d='M0 7a7 7 0 1 0 14 0a7 7 0 1 0 -14 0Z'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'%3E%3Cpath d='M0 7a7 7 0 1 0 14 0a7 7 0 1 0 -14 0Z'/%3E%3C/svg%3E");
}
/* ANNULAR — a filled ring of fire with the Moon PUNCHED clean through it. The hole is real
   transparency (evenodd), so it shows the scene behind rather than a sepia plug. */
.sw-annular {
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'%3E%3Cpath fill-rule='evenodd' d='M0 7a7 7 0 1 0 14 0a7 7 0 1 0 -14 0ZM3.3 7a3.7 3.7 0 1 0 7.4 0a3.7 3.7 0 1 0 -7.4 0Z'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'%3E%3Cpath fill-rule='evenodd' d='M0 7a7 7 0 1 0 14 0a7 7 0 1 0 -14 0ZM3.3 7a3.7 3.7 0 1 0 7.4 0a3.7 3.7 0 1 0 -7.4 0Z'/%3E%3C/svg%3E");
}
/* HYBRID — a near-solid disc carrying ONE hairline notch at ~2/3 of the radius: total along
   the middle of its track, a thread of ring toward the ends. The notch is cut, not painted. */
.sw-hybrid {
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'%3E%3Cpath fill-rule='evenodd' d='M0 7a7 7 0 1 0 14 0a7 7 0 1 0 -14 0ZM2.25 7a4.75 4.75 0 1 0 9.5 0a4.75 4.75 0 1 0 -9.5 0ZM2.85 7a4.15 4.15 0 1 0 8.3 0a4.15 4.15 0 1 0 -8.3 0Z'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'%3E%3Cpath fill-rule='evenodd' d='M0 7a7 7 0 1 0 14 0a7 7 0 1 0 -14 0ZM2.25 7a4.75 4.75 0 1 0 9.5 0a4.75 4.75 0 1 0 -9.5 0ZM2.85 7a4.15 4.15 0 1 0 8.3 0a4.15 4.15 0 1 0 -8.3 0Z'/%3E%3C/svg%3E");
}
/* PARTIAL — a BARE crescent, and nothing else. NO ring, NO enclosing circle, no outline:
   only the lit lune of Sun still showing.
   SIZE RULE: it spans the SAME circle as the total's disc. Its outer arc IS the full 7-unit
   limb (through 14,7 / 7,0 / 7,14) and the Moon bites in from the left, so the ink fills
   13.3 x 14 of the 14 x 14 box instead of the 6.9 x 14 the old moon-phase construction gave
   — a partial no longer reads as a smaller icon than a total.
   Same construction as crescentPath() in src/timeline.js (horns pinned at 0.90r), frozen
   here at a representative deep partial (lune thickness 0.42r): Moon r=5.6293 centred at
   (5.4307, 7), horn tips at x=0.7. */
.sw-partial {
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'%3E%3Cpath d='M0.7 3.9488A7 7 0 1 1 0.7 10.0512A5.6293 5.6293 0 1 0 0.7 3.9488Z'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'%3E%3Cpath d='M0.7 3.9488A7 7 0 1 1 0.7 10.0512A5.6293 5.6293 0 1 0 0.7 3.9488Z'/%3E%3C/svg%3E");
}

/* The end labels under the disc ramp (B8: OLDER / RECENT; 30 Jul 2026 adds FUTURE as the
   ramp's third part — see the comment over .lg-when's svg in index.html). Flex space-between
   on the parent (.lg-ramp-caps, above) is what keeps three labels from ever colliding or
   wrapping: it lays them out in normal flow, never overlapping, regardless of exactly how the
   ramp's own proportions fall under them. */
.lt-cap {
  font-family: var(--grot);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-faint);
  white-space: nowrap;
}
/* FUTURE's own end label takes the cool family's darker-for-text tone verbatim — the same
   FUTURE_TEXT_INK (#536b73) src/shadowfill.js already exports and src/personal.js/
   src/timeline.js already paint future year-labels in, so this cap reads as the same "future"
   ink as everywhere else in the piece, not a shade invented for the legend alone. */
.lt-cap-future { color: #536b73; }

/* ---------- Tooltip ---------- */
.tooltip {
  position: fixed;
  z-index: 20;
  pointer-events: none;
  min-width: 150px;
  max-width: 260px;
  padding: 2px 2px;
  /* House rule: NO plate/border/rounding and NO text shadow — plain ink text on the scene.
     Legibility over the busy globe comes from INK CONTRAST, helped by a FEATHERED SCRIM
     (::before below) rather than a plate: no hard edge, no border, no rounding. */
  transform: translate(16px, 14px);
  transition: opacity 0.14s ease;
}
/* READABILITY FIX (director, 27 Jul 2026: "на плашке надо это давать, а то не видно
   ничего" / "на чб норм, читаемо, а на светлом режиме глобуса — нет"): the tooltip read
   fine over the dark night globe (light ink on a near-black scene) but washed out over the
   busy pale sepia day globe, where --ink-faint sits too close in tone to the terrain
   underneath. Rather than a plate (forbidden), a SOFT FEATHERED SCRIM rides behind the
   whole card: a plain rect, pushed out past the text on every side, then blurred so its
   edge has no boundary at all — not a gradient trying to fake a vignette, an actual blur.
   It reads off the SAME --scrim custom property src/main.js already interpolates day<->night
   (light halo behind dark ink by day, dark halo behind light ink by night), so it is at
   full strength in exactly the case that needed it and still there, unobtrusive, at night. */
/* Director 27 Jul: the blurred/feathered scrim ("плашки с сильным размытием краёв")
   read as ugly soft blobs. So NO blur — a clean-edged plate behind the card. No border,
   no rounding (both forbidden), just a flat --scrim fill with a crisp edge. */
.tooltip::before {
  content: "";
  position: absolute;
  inset: -8px -14px;
  background: var(--scrim);
  z-index: -1;
}
/* A DATE is a number: the tooltip's headline is the number face, tight and tabular, so it
   holds still as the pointer moves from one eclipse to the next. */
.tt-date {
  font-family: var(--mono);
  font-weight: 700;
  font-size: 21px;
  color: var(--ink);
  margin-bottom: 5px;
  letter-spacing: -0.03em;
  line-height: 1.1;
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}
.tt-type {
  display: inline-flex; align-items: center; gap: 8px;
  font-family: var(--grot);
  font-size: 13px; letter-spacing: 0.18em; text-transform: uppercase;
  font-weight: 600;
  color: var(--ink-soft);
  margin-bottom: 11px;
}
.tt-rows { display: grid; grid-template-columns: auto 1fr; gap: 4px 16px; }
/* --ink-soft rather than --ink-faint: on the scrim above, faint fell too close in tone to
   the sepia day globe under the pointer; soft holds the same quiet rank against the value
   column while actually being legible. */
.tt-k {
  font-family: var(--grot); font-size: 13px; font-weight: 600;
  letter-spacing: 0.14em; text-transform: uppercase; color: var(--ink-soft);
}
.tt-v {
  font-family: var(--mono); font-size: 13.5px; color: var(--ink); font-weight: 700;
  letter-spacing: -0.02em; text-align: right;
  font-variant-numeric: tabular-nums; font-feature-settings: "tnum" 1;
}

/* ---------- Arc label ---------- */
.arclabel {
  position: fixed;
  z-index: 15;
  pointer-events: none;
  transform: translate(-50%, -140%);
  padding: 2px 2px;
  /* House rule: NO plate/border/rounding and NO text shadow — plain ink text on the scene,
     separated by ink contrast (gold headline, full-ink distance) only. */
  text-align: center;
  white-space: nowrap;
}
.arclabel .al-date {
  font-family: var(--mono); font-weight: 700; font-size: 14px; letter-spacing: -0.03em;
  color: var(--gold); font-variant-numeric: tabular-nums; font-feature-settings: "tnum" 1;
}
.arclabel .al-dist {
  font-family: var(--grot); font-size: 13px; font-weight: 500; color: var(--ink);
  margin-top: 2px; font-variant-numeric: tabular-nums;
}

/* ---------- Personal glyph year labels (Globe, City layer) ---------- */
/* One plain year hung above each SHOWN personal eclipse glyph. HOUSE RULE: no plate, no
   border, no rounding and NO TEXT SHADOW — just warm dark ink (light at night) at full
   strength with a little weight; that ink contrast is the whole separation from the globe.
   Positioned + collision-hidden per frame by personal.js updateYearLabels(). Never <13px. */
.personal-labels {
  position: fixed;
  inset: 0;
  z-index: 14;               /* above famous(13)/arclabel(15?)… below tooltip(20) */
  pointer-events: none;
}
/* A YEAR is a number: the number face, tabular, tightened so Mono's wide set does not
   swell these marks past the glyphs they belong to. */
.personal-labels .pl-year {
  position: absolute;
  transform: translate(-50%, -160%);
  font-family: var(--mono);
  font-size: 13px;
  font-weight: 700;
  line-height: 1.15;
  letter-spacing: -0.04em;
  color: var(--ink);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}
/* The two heroes (next-deep + last-total) read a touch louder and in gold, so the pair the
   card names in words is findable on the globe among the quieter year marks. */
.personal-labels .pl-year.is-hero {
  font-size: 15px;
  color: var(--gold);
}

/* ---------- Famous-eclipse labels ---------- */
/* ~10 quiet gold ticks + years on the globe, marking historically significant eclipses.
   The container fades in with the reveal (is-on set from script once the intro is over and
   we are in Globe mode) and out otherwise; individual labels are shown/hidden per frame by
   the facing test in famous.js. Muted and small so they never fight the eclipse field. */
.famous-labels {
  position: fixed;
  inset: 0;
  /* BELOW THE CARD (11), not above it. At 13 these labels painted straight over #panel (12) —
     over the city's whole card, its chart and its hover captions — which is what put four
     unreadable outliers in the caption-contrast sweep, identical at every plate alpha because
     the plate was never what they were fighting. The globe's own annotations belong under the
     interface that reads the globe. Still above the arclabel; still below the tooltip (20). */
  z-index: 11;
  pointer-events: none;      /* the container is inert; each label re-enables itself */
  opacity: 0;
  transition: opacity 1.2s ease;
}
.famous-labels.is-on { opacity: 1; }
body.intro-play .famous-labels { opacity: 0; }   /* never present during the opening */

.famous-label {
  position: absolute;
  transform: translate(-50%, -50%);
  pointer-events: none;       /* inert itself; the ring AND the year plate opt back in */
  will-change: left, top;
}
/* A small open gold ring at the exact point — subtle, but enough to make a famous eclipse
   findable among the field without a heavy marker. Was once the ONLY hover target; the
   year plate is now one too (see .fl-year below, director 27 Jul 2026: "нужно, чтобы они
   наводились не только на кружок затмения, но и на год") — both fire the identical
   pointerenter/pointerleave on their shared parent (src/famous.js), so either one reaches
   the same hover-corona path as the eclipse's own glyph. */
.famous-label .fl-ring {
  display: block;
  width: 13px; height: 13px;
  border: 1.5px solid var(--gold);
  border-radius: 50%;
  background: transparent;
  pointer-events: auto;
  cursor: help;
}
/* The year sits SNUG beside the ring. ON A PLATE (director, 27 Jul 2026: "годы заметных
   затмений на плашках, будут более заметными") — square (no rounding), no border, no
   shadow. RECOLOURED (director, 27 Jul 2026, 2nd ask): the plate now carries the eclipse's
   OWN colour (gold) with the LIGHT ground tone on top, rather than a sepia panel with gold
   text — a solid gold badge, not a tinted window. Never below 13px.
   NOW A HOVER TARGET TOO (director, 27 Jul 2026, 3rd ask): "нужно, чтобы они наводились не
   только на кружок затмения, но и на год" — pointer-events re-enabled here, `cursor: help`
   to match the ring. src/famous.js already listens for pointerenter/pointerleave on the
   SHARED PARENT (.famous-label), not on the ring specifically, so simply opening this child
   back up to hit-testing routes it through the exact same onEnter/onLeave -> main.js
   onFamousEnter/onFamousLeave -> updateCorona/clearCoronaHover path the ring and the
   glyph point use — no new wiring, no divergent behaviour. It still fades to opacity 0 on
   its OWN hover (below): the tooltip already carries the year, same as when the ring is
   hovered, so this is not a flicker — cursor and hit box stay put, only the paint changes. */
.famous-label .fl-year {
  position: absolute;
  left: 100%; top: 50%;
  transform: translate(5px, -50%);
  background: var(--gold);
  padding: 1px 5px 2px;
  border-radius: 0;
  font-family: var(--mono);
  font-size: 13px;
  line-height: 1.2;
  letter-spacing: -0.04em;
  color: var(--ground);
  font-weight: 700;
  font-feature-settings: "tnum" 1;
  opacity: 1;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
  pointer-events: auto;
  cursor: help;
  transition: opacity 0.18s ease;
}
.famous-label:hover .fl-ring {
  border-color: var(--gold);
  filter: brightness(1.25);
}
/* Hidden while the eclipse is hovered (via ring OR its glyph point): the tooltip lore card
   already shows the year, so the badge is redundant. Class is set from main.js's famous
   enter/leave + point-pick — NOT a :hover on the badge — so the ring stays the stable
   target and there is no flicker. */
.famous-label.is-hovered .fl-year {
  opacity: 0;
}

/* ---------- THE 2026 PLATE — the future's own colour, not the famous ten's gold ----------
   Director, 31 Jul 2026: "затмению 26 года надо тоже плашку как 'заметному', но она должна
   быть голубой." Same idiom as a famous plate (a ring on the point, a year plate beside it —
   built in src/famous.js's `futureItem`, sharing the ten's own collision pass so it never
   lands on a famous/preset/ambient label) — but this one is not one of the ten: it carries no
   lore, is never a hover target (pointer-events are off on every part of it), and hides under
   EVERY preset including "Written into history" itself, because it is not that set's eclipse.
   COLOUR: the RENDERED cool ink, not the declared uniform. The scene tone-maps, so the
   declared uFutureInk #B5C1DB (points.js) is not what lands on screen — measured directly on
   this eclipse's own glyph (id 9566, 12 Aug 2026), core pixels of the isolated point via the
   dev/future_floor_probe.mjs __glyphProbe kit, both at 1280x900 dsf1 and 390x844 dsf3: both
   read rgb(151, 163, 191), so that is the value used here, not the nearby "nearest future"
   legend-ramp value (rgb(145,158,190)) measured for a different point. */
.famous-label.is-future .fl-ring {
  border-color: rgb(151, 163, 191);
}
.famous-label.is-future .fl-year {
  background: rgb(151, 163, 191);
  color: var(--ground);
}

/* ---------- Magnitude-isoline percentage labels ---------- */
/* Contour-map labelling: "80%"/"60%"/"40%"/"20%", one per thin fan curve, shown only while
   its eclipse is hovered. Positioned + occluded per frame by src/paths.js updateIsoLabels()
   — same facing test as .famous-label (back-of-globe hidden) plus the shared Sun/corona
   keep-out disc, mirroring what the isoline ribbon shader itself already discards.
   HOUSE RULE compliance: no plate, no border, no rounding — separation comes from ink
   contrast alone, kept deliberately QUIET (lighter weight + muted --ink-faint, not the
   bold gold the totality border's own hover reads at) so it stays legible without reading
   as bold UI chrome. Never below the project's 13px small-label floor (see .tt-k). Sits
   just clear of the line it names (translate lifts it off the anchor point) rather than
   plated over it, per the director's "breaking/fading OR sitting just clear" brief. */
.iso-labels {
  position: fixed;
  inset: 0;
  z-index: 14;               /* same quiet tier as .personal-labels; below the tooltip (20) */
  pointer-events: none;
}
.iso-label {
  position: absolute;
  transform: translate(-50%, -145%);
  /* MINUSCULE (oldstyle) FIGURES, the way a contour map sets its numbers — the director asked
     for exactly that. This is the ONE place the piece uses a serif: neither Space Grotesk nor
     Space Mono ships oldstyle figures at all (measured: their digits are cap-height with no
     descenders, so `font-variant-numeric: oldstyle-nums` silently did nothing), whereas Georgia
     and Constantia have oldstyle figures as their DEFAULT numerals — no webfont to download.
     The fallbacks are ordered by what actually measured as oldstyle on Windows. */
  font-family: Georgia, Constantia, 'Iowan Old Style', 'Palatino Linotype', Palatino, serif;
  font-variant-numeric: oldstyle-nums;
  font-feature-settings: "onum" 1;
  /* 15px, not the usual 13px floor: oldstyle figures are x-height, not cap-height, so at 13px
     these would READ as about 10px — squarely in the size band the director has banned.
     LOOK TUNER hook: --iso-label-size, set on :root only by src/tuner.js; unset in the normal
     path, so var()'s fallback (15px, byte-identical to before) is what actually renders. */
  font-size: var(--iso-label-size, 15px);
  font-weight: 400;                /* Space Mono's Regular cut: lighter than the 700 numeric
                                       labels elsewhere (.tt-v/.pl-year/.tl-nowlabel) so this
                                       reads delicate, not like bold UI chrome */
  letter-spacing: 0.01em;
  /* "quiet labels and index marks" — the ladder's own token. LOOK TUNER hook: --iso-label-color,
     same convention as --iso-label-size above — unset normally, so the fallback wins. */
  color: var(--iso-label-color, var(--ink-faint));
  white-space: nowrap;
  pointer-events: none;
  will-change: left, top, opacity;
  /* Oldstyle (text) figures, contour-map style. Space Mono/Space Grotesk are not known to
     ship an onum glyph set (see docs note below) so this is likely a harmless no-op that
     falls back to lining figures — the quiet delicate feel is carried instead by the small
     size + light weight + muted ink + loosened tracking above, which work regardless of
     glyph support. */
  font-variant-numeric: oldstyle-nums;
  font-feature-settings: "onum" 1;
}

/* ---------- Tooltip: famous-eclipse lore block ---------- */
/* SEPARATED BY AIR, NOT BY A LINE. This block carried a border-top, which is the one thing the
   house rule forbids outright (no borders, no rounding, anywhere). The gap it used to sit in was
   21px of margin+padding with a hairline through it; it is now 22px of plain air, and the lore's
   own gold title is what announces that a new block has started. */
.tt-lore {
  margin-top: 22px;
}
/* The lore block is WORDS — a name and a paragraph — so it stays in Grotesk throughout. */
.tt-lore-title {
  font-family: var(--grot);
  font-weight: 600;
  font-size: 17px;
  letter-spacing: -0.022em;
  color: var(--gold);
  line-height: 1.14;
  margin-bottom: 4px;
}
.tt-lore-why {
  font-family: var(--grot);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-soft);   /* was --ink-faint — see .tt-k note above */
  margin-bottom: 8px;
}
.tt-lore-blurb {
  font-family: var(--grot);
  font-size: 13.5px;
  line-height: 1.62;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.tooltip.is-famous { max-width: 320px; }
/* THE ARC-ROW CARD SIZES ITSELF, HERE, IN THE SHEET. The 260px cap above is right for the short
   three-row card, but it wraps the approved Sun-altitude arc row onto two lines: arc 26 + gap +
   the altitude value + gap + "at greatest eclipse, 37.0°N 87.7°W" at 13px mono measures ~400px,
   and at 260 (or 340) the note wrapped and the row came out 40px tall instead of 26. src/main.js
   was lifting the cap with an inline max-width computed in JS; that belongs in one place, and this
   is it — the card is selected by the row it actually contains, so no class or inline style has to
   be maintained alongside the markup. The upper bound is never wider than the screen it is on: a
   400px card on a 375px phone would push the page sideways, and this piece must not scroll
   horizontally anywhere. On a narrow screen the note simply wraps, which is the right sacrifice —
   the alternative is type under 13px. (clamp's lower bound is the old Math.max(240, ...) floor.) */
.tooltip:has(.tt-alt-row) { max-width: clamp(240px, calc(100vw - 40px), 400px); }
/* ...and a famous eclipse's card carries the lore block too, so it is allowed the extra 20px. */
.tooltip.is-famous:has(.tt-alt-row) { max-width: clamp(240px, calc(100vw - 40px), 420px); }

/* ---------- Personal column ---------- */
/* No toggle, no card. A single stacked column pinned at top-left directly below the
   title: subheading -> city picker -> results. The subheading + picker stay pinned at
   the top; the results below scroll within the column when tall. Client hard rule: NO
   plates, NO borders/outlines, NO rounded corners — just flat text and a hairline under
   the picker. The box itself is pointer-transparent so its empty parts never steal a
   drag from the globe; only the picker and the live result blocks opt back in. */
.panel {
  position: fixed;
  /* 140px (30 Jul 2026), was 176. Both the 208 this started at and the 176 that replaced it
     were measured for a column with the [Globe | City Sky] tab bar sitting between the
     masthead and the picker. The bar is gone, and with it the reason for the gap: measured on
     the live page, the masthead's bottom edge is at 110.1 and the picker started at 176 — a
     flat 65.9px of nothing in the middle of one panel. 140 restores the rhythm the tabs used
     to occupy: masthead -> a real gap -> picker. */
  left: 34px; top: 140px; bottom: 92px;   /* under the masthead, clear of the bottom scrubber */
  z-index: 12;
  width: 300px;
  max-width: calc(100vw - 68px);
  display: flex;
  flex-direction: column;
  align-items: stretch;
  pointer-events: none;
}

/* Pinned top: quiet subheading + the searchable picker. Always visible (once the intro
   ends), never scrolls away. */
.panel-picker {
  flex: none;
  pointer-events: auto;
}
/* The column's own section label — same treatment as the legend's "TYPE". */
.panel-sub {
  font-family: var(--grot);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin: 0 0 10px;
}
.pick-row {
  display: flex;
  align-items: baseline;
  gap: 12px;
}
/* ---- OUR OWN SUGGESTION LIST (replaces the native <datalist>) ---------------------------
   Why it exists at all: Chrome's address autofill opened over the datalist and stole the
   pick — see the comment on #citySearch in index.html. This one is ours, so it can be
   capped, keyboard-driven, and drawn in the piece's language.

   IT SITS OVER THE GLOBE AND OVER THE CARD, both of which are busy, and the house rules forbid
   the usual answers: no plate, no border, no rounding, no text-shadow. What holds it instead is
   TONE — an OPAQUE wash of the piece's own ground colour, with no edge of its own. It has to be
   opaque rather than a tint: the card's hero block sits directly underneath and a translucent
   list let both sets of words through at once, which is unreadable. --ground is the same
   variable the glyph "holes" are punched to and src/main.js interpolates it toward the night
   palette on the day/night inversion, so the list follows the piece with no second rule. */
.pick-menu {
  position: absolute;
  z-index: 30;
  left: 0;
  right: 0;
  margin-top: 2px;
  padding: 6px 0 8px;
  max-height: 46vh;
  overflow-y: auto;
  background: var(--ground);
  border: none;
  border-radius: 0;
  box-shadow: none;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
}
.panel-picker { position: relative; }
.pick-opt {
  display: block;
  width: 100%;
  padding: 7px 1px;
  background: none;
  border: none;
  border-radius: 0;
  text-align: left;
  cursor: pointer;
  color: var(--ink-soft);
  font-family: var(--grot);
  font-size: 16px;              /* well over the 13px floor */
  font-weight: 500;
  letter-spacing: -0.012em;
  line-height: 1.15;
  text-shadow: none;
}
/* The highlight is WEIGHT AND INK, not a fill: the row the keyboard is on simply becomes the
   darkest thing in the list. One state serves hover and keyboard alike, so pointer and
   arrow keys can never disagree about which row is about to commit. */
.pick-opt:hover,
.pick-opt[aria-selected="true"] {
  color: var(--ink);
  font-weight: 700;
}
.pick-opt .po-cc {
  color: var(--ink-faint);
  font-weight: 500;
  margin-left: 6px;
}
.pick-opt:hover .po-cc,
.pick-opt[aria-selected="true"] .po-cc { color: var(--ink-soft); }
/* The picker itself: as flat as a text input gets — transparent, no box, no rounding,
   just a hairline underline that warms to gold on focus. Serif so it reads as a heading. */
.city-input {
  flex: 1;
  min-width: 0;
  padding: 6px 1px;
  background: transparent;
  border: none;
  border-bottom: 1px solid rgba(150, 175, 210, 0.30);
  border-radius: 0;
  color: var(--ink);
  font-family: var(--grot);
  font-size: 19px;
  font-weight: 500;
  letter-spacing: -0.018em;
  outline: none;
  transition: border-color 0.2s;
  /* LONG NAMES (2 Aug 2026, the same pass as the sky button's). Once a place is chosen this
     field HOLDS its name, and an <input> cannot wrap: at 238px on a desktop, "Villeray–Saint-
     Michel–Parc-Extension" came out hard-cut mid-word as "…–Parc-Exten", which reads as a
     rendering fault rather than as a name that did not fit. An ellipsis says the difference.
     Chrome applies this to an input only while it is NOT focused, which is exactly the case
     that matters: the truncation the visitor sits looking at. Typing is unaffected. */
  text-overflow: ellipsis;
}
.city-input:focus { border-bottom-color: rgba(240, 217, 160, 0.6); }
.city-input::placeholder { color: var(--ink-faint); font-weight: 400; }
/* The deselect affordance: plain text, no plate/border/rounding. Shown only once a city
   is chosen (hidden attribute otherwise). */
.pick-clear {
  flex: none;
  background: none;
  border: none;
  border-radius: 0;
  padding: 0;
  color: var(--ink-faint);
  font-family: var(--grot);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  cursor: pointer;
  white-space: nowrap;
  transition: color 0.2s;
}
.pick-clear:hover,
.pick-clear:focus-visible { color: var(--gold); outline: none; }

/* THE × COMPANIONS (3 Aug 2026 — "нужна более логичная система. крестик, чтобы закрыть город.
   к clear нужно добавить крестик еще рядом."). Two real buttons, #citySearchX on the field's own
   readout and #backBtnClearX beside CLEAR, both wired in src/personal.js to the SAME back() that
   CLEAR already calls. Same visual language as .tt-close (the tap-card's own × in the phone
   media query below): no plate, no border, no rounding, ink alone, a real touch target rather
   than a decoration.
   PHONE ONLY (display:none is the rule here; the phone media query below restores it). CLEAR and
   a mouse already do this job at every other width — this pass is scoped to the phone rail, and
   a redundant × next to a working CLEAR on a desktop would be exactly the "не понял" clutter the
   piece avoids everywhere else. */
.pick-x {
  display: none;
  flex: none;
  background: none;
  border: none;
  border-radius: 0;
  padding: 0;
  color: var(--ink-faint);
  font: 20px/1 var(--mono);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: color 0.2s;
}
.pick-x:hover,
.pick-x:focus-visible { color: var(--gold); outline: none; }

/* THE CITY CARD. Revealed on select, empty and invisible before. No plate, no border, no
   rounding — and NO shadow or glow of any kind: the type sits straight on the scene and
   earns its separation with ink contrast alone. This block is the prototype of the
   social-share card, so its rhythm is a deliberate scale, not accumulated margins:
     HERO (24px gap) -> one compact line (24px) -> the graph (24px) -> the "i" (12px) -> prose.
   Scrolls within the column; pointer-transparent when empty so drags pass through. */
.panel-body {
  margin-top: 16px;
  width: 100%;
  min-height: 0;
  /* B15: take ALL the leftover height in the column, so a card that fits raises no scrollbar
     (it used to be capped to its content height and a single rounding-pixel of overflow kept
     a track on screen). overflow-y is AUTO, so only a genuinely taller card scrolls. */
  flex: 1 1 auto;
  overflow-y: auto;
  overscroll-behavior: contain;
  /* A flex column so the reading order is set independently of the DOM order —
     HERO, one compact line, the GRAPH, then the "i" that opens the prose. */
  display: flex;
  flex-direction: column;
  pointer-events: none;
  scrollbar-width: thin;
  scrollbar-color: rgba(150, 170, 200, 0.32) transparent;
}
.panel-body > * { pointer-events: auto; }
/* Before a city is chosen the column is EMPTY. src/personal.js builds the "i" toggle up
   front and leaves it in the flow, so an orphan gold "i" and its hairline sat under the
   picker on a virgin screen (visible in the pre-pass capture too — this is not new). The
   card's own body class is the honest gate: no city, no card furniture. */
body:not(.panel-open) .panel-info { display: none; }
.panel-body::-webkit-scrollbar { width: 6px; }
.panel-body::-webkit-scrollbar-thumb { background: rgba(150, 170, 200, 0.30); border-radius: 3px; }
.panel-body::-webkit-scrollbar-track { background: transparent; }

.panel-result { margin-top: 18px; }
/* The first visible result block sits straight under the picker underline — no leading
   divider (which would double the hairline) and no top gap beyond the body's own. */
.panel-body > *:first-child { margin-top: 0; padding-top: 0; border-top: none; }
/* B15: the card must not raise a scrollbar unless it TRULY overflows. A trailing bottom
   margin on the last block pushed the content one rounding-pixel past the column and tripped
   a permanent track; the last child's bottom margin is dropped so a card that fits shows no
   scrollbar, and only a genuinely taller one scrolls (overflow-y is auto, never scroll). */
.panel-body > *:last-child { margin-bottom: 0; }
/* The card's running prose. WC's body measure, scaled to a 300px column: Grotesk 400 with
   a generous leading, emphasis in weight not colour. Tabular figures throughout, so the
   numerals inside a sentence sit on the same rhythm as the ones in the number face. */
.stat-line {
  font-size: 13.5px;
  line-height: 1.68;
  color: var(--ink);
  margin: 0 0 11px;
  font-variant-numeric: tabular-nums;
}
.stat-line b, .stat-note b { font-weight: 600; }
.stat-grid { display: grid; grid-template-columns: auto 1fr; gap: 5px 14px; margin: 12px 0; }
.stat-grid .sg-k {
  font-family: var(--grot); font-size: 13px; font-weight: 600;
  letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--ink-faint); display: inline-flex; align-items: center; gap: 8px;
}
.stat-grid .sg-v {
  font-family: var(--mono); font-size: 13.5px; font-weight: 700;
  letter-spacing: -0.01em; color: var(--ink); text-align: right;
  font-variant-numeric: tabular-nums; font-feature-settings: "tnum" 1;
}
/* The pulled-out sentence — the 2026 resolution. Grotesk at display weight and negative
   tracking, the piece's gold: a lede, not a heading. */
.stat-hero {
  font-family: var(--grot);
  font-size: 16px;
  font-weight: 500;
  line-height: 1.44;
  letter-spacing: -0.012em;
  color: var(--gold);
  margin: 14px 0 11px;
  font-variant-numeric: tabular-nums;
}
.stat-hero b { font-weight: 700; }
.stat-note {
  font-size: 13px;
  line-height: 1.66;
  color: var(--ink-soft);
  border-top: 1px solid var(--line);
  padding-top: 12px;
  margin-top: 12px;
  font-variant-numeric: tabular-nums;
}
/* ---------- Personal timeline (MULTI-TIER) ---------- */
/* The globe says WHERE; this says WHEN. Now stacked LANES, one per local eclipse
   type (Total, Annular, Hybrid, Partial), sharing one year x-axis below. Each lane
   carries a small muted type label at its left, then its glyphs by year (even-spread
   when they'd collide). SHAPE = local type, FILL = local magnitude. Empty lanes are
   dropped. The axis is a quiet reference: a thin line + 3–4 round-year ticks, 2026
   marked "now" in gold. Serif numerals, muted gold, nothing under 13px, no chart
   junk — no stems, no foot-dots, no plates/borders/rounding. */
.timeline {
  position: relative;
  margin-top: 20px;
  padding-top: 16px;
  border-top: 1px solid var(--line);
  /* House rule (no shadows): main.js lifts this very node onto the sky overlay in City Sky
     mode and used to give it an inline drop-shadow for legibility over the twilight. Kill it
     from here — the strip reads on ink contrast alone. (The inline write still lives in
     src/main.js liftTimelineToSky(); it should be removed there too.) */
  filter: none !important;
}
.tl-head { margin-bottom: 6px; }
.tl-sub {
  font-family: var(--grot);
  font-size: 13px;
  line-height: 1.55;
  letter-spacing: 0;
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}
/* Fits the card — no scrollbar; the SVG is built to the stage's own width. */
.tl-stage { overflow: hidden; }
.tl-svg { display: block; width: 100%; }

/* lane type label: muted, serif, ≥13px, sitting at the lane's left. Ink contrast only —
   the strip carries no shadow of any kind, so the quiet marks read at --ink-soft. */
/* Lane names are WORDS -> Grotesk, uppercase, tracked, in the quiet ink. */
.tl-lanelabel {
  font-family: var(--grot);
  font-size: 13px;
  font-weight: 600;
  fill: var(--ink-faint);
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.tl-axis { stroke: var(--ink-soft); stroke-width: 1; opacity: 0.8; }
.tl-tick { stroke: var(--ink-soft); stroke-width: 1; opacity: 0.8; }
/* The YEAR AXIS is numbers -> the number face, tabular so the ticks never jitter. */
.tl-ticklabel {
  font-family: var(--mono);
  font-size: 13px;
  font-weight: 400;
  letter-spacing: -0.03em;
  fill: var(--ink-faint);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}
/* 2026 = now: the strip's anchor. A faint gold guide rises through the lanes, and on the
   axis a bolder gold tick + a filled node + a slightly larger gold label mark the present. */
.tl-nowguide { stroke: var(--gold); opacity: 0.28; stroke-width: 1; }
.tl-now { stroke: var(--gold); stroke-width: 2; }
.tl-nowdot { fill: var(--gold); }
.tl-nowlabel {
  font-family: var(--mono);
  font-size: 13.5px;
  font-weight: 700;
  fill: var(--gold);
  letter-spacing: -0.03em;
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

/* one eclipse; --w is its local-magnitude weight (1 = totality/annularity here) */
.tl-item { --w: 0.6; cursor: pointer; outline: none; }
.tl-hit { fill: transparent; }
/* HOUSE RULE: glyphs are FILLED icons, zero stroke/outline. The fill rides the theme ink:
   dark warm ink on the sepia day, light at night. Total = solid disc; Annular = disc with a
   punched hole; Partial = filled crescent — all pure fills, no edge/stroke anywhere. */
.tl-g-fill { fill: var(--ink); stroke: none; opacity: calc(0.5 + 0.5 * var(--w)); }
/* A hole is punched, not stroked (annular ring of fire): it reveals the sky/ground behind. */
.tl-g-hole { fill: var(--ground); stroke: none; }
/* A place totality (or the ring) actually reached. This used to be a soft gold disc behind
   the glyph; at glyph scale it read as a RING around the mark — the outline the house rule
   forbids — so it is gone. The cue is now the glyph's own weight: on-path draws at full gold
   strength, everything else sits back in ink. No ring, same information. */
.tl-item .is-onpath .tl-g-fill { fill: var(--gold); opacity: 1; }

/* the hero (nearest place-crossing eclipse, or the deepest partial) reads a touch louder */
.tl-item.is-hero .tl-g-fill { opacity: calc(0.5 + 0.5 * var(--w)); }
.tl-item.is-total .tl-g-fill { opacity: calc(0.55 + 0.45 * var(--w)); }

/* Future eclipses (after "now") read QUIET — desaturated to steel and dimmer — so the past
   reads at full warm strength and the present is the anchor. Hover/active restores them fully. */
.tl-item.is-future .tl-g-fill { fill: var(--ink-faint); opacity: calc(0.34 + 0.34 * var(--w)); }
.tl-item.is-future .is-onpath .tl-g-fill { fill: var(--ink-faint); opacity: 0.62; }

.tl-item:hover .tl-g-fill,
.tl-item.is-active .tl-g-fill { fill: var(--ink); opacity: 1; }
.tl-item:focus-visible .tl-g-fill { fill: var(--ink); opacity: 1; }

/* THE HOVER EMPHASIS LAYER (src/timeline.js: markEmphasis). The hovered mark is drawn a second
   time in here — the svg's last child — so it paints over neighbours it overlaps without the
   mark itself ever moving in the tree. It moved before, and that lost the chart its click: the
   node was removed and re-inserted under the pointer once per frame, so mousedown and mouseup
   never resolved to the same node in the same place and no `click` was ever synthesised.
   POINTER-TRANSPARENT is the load-bearing declaration here: the copy must never take a hit, so
   the real mark underneath keeps receiving every hover and press. */
.tl-emph { pointer-events: none; }

/* timeline detail — sits over the strip, centred on the glyph column (left set in JS) */
.tl-tip {
  position: absolute;
  top: 24px;
  z-index: 3;
  width: max-content;
  max-width: min(240px, calc(100% - 4px));
  padding: 2px 2px;
  /* House rule: NO plate/border/rounding and NO text shadow — plain ink on the scene,
     legible by ink contrast (full-strength ink, weight on the values). */
  pointer-events: none;
}
.tl-tip-date {
  font-family: var(--mono); font-weight: 700; font-size: 16px;
  letter-spacing: -0.028em; color: var(--ink);
  font-variant-numeric: tabular-nums; font-feature-settings: "tnum" 1;
}
.tl-tip-type {
  display: inline-flex; align-items: center; gap: 8px;
  font-family: var(--grot); font-size: 13px; font-weight: 600;
  letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--ink-soft); margin: 5px 0 9px;
}
.tl-tip-rows { display: grid; grid-template-columns: auto 1fr; gap: 4px 14px; }
.tl-tip-k {
  font-family: var(--grot); font-size: 13px; font-weight: 600;
  letter-spacing: 0.14em; text-transform: uppercase; color: var(--ink-faint);
}
.tl-tip-v {
  font-family: var(--mono); font-size: 13px; font-weight: 700; letter-spacing: -0.02em;
  color: var(--ink); text-align: right;
  font-variant-numeric: tabular-nums; font-feature-settings: "tnum" 1;
}
.tl-tip-flag {
  margin-top: 9px; padding-top: 9px;
  border-top: 1px solid var(--line);
  font-family: var(--grot); font-size: 13px; line-height: 1.5;
  color: var(--gold);
}

/* ---------- Card layout: a clean hierarchy, not a wall of prose ---------- */
/* Reading order set on the flex column, independent of DOM order:
     1 HERO  →  2 one compact line  →  3 the GRAPH  →  4 the "i" toggle  →  5 collapsible prose.
   The old standalone "Last total" block is retired (folded into the one-line + the prose).
   ONE spacing scale runs the whole card so the rhythm reads deliberate: 20px between
   blocks, 16px from a divider to its content, 10/9px inside the hero. Nothing else. */
.panel-deep   { order: 1; }
.panel-next   { order: 2; }
.timeline     { order: 3; }
/* Same order as the chart, tied deliberately rather than given the next free number (4, which
   .panel-info already holds): a tie is broken by DOM position, and this row is inserted as
   #timeline's own next sibling (src/personal.js's mountDownloadRow), so "same order as the
   chart, immediately after it in the DOM" is what actually reads as "under the chart" — a fixed
   order:4 would have been equally correct today but would silently stop meaning that the moment
   any other order:4 block was ever added between them. */
.cd-actions   { order: 3; }
.panel-info   { order: 4; }
.panel-result { order: 5; }
.panel-note   { order: 6; }
.panel-last   { display: none; }   /* retired from the card */

/* ---------------------------------------------------------------------------------------------
   THE DOWNLOAD ROW — the piece's own copy of card.html's action row (src/personal.js's
   mountDownloadRow(), see that function's own header for the placement/direct-vs-stage reasoning).
   THE SAME CLASS NAMES AS src/card-stage.css ON PURPOSE — that file's own comment: "the same
   button lands in the piece next to the four he sketched... so it has to be built somewhere it
   will read identically in both places". Reusing '.cd-actions'/'.cd-act'/'.cd-act-note' rather
   than inventing piece-local names is what guarantees that, and it is SAFE from colliding with
   card-stage.css's own copy of these rules: index.html never loads that file (only styles.css +
   about.css), and card.html loads styles.css FIRST and card-stage.css SECOND, so on that page
   card-stage.css's later, identical-specificity declarations always win and this block has no
   effect there at all — verified by reading both files' <link> order rather than assumed.
   House rules apply here exactly as everywhere else: no border, no radius, no shadow, nothing
   under 13px. A verb in the piece's own control weight, not a heading. --------------------------
   --------------------------------------------------------------------------------------------- */
.cd-actions {
  display: flex;
  align-items: baseline;
  gap: 16px;
  max-width: 100%;
  /* 16px, the same "divider to its content" step this card's own reading-order comment (above)
     already names for the rest of the column — this row is close kin to the chart above it, not
     a new section that would earn the fuller 20px block gap .timeline itself opens with. */
  margin-top: 16px;
}
.cd-act {
  font-family: var(--grot);
  font-size: 14px;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--gold);
  background: none;
  border: none;
  border-radius: 0;
  padding: 0;
  text-decoration: none;
  cursor: pointer;
}
.cd-act:hover, .cd-act:focus-visible { text-decoration: underline; text-underline-offset: 3px; }
.cd-act:focus-visible { outline: none; }
.cd-act[disabled] { color: var(--ink-faint); cursor: default; text-decoration: none; }
.cd-act-note {
  font-family: var(--mono);
  font-size: 13px;
  color: var(--ink-faint);
  font-variant-numeric: tabular-nums;
}

/* (b) THE HERO — the card's headline: the next eclipse you can actually see from here.
   Plain text on the scene: no plate, no border, no rounding, NO shadow. A small kicker, a
   big serif date carrying its type glyph, and one quiet sub-line with "in N years" + the
   local effect. This is the block a share card would crop to. */
.panel-deep { margin-top: 0; padding-top: 0; border-top: none; }
/* With the shadow gone, the quiet labels carry themselves on INK CONTRAST: --ink-soft
   (≈4.9:1 on the sepia day ground) plus a little weight, never --ink-faint, which fell
   under 3.5:1 and only ever looked legible because a black halo was propping it up. */
.hero-kicker {
  font-family: var(--grot);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.18em;
  line-height: 1.45;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-bottom: 11px;
}
/* THE CARD'S HEADLINE IS A DATE, so it is set in the number face — Mono 700, tight, with
   tabular figures so the headline never reflows as the day/month change width. This is the
   one place the two faces meet at display size, and the rule that decides it is the piece's
   own: Grotesk speaks, Mono counts. */
.hero-date {
  display: flex;
  align-items: center;
  gap: 12px;
  font-family: var(--mono);
  font-weight: 700;
  font-size: 25px;
  letter-spacing: -0.028em;
  line-height: 1.06;
  color: var(--gold);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}
/* The glyph is part of the headline, so it is sized to the headline — not left at the
   13px legend size, which is what made it read as a stray speck. */
.hero-date .sw { width: 18px; height: 18px; }
/* "None deep enough on record" is a sentence, not a number: it goes back to Grotesk. */
.hero-date.hero-none {
  font-family: var(--grot);
  font-weight: 600;
  font-size: 19px;
  letter-spacing: -0.02em;
  line-height: 1.2;
  color: var(--ink-soft);
}
.hero-sub {
  margin-top: 10px;
  font-size: 13.5px;
  line-height: 1.58;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.hero-sub b { color: var(--gold); font-weight: 600; }

/* (c) TOTALITY HERE — REWORKED (director, 27 Jul 2026: "нужно больше акцент на том, что
   это было последнее, а это будет следующее. чуть крупнее шрифт и более конкретные
   фразы"). Was one compact row, "last 1724 · next 2081" — a label the reader had to parse.
   Now it is a small kicker over TWO plain sentences, one PAST ("was") and one FUTURE
   ("will be"), so the before/after contrast is the grammar itself, not just a word order.
   Sized a step up from the old 13.5px line. Graceful when a city has no past totality, no
   future one, or (rarely, in this era of the record) neither — see renderOneLine() in
   src/personal.js, which always emits a sentence for both halves rather than an em-dash. */
.panel-next {
  /* NO RULE ABOVE IT. The hairline here predates the block's rebuild into the two display-size
     years, and it is the standing no-borders rule's business: the pair separates itself from the
     hero by weight and by air, not by a line drawn under the previous thing. Same correction the
     preset chips got on 30 Jul, for the same reason. The margin carries the whole separation. */
  margin-top: 32px;
  display: flex;
  flex-direction: column;
  gap: 5px;
}
/* Same [hidden]-vs-author-display bug as .sky-enter above: this rule's own `display: flex`
   silently beat the `hidden` attribute personal.js toggles, so "Totality here: last …
   next …" survived × CLEAR. */
.panel-next[hidden] { display: none; }
.panel-next .ol-kicker {
  font-family: var(--grot);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-bottom: 3px;
}
.panel-next .ol-line {
  margin: 0;
  font-family: var(--grot);
  font-size: 15.5px;         /* up from 13.5px — the director's "чуть крупнее" */
  font-weight: 500;
  line-height: 1.42;
  color: var(--ink-soft);
}
/* The year in each sentence is the one number in it, so it is the number face, in gold,
   set a step above body size — the reader's eye lands on 1724 and 2081, not the sentence. */
.panel-next .ol-line b {
  font-family: var(--mono);
  font-weight: 700;
  font-size: 16px;
  letter-spacing: -0.02em;
  color: var(--gold);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

/* (e) the "i" affordance — plain gold text with a small serif "i" mark. NO plate, NO border,
   NO circle/rounding (house rule). Opens/closes the detailed prose below it. */
.panel-info {
  margin-top: 20px;
  padding: 0;
  background: none;
  border: none;
  border-radius: 0;
  display: inline-flex;
  align-items: baseline;
  gap: 8px;
  cursor: pointer;
  text-align: left;
  align-self: flex-start;
  pointer-events: auto;
}
/* The same "i" mark the legend rows now use — mono, no circle, no rounding, no border. */
/* Same [hidden]-vs-author-display bug as .sky-enter/.panel-next above: `display:
   inline-flex` on .panel-info silently beat the `hidden` attribute, so the "i" detail
   toggle survived × CLEAR too. (body:not(.panel-open) .panel-info{display:none} above
   already covered the very-first-load case; this covers every clear after that.) */
.panel-info[hidden] { display: none; }
.panel-info .pi-mark {
  font-family: var(--mono);
  font-style: normal;
  font-weight: 700;
  font-size: 13px;
  line-height: 1;
  color: var(--gold);
}
.panel-info .pi-text {
  font-family: var(--grot);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-faint);
  transition: color 0.2s;
}
.panel-info:hover .pi-text,
.panel-info:focus-visible .pi-text { color: var(--gold); }
.panel-info:focus-visible { outline: none; }
.panel-info[aria-expanded="true"] .pi-mark { opacity: 0.7; }

/* the collapsible prose sits directly under the toggle */
.panel-result { margin-top: 14px; }

/* ---------- Honesty note (now empty; prose folded into panel-result) ---------- */
.panel-note { margin-top: 6px; }

/* ---------- Hint ---------- */
.hint {
  position: fixed;
  bottom: 84px; left: 50%;   /* clear of the bottom scrubber */
  transform: translateX(-50%);
  z-index: 8;
  font-family: var(--grot);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink-faint);
  pointer-events: none;
  transition: opacity 0.8s ease;
}
.hint.gone { opacity: 0; }
body.panel-open .hint { display: none; }
/* "hover a point" is a mouse instruction; on any coarse (touch-first) pointer — tablets and
   phones alike, not just the narrow phone breakpoint below — there is nothing to hover, so the
   hint would just be a stale, wrong sentence sitting over the globe. */
@media (pointer: coarse) { .hint { display: none; } }

/* ---------- No interface during the intro ---------- */
/* While the opening sequence plays there is nothing on screen but the image and the
   year counter: legend, the three view tabs, the personal-layer card and the hint are
   all absent. The class ships in index.html's MARKUP, so the interface is never painted
   and never has to fade out; it is removed when the sequence ends or is skipped, and the
   transition below is what eases everything back in.
   The title block deliberately stays: it is the piece's own caption, not interface. */
#legend, #panel, .hint, #scrubber {
  transition: opacity 1.2s ease;
}
body.intro-play #legend,
body.intro-play #panel,
body.intro-play .hint,
body.intro-play #scrubber {
  opacity: 0;
}
/* Invisible must also mean untouchable — the column's picker and result blocks re-enable
   pointer events for themselves (.panel-picker, .panel-body > *), so the whole #panel
   subtree has to be switched off explicitly during the intro. */
body.intro-play #legend, body.intro-play #legend *,
body.intro-play #panel, body.intro-play #panel *,
body.intro-play #scrubber, body.intro-play #scrubber *,
body.intro-play .hint {
  pointer-events: none !important;
}

/* ---------- Intro year counter ---------- */
/* Quiet serif readout of the year the opening wave is currently drawing. Sits low
   and centred, clear of the title (top-left) and the mode switch (top-centre).
   Fades in with the wave and out when the intro settles; never permanent chrome. */
.yearcount {
  position: fixed;
  bottom: 62px; left: 50%;
  transform: translateX(-50%);
  z-index: 9;
  font-family: var(--mono);
  /* The year is the only text on screen during the intro besides the title, so it is
     allowed to be large and quiet. Bumped well up (26 -> 46px) at the client's request.
     It is a COUNTER, so it is the number face — tabular so the digits do not jitter as it
     runs, and pulled in tight because Mono at display size sets very wide. */
  font-size: 44px;
  font-weight: 400;
  line-height: 1;
  letter-spacing: -0.02em;
  white-space: nowrap;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
  opacity: 0;
  pointer-events: none;
  transition: opacity 1.1s ease;
}
.yearcount.is-on { opacity: 1; }

/* ---------- Intro skip button ---------- */
/* Explicit, quiet control for "кнопка скип заставки" (director, 29 Jul 2026): the intro
   was already skippable by any click/key/wheel (main.js: window pointerdown/wheel/keydown
   -> skipIntro), but nothing said so. English label, the piece's own Grotesk/tracking
   (same family+weight+letter-spacing as .hint above). Sits in the bottom-right corner —
   clear of the title (top-left), the year counter (bottom-centre) and the picture itself —
   and, since 31 Jul 2026, clear of the sound toggle too: that control is now on screen the
   whole time (including during this one's only appearance), so it was moved OUT of this corner
   to the right margin at top: 348px — 464px of clear air above this button. See B10.
   NOT SEEN (director, 30 Jul 2026: "на заставке нет снизу кнопки с обводкой пропустить
   заставку") — checked live: the button WAS there (opacity 0.55, in the viewport, on top
   of the canvas per elementFromPoint), just too quiet to register. --ink-faint at 0.55
   opacity is the same illegibility already documented at the tooltip fix further down this
   sheet — that tone sits too close to the busy sepia day globe underneath. Promoted one
   rung to --ink-soft and dropped the extra opacity dimming (the ink ladder already carries
   "quiet" on its own; stacking 0.55 on top of an already-tertiary tone is what made it
   disappear). The opacity that remains is only the intro-play show/hide switch, and it now
   has NO transition on it — before, removing .intro-play let the button fade out over
   0.6s; the director wants it "gone the instant the intro ends", so only color/border-color
   still ease (for the hover tell), opacity flips instantly with the class.
   OUTLINE (director-granted exception #3, same 30 Jul 2026 note, "с обводкой" / "with an
   outline"): a hairline box like .lg-i's, ONLY for this control — do not extend outlines
   to anything else in the piece. Quiet at rest (--ink-soft ink + border), brightens to
   --gold on hover/focus like every other outlined control here. */
.intro-skip {
  position: fixed;
  right: 28px; bottom: 28px;
  z-index: 20;
  margin: 0;
  padding: 6px 12px 7px;
  border: 1px solid var(--ink-soft);
  border-radius: 0;
  background: none;
  font-family: var(--grot);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink-soft);
  cursor: pointer;
  opacity: 0;
  pointer-events: none;
  transition: color 0.25s ease, border-color 0.25s ease;
}
body.intro-play .intro-skip {
  opacity: 1;
  pointer-events: auto;
}
.intro-skip:hover,
.intro-skip:focus-visible {
  color: var(--gold);
  border-color: var(--gold);
}
.intro-skip:focus-visible { outline: none; }

/* ---------- Bottom year-range scrubber ---------- */
/* A minimalist global control: a thin track spanning -2000..3000 with two draggable
   handles selecting the visible year range. Client hard rule honoured strictly — NO
   borders, NO rounded corners, no outlined plates: just a thin line, two clean
   rectangular handles, tick marks and text. Muted palette, serif labels >=13px. Hidden
   during the intro (body.intro-play fade above), eased back in when it ends. */
.scrubber {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  height: 72px;
  padding: 0 clamp(24px, 5vw, 80px);
  z-index: 9;
  pointer-events: none;                /* container inert; handles/track opt back in */
  user-select: none;
  -webkit-user-select: none;
}
/* Live readout of the selected range, e.g. "−2000 — 2026" (BCE via the U+2212 minus). */
.scrubber .scrub-readout {
  position: absolute;
  top: 8px; left: 0; right: 0;
  text-align: center;
  font-family: var(--mono);      /* an instrument readout: the number face, always */
  font-size: 15px;
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}
/* The track: a full-width strip carrying the line, the selected span, ticks and handles.
   It opts pointer events back on so a press on the bare track grabs the nearer handle. */
.scrubber .scrub-track {
  position: absolute;
  left: clamp(24px, 5vw, 80px);
  right: clamp(24px, 5vw, 80px);
  top: 40px;
  height: 22px;
  pointer-events: auto;
  touch-action: none;
}
/* The base track line (thin, muted). No border, no rounding. Rides the theme ink. */
.scrubber .scrub-line {
  position: absolute;
  left: 0; right: 0; top: 0;
  height: 2px;
  background: var(--ink-faint);
  opacity: 0.55;
}
/* The selected sub-range: the same line, gold, sitting on top — a touch heavier than the
   base track so the span between the two handles reads as the thing being dragged. */
.scrubber .scrub-sel {
  position: absolute;
  top: -1px; height: 4px;
  background: var(--gold);
  opacity: 1;
}
/* Tick marks below the line, each with a year label. Labels are >=13px and readable. */
.scrubber .scrub-ticks {
  position: absolute;
  left: 0; right: 0; top: 0;
  height: 22px;
}
.scrubber .scrub-tick {
  position: absolute;
  top: 0;
  width: 1px; height: 6px;
  background: var(--ink-faint);
  opacity: 0.7;
  transform: translateX(-0.5px);
}
/* The year axis under the track — the same number face as the readout, one weight down. */
.scrubber .scrub-tick-label {
  position: absolute;
  top: 8px; left: 50%;
  transform: translateX(-50%);
  font-family: var(--mono);
  font-size: 13px;
  font-weight: 400;
  letter-spacing: -0.03em;
  color: var(--ink-faint);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}
/* The axis runs edge to edge, so the FIRST and LAST labels are centred on a tick that sits
   on the very end of the track and half of each hangs off the screen ("2000 BCE" lost its
   "20" at phone width). The two end labels hang INWARD instead; the middle four stay
   centred on their ticks. src/scrubber.js builds six .scrub-tick children in order. */
.scrubber .scrub-ticks .scrub-tick:first-child .scrub-tick-label {
  left: 0; transform: none;
}
.scrubber .scrub-ticks .scrub-tick:last-child .scrub-tick-label {
  left: auto; right: 0; transform: none;
}
/* The two handles: SUBSTANTIAL solid gold bars straddling the line. They were 3x18px
   hairlines and did not read as controls at all — they now carry real weight (10x28px) so
   the affordance is obvious at a glance. Weight and solid ink do the work: still NO
   rounding, NO border, NO outlined pill, no shadow (house rules). An even larger invisible
   grab target keeps them comfortable to catch. */
.scrubber .scrub-handle {
  position: absolute;
  top: -13px;
  width: 10px; height: 28px;
  margin-left: -5px;
  background: var(--gold);
  pointer-events: auto;
  cursor: ew-resize;
  touch-action: none;
  transition: filter 0.15s, background-color 0.15s;
}
.scrubber .scrub-handle::before {     /* invisible wider grab target: 32 x 44 */
  content: "";
  position: absolute;
  top: -8px; bottom: -8px; left: -11px; right: -11px;
}
.scrubber .scrub-handle:hover,
.scrubber .scrub-handle:focus-visible,
.scrubber .scrub-handle:active {
  background: var(--gold);
  filter: brightness(1.2);
  outline: none;
}

/* ---------- Presets: five quiet "questions" over the field (src/presets.js) ---------- */
/* An OFFER, not a toolbar — built in JS (src/presets.js), not markup, so this file only
   carries its look. RELOCATED (director, 29 Jul 2026: "ниже строки поиска города" — the
   first placement floated centred over the globe and covered the subject). Mounted by
   src/presets.js as a normal-flow child directly after `.pick-row` (the #citySearch row),
   inside `.panel-picker` — so it takes the column's own left edge and 300px width for
   free, no hand-measured offsets of its own, and inherits every rule that already hides
   `.panel` (intro, City Sky). Only the city-selected case needs its own rule below.
   MAIN GLOBE SCREEN ONLY: stands down once a city is selected (a different, personal
   question) — see src/presets.js's own note on why presets do not try to have a second,
   city-aware meaning. */
/* GAP ABOVE THE GROUP (director, 29 Jul 2026: "зачем впритык к поиску ставить пресеты" —
   the block read as one crowded stack with the search field). He asked for a BIGGER GAP and
   for nothing else; a first pass answered it with the `.timeline` idiom — margin + padding +
   a hairline border-top — which imported a RULE the piece is not allowed to draw. The
   project's standing rule is absolute: no borders on plates, no decorative rules anywhere.
   So the separation is carried by WHITESPACE ALONE, and the whitespace absorbs what the
   border and padding were contributing plus the increase he actually asked for:
   20 + 16 = 36px of mixed margin/padding/rule  ->  44px of plain margin.
   That is still more than five times the 8px gap BETWEEN the wrapped chip rows below, so the
   group reads as its own block rather than more stack. */
.presets {
  margin-top: 44px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 8px;
  transition: opacity 0.4s ease;
}
/* MAIN GLOBE SCREEN ONLY — and "only" means GONE, not merely invisible. This was
   `opacity: 0`, which left the group's full 155px box sitting in the middle of the card's
   normal flow: the selected city's hero was pushed down behind a silent hole the size of the
   presets (measured 300x155 at y=233 with London chosen). display:none takes it out of flow
   so the card closes up. City Sky needs no rule of its own — `.panel-picker` is already
   hidden there — but it is verified as part of this, not assumed. */
body.panel-open .presets {
  display: none;
}
.pr-row {
  display: flex;
  justify-content: flex-start;
  gap: 8px 10px;
}
/* DIRECTOR-GRANTED EXCEPTION (29 Jul 2026, revised three times same day) to the project's
   "no borders, no rounding" rule — for these five preset chips ONLY: a soft OUTLINE, FULLY
   ROUNDED into a pill ("сглаживание более сильное, прям в круг"). Outline only, no fill —
   an earlier pass added a light fill and square corners; both were corrected out ("стало
   слишком плашечно, убери заливку"). Do not extend rounding OR a fill to any other control
   in the piece; if a later pass is tempted to "fix" this back to square/bordered-only, this
   comment is the record that both are deliberate, and in the OPPOSITE direction of the
   project's own house rule. Legibility comes from the outline weight/tone alone — if it
   gets hard to see, strengthen the outline, do not add a fill.
   THE PILL IS NOW `.pr-item`, NOT `.pr-btn` (29 Jul 2026, third correction): the saros "i"
   and the active preset's × both used to sit OUTSIDE the label's own bordered pill, as
   loose siblings next to it — the director saw this as "a stray third chip" / "a control
   floating next to the thing it belongs to". The border/radius/padding moved up one level,
   onto `.pr-item`, so the label, the "i" (saros only) and the × (active only) all live
   INSIDE one shared outline — "this label, and its footnote/its undo", never a separate
   control next to it. `.pr-btn` itself is now bare (no border/radius/padding) — just the
   label text, coloured by state; `:has()` reads that state back onto the shared pill. */
.pr-item {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  position: relative;
  border: 1px solid var(--line);
  border-radius: 999px;                   /* the one sanctioned rounding in the piece — a true pill */
  padding: 6px 14px;
  transition: border-color 0.2s ease;
}
.pr-item:hover { border-color: var(--ink-faint); }
.pr-item:focus-within { border-color: var(--ink-faint); }
.pr-item:has(.pr-btn[aria-pressed="true"]) { border-color: var(--gold); }
.pr-item:has(.pr-btn.is-pending),
.pr-item:has(.pr-btn.is-unavailable) { opacity: 0.4; }
/* "NEXT 50 YEARS" UNDER "WRITTEN INTO HISTORY", EACH ON ITS OWN LINE (director, 3 Aug 2026:
   "next 50 years можно под written into history, чтобы каждая на своей строчке"). The five chips
   used to be one wrapping row, which packed two or three per line by accident of how long their
   own labels happen to be — never a designed pairing. src/presets.js's DEFS array already places
   the two he names back to back ('famous' then 'next50'), so the whole of the fix is that this
   row stops wrapping and becomes a column.
   ...AND `align-items: flex-start` IS THE OTHER HALF OF IT, not a detail. The first attempt forced
   the break with `flex: 0 1 100%` on those two chips alone, which does put each on its own line —
   and also makes it fill the column: "что за бред, зачем плашки тут по ширине растянул? ширина -
   по длине текста должна быть как и было раньше". A stretched pill is a button; a pill the width
   of its own words is a label you can press, which is what these have always been. In a column
   flex the cross axis is the width, so the default `stretch` is exactly what blew them out, and
   naming `flex-start` gives every chip its own text's width back — ragged right, one per line,
   nothing else about them touched. The soft rounded outline STAYS: it is his own ask ("пресеты с
   аккуратно обводкой сглаженной") and the one place in the piece that carries one. */
.pr-row {
  flex-direction: column;
  align-items: flex-start;
}
/* PHONE ONLY (the media query below restores it). On a desktop the five chips stand in the column
   permanently, so there is nothing to summon and a trigger would be a control that opens what is
   already open. See the phone rule for why the phone cannot simply do the same. */
.pr-open { display: none; }
.pr-btn {
  padding: 0;
  margin: 0;
  border: 0;
  background: none;
  color: var(--ink-soft);
  font-family: var(--grot);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.05em;
  white-space: nowrap;
  cursor: pointer;
  transition: color 0.2s ease;
}
.pr-btn:hover { color: var(--ink); }
.pr-btn[aria-pressed="true"] { color: var(--gold); }
.pr-btn:focus-visible { outline: none; color: var(--gold); }
.pr-btn.is-pending,
.pr-btn:disabled { cursor: default; }
.pr-btn.is-unavailable { cursor: not-allowed; }
/* THE "i" — director, 29 Jul 2026, correcting his OWN earlier ask for a round badge:
   "every other 'i' in the piece (the legend's .lg-i) is square, and consistency across the
   work beats making one badge sit prettily among the pills." So this reuses the LEGEND'S
   OWN `.lg-i` class verbatim (styles.css, the legend section) — same shape, size, weight,
   colour, hover/open behaviour — rather than a parallel style that could drift from it.
   src/presets.js builds the element with `class="lg-i"`, nothing else. The one addition
   needed HERE is `flex: 0 0 auto`, because `.lg-i` was never a flex child before living
   inside this chip's `.pr-item` row (see above) — without it an inline-flex child's default
   shrink could squeeze it. Lives INSIDE the "One saros" chip's own pill, at the label's
   right, never touching the label's own hit box (the item's `gap` keeps real air between
   them) and never able to wrap onto a different line (the item never wraps internally). */
.pr-item .lg-i { flex: 0 0 auto; }
/* THE × — director, 29 Jul 2026: "и без обводки" (his "inside the outline" meant inside the
   CHIP'S OWN pill, not a ring around the × itself — that reading belongs to the "i" only, a
   different control). NO border, NO circle, NO background of its own: just the mark, quieter
   than the label, sitting in the pill `.pr-item` already draws around it. Built fresh into
   the active chip and torn down the instant it stops being active (src/presets.js's
   addActiveX/removeActiveX) — there is never a hidden-but-present × for a style change to
   accidentally reveal again. */
.pr-x {
  flex: 0 0 auto;
  padding: 0;
  margin: 0;
  border: 0;
  border-radius: 0;
  background: none;
  font-family: var(--mono);
  font-size: 14px;
  line-height: 1;
  color: var(--ink-faint);
  cursor: pointer;
  transition: color 0.2s ease;
}
.pr-x:hover,
.pr-x:focus-visible { color: var(--gold); outline: none; }
.pr-note {
  font-family: var(--grot);
  font-size: 13px;
  font-weight: 400;
  color: var(--ink-faint);
  text-align: left;
  min-height: 1.3em;
}
/* The saros "i" plate — same panel look the legend's per-type cards use (--panel-bg,
   no border, no rounding), opening DOWNWARD off the row now that the row sits near the
   TOP of the screen (there is globe below it, not above). Left-anchored to the item
   (not centred) so it never overflows past the narrow 300px column on either side.
   .lg-plate-name/.lg-plate-text/.lg-card-close are the legend's own classes, reused
   verbatim inside for identical typography. */
.pr-card {
  position: absolute;
  top: 100%; left: 0;
  margin-top: 10px;
  width: min(300px, 84vw);
  text-align: left;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity 0.18s ease, visibility 0s linear 0.18s;
  background: var(--panel-bg);
  padding: 14px 32px 16px 16px;
  z-index: 12;
}
.pr-card.is-open {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transition: opacity 0.18s ease, visibility 0s;
}
.pr-card .lg-plate-name { padding-bottom: 8px; margin-bottom: 10px; border-bottom: 1px solid var(--line); }

/* The open card is tall; on a small screen it would reach the title even from below,
   so the title recedes (never disappears) while the panel is open. */
.titleblock { transition: opacity 0.45s ease; }
@media (max-height: 720px) {
  body.panel-open .titleblock { opacity: 0.3; }
}

/* =====================================================================================
   NARROWER SCREENS
   Two real layouts, not one shrunk desktop.

   (A) TABLET / narrow desktop, <= 1000px. The three-column arrangement still holds —
       masthead left, picker and card under it, legend right — on a shorter column, since
       the bottom scrubber needs more room here than it does at full width.

   (B) PHONE, <= 620px. Not a column at all: a MASTHEAD, a PICKER, and two BOTTOM SHEETS.
       Top to bottom — masthead + the legend's button on one rail, the picker, then all the
       globe there is, then whichever sheet is up. Everything that used to be printed loose
       over the globe (the legend's type chips, the card) now lives inside a sheet with a
       real surface under it. See that block's own header for the case on each element.
   ===================================================================================== */

/* ---------- (A) tablet / narrow desktop ---------- */
@media (max-width: 1000px) {
  .titleblock { width: min(400px, 46vw); }
  .titleblock h1 { font-size: 27px; }
  /* 146px, was 182: the 182 was measured for a masthead with the [Globe | City Sky] tab bar
     on its own row underneath it (.viewtabs top:142px here). The bar is gone, so the column
     had a ~36px hole in it where the tabs used to be. Same 36px comes off the desktop value
     above for the same reason. */
  .panel { top: 146px; bottom: 118px; }
  .legend { bottom: 118px; }
}

/* ---------- (B) phone ---------- */
/* REDESIGNED 30 Jul 2026 after the director's first test on a real phone. What was here was a
   shrunk desktop: the masthead, the two view tabs, the picker, a docked card sliver, a wrapped
   strip of legend chips and the year scrubber, all six of them printed straight onto the globe
   with no surface behind any of them. Measured on the 390x844 capture with Paris chosen, the
   card's hero and the whole legend strip sat over the Sahara in the globe's own ink; the
   director's words for it were "а то не видно".

   The shell is: a MASTHEAD, a PICKER, and two BOTTOM SHEETS — the legend's, opened by a button,
   and the city's, docked at the bottom. Everything else folds into one of the two or stands
   down. See each rule's own note for the case.
   UPDATED 3 Aug 2026 (third pass): the picker lived briefly on the bottom edge, stacked under
   the city card as a fourth surface — see "THE PICKER GOES BACK TO THE TOP", further down this
   block, for why that was undone. It is a TOP RAIL again, alongside the masthead, and the two
   bottom sheets (the legend's and the city's) now share the bottom edge with the year scrubber
   rather than with the picker.
   ------------------------------------------------------------------------------------------
   FOLDED AWAY AT PHONE WIDTH, and why:
     .subtitle    the standfirst — two lines of 13px over the globe, unreadable, and the title
                  above it already names the piece. It survives at every other width and on the
                  About page.
     .viewtabs    GONE AT EVERY WIDTH, not folded — the whole [Globe | City Sky] bar was removed
                  outright (director: "кнопка Сити Скай вообще не нужна. выбор города и все").
                  Markup, rules and wiring are all out. City Sky carries its own way out
                  (.cmb-leave) and the card's "Step into <city>'s sky" is the way in.
     .presets     already stood down here before this pass.
     .hint        a pointer hint belongs to a pointer.
     .scrubber    ONLY while a city is chosen — with a place selected the card's own chart is the
                  year instrument, and two year controls on one screen would be one too many. With
                  no city chosen it is the only time control there is, and it stays, alone on the
                  bottom edge (3 Aug 2026: no longer sharing a plate with the picker, which moved
                  back to the top — see the note above).
     .lg-when     NO LONGER folded away: the older/recent/future ramp is back, inside the
                  legend sheet, where there is finally room for it.
   ========================================================================================== */
@media (max-width: 620px) {
  /* pointer hints belong to a pointer */
  .hint { display: none; }
  .presets { display: none; }
  /* THE DELICATE PHONE EXCEPTION (director, 3 Aug 2026, the SAME note that asked for the desktop
     pairing above: "и я бы обдумал как это деликатно всё же дать на мобилке тоже... пока не знаю
     как, чтобы не перегрузить интерфейс"). Read against the rule directly above this one: ALL
     FIVE presets have been folded away on a phone since an earlier pass ("already stood down
     here before this pass" — the FOLDED AWAY table's own words), because the top rail
     (.panel-picker) is a bare strip with no room to spare, not because presets themselves are a
     desktop-only idea. Undoing that fold for the whole row would re-crowd exactly the rail that
     earlier pass existed to clear. So what comes back is ONLY the two he is naming right here, in
     the exact pairing the desktop rule above gives them — nothing else returns. MEASURED (dev/
     d3_presets_phone.mjs, identical at 360x640/390x844/576x1280 since every visible chip is
     forced full-width regardless of viewport): `.panel-picker` grows from 236.9px WITH this
     exception to ~98px without it — these two chips cost the rail about 139px (94.9px of their
     own stacked content plus the block's 44px margin-top), and ONLY while the rail is showing at
     all, i.e. only before a city is chosen. That is the actual number "не перегружая интерфейс"
     was weighed against, not a guess.
     THE FIRST ATTEMPT AT IT WAS WITHDRAWN AND THEN REBUILT, AND THE DIFFERENCE IS THE WHOLE
     POINT (3 Aug 2026, same day, three passes). Un-hiding the two chips as they stand on the
     desktop put two full-width PILLS — visible outline, fully rounded — over the globe's upper
     limb, with the year plate "1878" printed straight through the word "history" and "Where I am
     now" tangled underneath (dev/shots/discfrac/q_after_576x1280_rest.png). That is not an
     argument against presets on a phone. It is three separate faults in how they were drawn:
       1. THE PILL IS A DESKTOP GRANT, NOT A GLOBAL ONE. The outline and the 999px rounding are a
          director-granted exception (see its own note above .pr-row) made for five chips standing
          on PAPER in a side column. Carried onto a phone they sit on the globe, where an outline
          is a second edge competing with the limb and a pill reads as a toolbar. So the phone
          takes the plate off entirely and keeps only the ink.
       2. TYPE ON THE BUSIEST PART OF THE FRAME. Nothing about them belonged over the disc; they
          belong in the rail, in the rail's own voice, which is .sc-here's — flat gold, 15px, no
          plate, sitting under "Where I am now" as one more quiet way into the field rather than
          as a control bar.
       3. NOTHING KEPT THE YEAR PLATES OFF THEM. famous.js had a bottom keep-out and no top one,
          so a plate could land anywhere in the rail — true of the picker before these chips
          existed, and now fixed for the whole rail at once (`topReserve`, src/main.js ->
          src/famous.js).
     AND THE DISC DOES NOT PAY FOR IT. `#presets` joins src/main.js's TOP_FURNITURE, so the rail
     is measured with them in it and the globe is fitted below rather than covered — measured
     before/after, dev/disc_fraction.mjs, in the state that carries them (no city chosen): the
     diameter is unchanged at 0.621 of the viewport height at 576x1280 and 412x915 and 0.638 at
     390x844, because at every one of those aspect ratios the WIDTH is what binds the disc, not
     the height, so a taller rail moves the globe down without shrinking it. 360x640 is the one
     phone where height binds, and it is handled on its own terms below. */
  /* THE TRIGGER. Absolutely positioned into the field's own row, so the rail is not one pixel
     taller for having it — see the arithmetic in src/main.js's TOP_FURNITURE note, which is what
     ruled a resident row out. It shares that row with CLEAR and the two ×, and never appears at
     the same time as any of them: `body.panel-open` (a city is chosen) hides it, exactly as
     `.presets` itself has always been hidden in that state. Flat gold ink, no plate, no border,
     no rounding, 15px, a 44px target bought with padding — the rail's own voice throughout. */
  .pr-open {
    position: absolute;
    top: 0; right: 0;
    z-index: 2;
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    padding: 0 1px;
    background: none;
    border: 0;
    border-radius: 0;
    color: var(--gold);
    font-family: var(--grot);
    font-size: 15px;
    font-weight: 600;
    letter-spacing: -0.01em;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    transition: color 0.2s;
  }
  body.panel-open .pr-open { display: none; }
  .pr-open[aria-expanded="true"] { color: var(--ink); }
  /* THE LIST, SUMMONED. Same idiom as #cityMenu directly above it in this rail: it opens into the
     column's flow, over the globe, on the one surface this rail is allowed to give anything (see
     .panel-picker's own note — the list is the one piece that can grow tall enough to sit over
     the globe's imagery and has to hold its own there). A wash, not a plate: no border, no
     rounding, no shadow. It is MODAL by the law at the head of this file — summoned by a tap,
     dismissed by a tap outside, by Escape, or by choosing one — so it covers rather than pushes,
     and the disc underneath is untouched at every phone size. */
  .presets { display: none; }
  body.presets-open .presets {
    display: flex;
    margin-top: 8px;
    gap: 0;
    background: var(--ground);
    padding: 4px 0 8px;
  }
  .presets .pr-item { display: none; }
  body.presets-open .presets .pr-item:has(.pr-btn[data-preset="famous"]),
  body.presets-open .presets .pr-item:has(.pr-btn[data-preset="next50"]) { display: flex; }
  /* The pill is a DESKTOP grant (see its own note over .pr-row) — five chips on paper in a side
     column. Here the same chips sit on the globe, where an outline is a second edge competing
     with the limb. Written out rather than left to cascade, because the rule being overridden is
     deliberate and a reader needs to see that this is a decision and not an omission. */
  .presets .pr-item {
    border: 0;
    border-radius: 0;
    padding: 0;
  }
  .presets .pr-btn {
    color: var(--gold);
    font-size: 15px;
    font-weight: 600;
    letter-spacing: -0.01em;
    /* A 44px target made of space, exactly as the rail's other controls are — the type does not
       grow, the box does. */
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }
  /* The active one is marked by WEIGHT OF INK, since there is no outline left to tint: the view
     you are in is printed in full ink against the other's gold. */
  .presets .pr-item:has(.pr-btn[aria-pressed="true"]) .pr-btn { color: var(--ink); }
  /* The preset's own note line, which on a desktop explains what a chip just did — it belongs to
     the list, so it comes and goes with it. */
  .pr-note { font-size: 13px; }

  /* ---------- the masthead: the title, and nothing else ---------- */
  /* right:152px keeps it clear of the top rail in the opposite corner — the ♪ toggle and the
     legend button, measured: the rail runs from x=250 to the right margin at 390px wide. */
  .titleblock { top: 15px; left: 16px; right: 140px; width: auto; }
  .eyebrow { display: none; }            /* the edition line survives on the About page */
  /* 20px, not the 24px this used to be: the masthead now shares its line with the top rail
     (♪ + Legend), which leaves it 234px. Measured, "Where the Shadow Fell" sets to ~225px at
     22px and broke to two lines by a hair; at 20px it holds one line with room to spare. */
  .titleblock h1 { font-size: 20px; letter-spacing: -0.024em; line-height: 1.05; }
  .subtitle { display: none; }

  /* ---------- the legend's button, top right ---------- */
  /* Opposite the masthead: the one corner on a phone that never collides with the city card's
     sheet at the bottom, so the legend is reachable whether or not a place is chosen. 44px of
     touch target, bought with padding — the type itself stays at the project's 13px floor. */
  .legend-btn {
    position: fixed;
    top: 8px; right: 8px;
    z-index: 22;
    display: inline-flex;
    align-items: center;
    gap: 7px;
    min-height: 44px;
    padding: 8px;
    border: 0;                    /* house rule: no outline, no plate, no rounding */
    border-radius: 0;
    background: none;
    /* The PRIMARY ink, not the desktop legend's quiet --ink-faint: this word sits alone on the
       globe's own bright rim with nothing behind it, and a tertiary grey there is the exact
       fault being fixed ("а то не видно"). It is the piece's one phone control that has to be
       findable before you know it exists. */
    color: var(--ink);
    font-family: var(--grot);
    font-size: 13px;
    font-weight: 700;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    transition: color 0.25s;
  }
  .legend-btn[aria-expanded="true"] { color: var(--gold); }
  .legend-btn:focus-visible { outline: none; color: var(--gold); }
  /* The same caret the legend's own "How to read" toggle uses — one mark, not a new icon. */
  .lb-caret {
    width: 0; height: 0;
    border-left: 4px solid transparent;
    border-right: 4px solid transparent;
    border-bottom: 5px solid currentColor;
    opacity: 0.85;
    transition: transform 0.28s ease;
  }
  .legend-btn[aria-expanded="true"] .lb-caret { transform: rotate(180deg); }
  body.intro-play .legend-btn { opacity: 0; pointer-events: none; }
  /* Inside City Sky the globe's legend has nothing to explain — the sky screen has its own
     vocabulary — and the corner belongs to that screen. */
  body.sky-open .legend-btn { opacity: 0; pointer-events: none; }

  /* THE SOUND TOGGLE, RELOCATED for the label (31 Jul 2026, second pass: "нужно Music on off,
     очень заметно, думаю в районе легенды"). It used to be a bare 30px glyph squeezed into the
     41px gap between the masthead and the Legend button, on the same rail as both — there was
     never room in that gap for "Music on"/"Music off" as words (measured: the gap is 41px,
     "Music off" alone needs ~70px at a readable size before the icon or any padding). Rather
     than shave the label down to fit a slot sized for an icon, or shrink the Legend button and
     the masthead into a pixel-fight with almost no safety margin, the control gets its OWN row,
     directly under the Legend button and sharing its right edge (`right: 8px`, unconstrained
     width) — literally "в районе легенды": the next line down from the one control the director
     was already looking at.
     WHY THIS IS SAFE. `.panel` (the picker) is pushed from `top: 58px` to `top: 120px` below to
     clear it (see that rule) — the only thing that used to sit near here, the picker's own CLEAR
     link, only exists once a place is chosen and now lands well under this row. City Sky's own
     top-left cluster (`.cmb-leave`, `.cmb-lookup-hint`, positioned by src/citysky_mapbox.js) sits
     at y 16–53 and x <= 291 (measured live); this row starts at y 60 and is right-anchored at
     x 382, clear of both axes. And #panel itself is `opacity: 0` for the whole of `body.intro-
     play` (see the "No interface during the intro" rule far above), so the one moment this
     control's pre-gesture caveat line is guaranteed on screen — before any real interaction —
     the picker is not there to be pushed into anyway. */
  .eclipse-sound {
    top: 60px;
    bottom: auto;
    right: 8px;
    left: auto;
    width: auto;
    padding: 7px 0;
    gap: 8px;
    /* THE MARK HOLDS THE RIGHT EDGE, THE WORDS HANG OFF IT (2 Aug 2026, the coherence pass).
       This control is the one piece of interface that is present on EVERY screen, and inside
       City Sky it is the mark alone — the label is unprinted there by the "only sky" block far
       below, which is right and is not being undone. But with the mark drawn FIRST the label's
       width was what sat against the right edge, so shedding the label moved the speaker about
       100px across the frame: the viewer's eye had to re-find the same control on the second
       screen. Reversed, the mark is what is anchored at `right: 8px`, the words are what come
       and go behind it, and the speaker is at the same x and the same y on both screens. */
    flex-direction: row-reverse;
    /* ...and its Y is pinned to the row's top rather than to the centre of whatever the label
       currently is. Centred, the mark rode 6px down whenever the pre-gesture caveat line
       ("starts on touch") was showing and back up when audio.js cleared it — a 22px mark moving
       6px on its own is small, but it is the one control that has to be the same object on every
       screen, and this is the last thing that could move it. Mark top is now 7px (this rule's own
       pad) under `top: 60px` on the globe and 11px under `top: 56px` in the sky: y 67 both. */
    align-items: flex-start;
  }
  .eclipse-sound .es-text { align-items: flex-end; }   /* hugs the same right edge as .legend-btn */
  .es-mark { width: 22px; height: 22px; }
  .es-label { font-size: 14px; letter-spacing: 0.02em; }
  /* Fixed to ONE line (never wraps to a second) so this row's height is predictable even in the
     rare case the picker has to render while the caveat is still showing (see the note above) —
     "starts on touch" measures ~98px, well inside what the right-anchored row has to give it. */
  .es-caveat { font-size: 13px; white-space: nowrap; }

  /* ---------- the picker: it has ONE home, and this is the top rail ---------- */
  /* #panel is a WRAPPER here and nothing else. Both of its children are `position: fixed` at
     phone width — the picker to the top rail (the block below), the card sheet to the bottom
     edge (SHEET 2) — so this box has no in-flow content in either state and is collapsed to
     nothing rather than left sitting somewhere as a ghost box over the globe. It still matters
     as a box for exactly two reasons: it carries the opacity/visibility that stands the whole
     column down inside City Sky, and the z-index that brings it back
     (`body.sky-open.citysky-mapbox.citysky-card-open .panel`, far below). */
  .panel {
    left: 16px; right: 16px; width: auto; max-width: none;
    top: auto; bottom: 0; height: 0;
  }
  .hero-date { font-size: 23px; }
  .panel-next { gap: 6px 14px; }

  /* ========================================================================================
     THE PICKER GOES BACK TO THE TOP (3 Aug 2026, third pass — reverses the 2 Aug bottom deck)
     ----------------------------------------------------------------------------------------
     Director, looking at the bottom-docked deck the PREVIOUS pass built: «твоя идея, что город
     всегда внизу — неудачная и непонятная.» and, on the same screen: «город наверное наверх
     надо всё же, а то он там внизу как часть карточки.»

     He is right about the mechanism, not only the verdict. Docked at the bottom edge, directly
     under the city card's own sheet, the field read AS PART of that card — one plate, two jobs
     — instead of as a control in its own right. Two surfaces sharing an edge with nothing
     between them is exactly that illusion, however cleanly the code kept them apart underneath.

     THE ONE THING THE PREVIOUS PASS GOT RIGHT, and this pass keeps whole: the picker does not
     move between states. That was the fault of the pass BEFORE the deck — the field lived at
     the bottom with no city and jumped to a top-rail readout the instant one was chosen —
     director then: «выбор города снизу сначала показался хорошим решением. но потом хочешь
     выбрать другой и он сверху!» That fault is not being reopened. The field, "Where I am now"
     and the suggestion list are now pinned to the TOP RAIL in EVERY state; only the deck's own
     plate and the scrubber's borrowed foot go away.

     THE SUGGESTION LIST OPENS DOWNWARD AGAIN, out of the field and over the globe — the plain
     dropdown idiom this piece used before the bottom deck briefly reversed it for one pass. It
     keeps the base `.pick-menu` rule's own opaque wash (`background: var(--ground)`, simply no
     longer overridden here) so it holds its own against the globe with tone alone — no plate
     border, no rounding, no shadow — the same technique the legend sheet uses, on a smaller
     surface.

     THE YEAR SCRUBBER GOES BACK TO THE BOTTOM EDGE ON ITS OWN, because it never actually left.
     `.scrubber` ("Bottom year-range scrubber", declared far above this block) has been
     `position: fixed; bottom: 0` through every pass, phone included — the deck never reparented
     it, it only grew a 72px foot of its own bottom padding so its opaque plate sat BEHIND the
     scrubber instead of printing over it. That padding trick is gone (the rail below carries no
     bottom reservation of any kind, being nowhere near the bottom any more) and nothing else
     has ever positioned the scrubber, so it is simply, automatically, exactly where the base
     rule already puts it: alone on the bottom edge at rest. `body.panel-open .scrubber` (its own
     rule, unchanged, further down this file) still stands it down once a city is chosen — the
     card's own chart is the year instrument then, and two year controls on one screen would be
     a new incoherence in place of the one this pass removes.

     COMPACT, ON PURPOSE. His other complaint on this same screen: «сверху остается все еще
     куча места» — a pile of dead space at the top. So the rail is sized by its rows alone (a
     46px field row, an 8px seam, a 44px "Where I am now" row) rather than by a plate or a
     centring box, and it sits `top: 116px` — 8px under the sound row's own bottom edge, measured
     108.3px at every phone width tried (390/412/576/360), worst case: the pre-gesture "starts
     on touch" caveat still on screen. 116 is a constant rather than arithmetic on purpose: the
     furniture it clears (masthead, LEGEND, MUSIC) is body-level chrome this stylesheet does not
     lay out, and src/main.js's own TOP_CHROME_PX already treats 112 as that same edge for a
     different reason (the globe's own portrait fit) — 116 leaves 4px of daylight rather than
     sharing the identical pixel with a number this file does not own.

     WHAT THIS RETIRES. --deck-h, PICKER_DECK_PX and the `bottom: var(--deck-h)` the city card
     stacked on all described a bottom-anchored rail that no longer exists, so they come out
     below rather than staying on as numbers nobody reads. THE CITY CARD NOW DOCKS STRAIGHT TO
     THE BOTTOM EDGE (see SHEET 2's own rule) — there is no rail there left to stack on.
     REPORTED, NOT PATCHED HERE: src/main.js's bottom-furniture walk-up (`BOTTOM_FURNITURE`,
     `phoneBottomBandPx`) still lists `.panel-picker` as a box that may touch the viewport floor.
     It cannot any more — this box lives at the top now, and at rest nothing in that list still
     reaches the bottom edge, though `.scrubber` genuinely does. That list is main.js's own, and
     is for that file's owner to update alongside the framing pass already under way there. */
  .panel-picker {
    position: fixed;
    left: 16px; right: 16px; top: 116px; bottom: auto;
    z-index: 19;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    /* No padding, no background: the rail is bare ink over the globe, reading as one family
       with the masthead and the LEGEND/MUSIC marks beside it rather than as a plate of its own
       — a plate here is the very thing that made the old deck read as "part of the card". Only
       the suggestion list (below) gets a surface, because it is the one piece of this rail that
       can grow tall enough to sit over the globe's own imagery and needs to hold its own there. */
  }
  /* THE SUGGESTION LIST IS NOW THE FIRST THING THAT CAN GROW THE COLUMN, directly under the
     field it belongs to — "Where I am now" and the status line follow after it, so opening it
     never puts a stray control between the query and its own results. Ordered rather than
     reparented in the DOM: src/personal.js finds every one of these by id, never by position,
     so the flex `order` below is free to run the other way from the old deck's without moving a
     single node in index.html. */
  .panel-picker .pick-row   { order: 0; }
  /* THE LIST DROPS FROM THE FIELD — IT IS NOT A SECOND PICKER STANDING IN THE RAIL (3 Aug 2026).
     Director, with a screenshot: «а это что за бред? выбрали город в одном месте, выбиралка внизу
     появилась, ну это фейспалм просто». `position: static` put this list in the COLUMN'S FLOW, so
     opening it did not overlay anything — it displaced everything under it. MEASURED
     (dev/_menu_audit.mjs, 576x1280, real tap on the field): the list arrives 389px tall and pushes
     "Where I am now" from y=170 to y=565, with the preset entry below that. What that reads as is
     exactly what he said: the field at the top, and a separate standing panel of twelve city names
     further down the same rail, with unrelated controls stranded underneath it. Two ways to choose
     a city on one screen, which is the fault this whole pass exists to remove.
     A DROPDOWN OVERLAYS. Anchored under .pick-row (order 0, min-height 46) and taken out of flow,
     so the rail behind it does not move, it is plainly the field's own list, and it is gone the
     moment the field is done with. It keeps its own opaque wash — it is the one piece of this rail
     allowed a surface, because it is the one that has to hold its own over the globe's imagery.
     THIS ALSO STOPS THE GLOBE BREATHING WITH IT. src/main.js's TOP_FURNITURE deliberately does not
     name #cityMenu ("the disc must not breathe with a menu") — but while the list was IN the flow
     it moved .sc-here, which IS named, so the measured rail swung 214 -> 162 every time the field
     was touched. Out of flow, the two agree again. */
  .panel-picker .pick-menu  {
    position: absolute;
    left: 0; right: 0; top: 46px;
    z-index: 3;
    margin: 0; padding: 0;
    max-height: 44vh;
  }
  .panel-picker .sc-here    { order: 2; margin: 8px 0 0; }
  .panel-picker .sc-msg     { order: 3; }
  /* THE PRESETS COME LAST, and src/presets.js says why in its own words: the field and "Where I
     am now" are two ways of answering "which place?" and belong next to each other, while a
     preset "is a separate act that follows". That file already inserts this block after
     #skyChooseHere in the DOM — but every other child of this rail carries an explicit `order`
     and #presets did not, so it fell into the default group and rendered ABOVE a control it is
     supposed to follow. Ordered here rather than re-anchored there, for the same reason the rest
     of this column is: nothing in src/personal.js or src/presets.js finds these by position. */
  .panel-picker #presets    { order: 4; }
  /* 44px of thumb for the field itself, bought with padding — the type stays at the 19px it is
     everywhere else, well over the 13px floor. The row keeps the deck's own PINNED 46, unchanged
     — the row's own height math never cared which edge it was docked to. */
  .panel-picker .city-input { padding: 10px 1px; }
  .panel-picker .pick-row { min-height: 46px; align-items: center; }
  /* CLEAR and its two × companions (the "closing a city" pass, see index.html and
     src/personal.js) are real thumb targets on the rail, made of space as everything else here
     is: no plate, no border, no rounding — only the box each answers to grows. */
  .panel-picker .pick-clear,
  .panel-picker .pick-x {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 44px;
    padding: 0 1px;
  }
  /* CLEAR is a line of words and is already well past 44px wide on its own (it measures ~52).
     The × is one glyph — `min-width` is what buys IT the same real target, rather than a hit
     area 20-odd px wide with 44px of empty air above and below it. */
  .panel-picker .pick-x { min-width: 44px; }
  /* ...AND EACH × HAS TO LOOK LIKE IT BELONGS TO SOMETHING (3 Aug 2026). Two 44px boxes with the
     glyph centred in each, separated by the row's own 12px gap, printed as «× CLEAR ×»: three
     marks in a line, evenly spaced, none of them visibly attached to anything — measured at
     576x1280, the field's underline ended at x=383 and its × sat at 418, floating in the gap. That
     is the opposite of the «более логичная система» he asked for.
     THE TARGET STAYS 44px AND THE INK MOVES INSTEAD. Each × is pushed to the LEFT edge of its own
     box so it hugs the thing it closes — the field's × terminates the field, CLEAR's × sits
     against the word — and the row's gap is spent between the two GROUPS rather than evenly
     between all four children. Nothing about the hit areas changes; they still tile the rail. */
  .panel-picker .pick-row { gap: 0; }
  .panel-picker .pick-x { justify-content: flex-start; padding-left: 3px; }
  .panel-picker .pick-clear { margin-left: 14px; }
  /* ...and `display` above would otherwise beat the UA's own `[hidden] { display: none }`,
     which is the whole mechanism src/personal.js uses to keep these off the screen until there
     is something to clear. */
  .panel-picker .pick-clear[hidden],
  .panel-picker .pick-x[hidden] { display: none; }
  /* A column that stretches its children would blow "Where I am now" out to the full width of
     the rail; it is a line of words with an icon, so it takes its own width and sits on the left. */
  .panel-picker .sc-here { align-self: flex-start; }
  /* "WHERE I AM NOW" BELONGS TO THE EMPTY STATE, unchanged reasoning from the deck it replaces:
     once a place is named, the field is holding that name and is one tap from the whole list,
     and geolocation cannot name a city other than the one you are already standing in. CLEAR
     (or either ×) is the way back to the state that offers it. .sc-msg is NOT covered by this —
     it is status, and has to be able to speak in either state. */
  body.panel-open .panel-picker .sc-here { display: none; }

  /* ========================================================================================
     THE BOTTOM EDGE HAS ONE LAW (2 Aug 2026, the coherence pass — table updated 3 Aug 2026
     when the picker left the bottom edge for the top rail; the LAW ITSELF is unchanged)
     ----------------------------------------------------------------------------------------
     Director: «ux на мобилке плохо продуман. связь между экранами, тултипами, состояниями.»
     Everything that still lives on this phone's bottom edge divides cleanly into two kinds,
     and each kind has ONE behaviour:

       PERSISTENT — exactly one of these owns the edge at a time; the other stands down rather
       than stacking under it, so there is never a seam between two persistent surfaces.
         the year scrubber   no city chosen                             bottom: 0
         the city card       replaces it, once a place is named         bottom: 0
       MODAL — they are summoned, they COVER the edge outright, and ONE TAP OUTSIDE puts them
       away again on either screen.
         the legend sheet   summoned by LEGEND, top right               bottom: 0
         the eclipse card   summoned by a tap on an eclipse             bottom: 0

     So: if it is on screen because of where you are, it OWNS the edge alone; if it is on screen
     because you asked for it, it covers, and tapping the picture behind it returns you. Nothing
     has to be re-learned crossing from the globe to a city to an eclipse to the sky, because
     there is one rule and four surfaces obeying it.
     (THE PICKER ITSELF NO LONGER PLAYS IN THIS TABLE. It moved to the top rail in the 3 Aug
     pass — see "THE PICKER GOES BACK TO THE TOP", above — because reading as part of the same
     bottom-edge stack as the city card was exactly what made it look like a piece of the card
     rather than its own control. The law here was always about the BOTTOM edge specifically;
     a rail that is not on that edge is simply outside its scope now, same as the masthead.)

     THE ONE THING THAT COULD STILL SHOW TWO PLATES AT ONCE was the legend sheet over the city
     card. Measured before the coherence pass at 360x640 with Paris chosen and the legend up:
     the legend is 433 tall and the EXPANDED card 403, so 44px of the card's own plate stood
     above the legend's top edge — two surfaces, two grips, and a stripe of globe trapped
     between them. A modal surface covers, so the card goes away while the legend is up and
     comes back at the detent it was left in. `visibility` rather than `display`: the sheet
     keeps its laid-out height, so src/timeline.js's mountSheet has nothing to re-measure on the
     way back, and src/main.js's phoneBottomChromePx() correctly stops counting it and starts
     counting the legend (it already skips `visibility: hidden`).

     THE ECLIPSE CARD GOES THE SAME WAY, for the same reason and by the same rule: it is raised
     by a tap on the globe, and the LEGEND button in the opposite corner is still perfectly
     reachable while it is up, so two modal sheets could stand on the same edge at the same
     z-index with nothing deciding between them but source order. The legend covers everything
     and gives it back — the eclipse stays pinned underneath and its card returns when the legend
     is dismissed, exactly as the city card does. `visibility` again, so src/main.js's own
     tapCardUp state is never contradicted by the stylesheet.
     ======================================================================================== */
  body.legend-open.panel-open .panel-body,
  body.legend-open .tooltip.tap-card-sheet { visibility: hidden; }

  /* ========================================================================================
     THE GRIP — shared by both sheets
     ======================================================================================== */
  .sheet-grip {
    display: flex;
    align-items: center;
    gap: 10px;
    /* 44px of thumb, made of space: 13px type in a 44px-tall strip. No pill, no handle bar. */
    min-height: 44px;
    padding: 12px 0 10px;
    margin: 0;
    cursor: grab;
    /* The gesture belongs to the sheet, not to the page: without this the browser claims the
       vertical drag for its own scroll before a pointermove is ever delivered. */
    touch-action: none;
    -webkit-tap-highlight-color: transparent;
    user-select: none;
    -webkit-user-select: none;
  }
  .sheet-grip:active { cursor: grabbing; }
  /* The card's grip is .panel-body's first child, and the card's own
     `.panel-body > *:first-child { padding-top: 0 }` rule — written for the desktop card, where
     the first block must sit straight under the picker's underline — was zeroing the grip's top
     padding. Measured, the sky button's outline ended up flush against the sheet's top edge.
     Re-asserted here, inside the phone block, so the desktop rule keeps doing its own job. */
  body.panel-open .panel-body > .sheet-grip { padding-top: 12px; padding-bottom: 12px; }
  /* The CARD's grip belongs to the card. With no place chosen there is no sheet — .panel-body
     is still in the picker's own flex column and unplated — so its grip must not stand there
     under the search field advertising a drag that does nothing. */
  body:not(.panel-open) #cardGrip { display: none; }
  .sg-caret {
    flex: none;
    width: 0; height: 0;
    border-left: 5px solid transparent;
    border-right: 5px solid transparent;
    border-bottom: 6px solid var(--gold);
    transition: transform 0.28s ease;
  }
  /* Open, the caret points the way out — down. */
  .is-open-detent .sg-caret { transform: rotate(180deg); }
  .sg-cue {
    font-family: var(--grot);
    font-size: 13px;
    font-weight: 600;
    letter-spacing: 0.04em;
    color: var(--gold);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  /* ---------- THE WAY INTO CITY SKY, IN THE COLLAPSED SHEET ----------------------------------
     With the tab bar gone the card's own button is the ONLY route into City Sky, and measured
     in the card's natural reading order it sat at y 961–1013 in an 844-tall viewport: below the
     fold of the collapsed sheet, reachable only by someone who had already discovered that the
     sheet drags open. A visitor who never drags could not reach that screen at all.
     FIX: src/personal.js moves the button (the same node, not a copy) into the GRIP ROW at phone
     width, so it rides at the top of the sheet and is on screen in BOTH detents. It costs the
     collapsed state no extra height — the row was already there — and it puts the way in and the
     way open on the same line, which is the honest reading: this card goes somewhere.
     It keeps its sanctioned --gold outline, at a size that fits a thumb row. */
  .sheet-grip .sky-enter {
    width: auto;
    flex: 0 1 auto;
    min-width: 0;
    margin: 0 0 0 auto;                 /* pushed to the right of the drag cue */
    padding: 8px 12px;
    min-height: 40px;
    font-size: 13.5px;
    line-height: 1.15;
    /* IT WRAPS NOW, IT DOES NOT TRUNCATE (2 Aug 2026, the long-name pass). This was
       `white-space: nowrap; overflow: hidden; text-overflow: ellipsis`, and measured on a
       390x844 phone (dev/longname_probe.mjs) that ellipsis was eating the part of the label
       that says what the button DOES: "Villeray–Saint-Michel–Parc-Extension's sky →" came out
       as "Villeray–Saint-Michel–Parc-E…", with «'s sky» and the arrow both gone. A control
       whose verb has been truncated away is not a shortened control, it is a broken one — and
       it is the same «неаккуратно» the director raised. It takes a second line instead: the
       row grows by about 15px in the collapsed sheet, which measurePeek() in src/personal.js
       reads live rather than assuming, and the whole label survives at every name in
       data/cities.json. The desktop button's own long-name rules are inherited from
       .sky-enter above, arrow-binding included. */
    white-space: normal;
    text-align: right;
    /* The grip owns the vertical drag; this button owns its own taps. */
    touch-action: manipulation;
  }
  /* The cue gives way before the button does: on a narrow screen with a long city name it is the
     drag cue that shortens, never the route into the sky. */
  .sheet-grip .sg-cue { flex: 0 1 auto; min-width: 0; }

  /* ========================================================================================
     SHEET 1 — THE LEGEND
     The desktop column becomes a sheet carrying exactly what the column carried: the type key
     with its four "i" explanations, the older -> recent -> future ramp, and the About link.
     Nothing is dropped this time; the phone finally has room for the ramp because the content
     is no longer competing with the globe for the same pixels.
     ======================================================================================== */
  .legend {
    position: fixed;
    top: auto; bottom: 0; left: 0; right: 0;
    width: auto;
    z-index: 20;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    /* A sheet may rise as far as the last of the BODY-LEVEL top chrome (112px, src/main.js's
       TOP_CHROME_PX) and no further, or the LEGEND button and the MUSIC row print over their own
       sheet. Unlike the city card's own cap (SHEET 2, above) this one does NOT also clear the
       picker rail — and does not need to: the picker is a persistent surface with z-index 19,
       the legend is a MODAL one at 20, so if this sheet's content is ever tall enough to reach
       that high it is supposed to cover the rail, exactly as "THE BOTTOM EDGE HAS ONE LAW" says
       a modal surface does. Measured, it does not bite at any tested size — the legend's content
       is 455-468px, well under this cap at every phone height tried. */
    max-height: calc(100vh - 112px);
    overflow: hidden;
    padding: 0 16px calc(18px + env(safe-area-inset-bottom, 0px));
    /* THE PLATE. Two existing tokens, composited: --panel-bg is the sheet's own surface but is
       deliberately translucent (0.82) for its desktop uses, and a translucent plate over the
       globe is the very problem being fixed. Laying it over --ground — the piece's current sky
       colour, which src/main.js interpolates toward night with everything else — makes it
       opaque without inventing a colour and without losing the day/night inversion. No border,
       no rounded top edge, no shadow: tone alone separates it from the globe. */
    background: linear-gradient(var(--panel-bg), var(--panel-bg)), var(--ground);
    /* Hidden by its own height until mountSheet says otherwise; the fallback keeps it off
       screen for the frame before the script runs. */
    transform: translateY(var(--sheet-hidden, 100%));
    pointer-events: auto;
  }
  .legend.is-closed { visibility: hidden; }
  .legend > * { pointer-events: auto; }
  .legend .sheet-grip { justify-content: flex-start; }
  .legend:not(.is-closed) .sg-caret { transform: rotate(180deg); }

  /* Inside the sheet the column reads top to bottom as it does on desktop. "How to read" is
     the desktop column's declutter toggle — inside a sheet that can simply be closed, it is a
     control with nothing to do, so only the section label stays on the header row. */
  .legend-help { display: none; }
  /* ...and with the toggle gone, its state must not be able to strand the sheet. main.js sets
     data-guide on the legend and the desktop rule collapses the body and the "Type" label from
     it; a reader who collapsed the column on a desktop and then narrowed the window would open
     an empty sheet. Here the sheet's own open/closed IS the declutter, so the state is ignored. */
  .legend[data-guide="true"] .legend-body { display: flex; }
  .legend[data-guide="true"] .legend-title { display: inline; }
  .legend-head { margin-bottom: 14px; }
  .legend-body {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    overflow-y: auto;
    overscroll-behavior: contain;
  }
  /* 8px, not 0 (2 Aug 2026, the coherence pass). The "i" carries the piece's ONE sanctioned
     hairline, and at 44px of touch target four of them stacked flush share their edges: measured
     on the 390x844 legend sheet, the four boxes drew as a single 176px-tall ruled column with
     three internal dividers — a table, which is a different object from four separate marks and
     is exactly the kind of chrome the no-borders rule exists to prevent. Air between the rows
     gives each mark its own box back. Costs the sheet 24px of its 433. */
  .lg-types { flex-direction: column; gap: 8px; }
  /* Full-width rows: the swatch + name on the left rail, the "i" on the right, each row a real
     44px target. Spacing, not chrome. */
  .lg-row { justify-content: space-between; gap: 12px; min-height: 44px; }
  .lg-row .chip { flex: 1 1 auto; padding: 10px 2px; font-size: 14px; }
  /* Measured at 34x33 with padding alone — under the 44px a thumb needs. The hairline box is
     the sanctioned exception it has always been; what grows here is the TARGET, and the mark
     inside it stays exactly the 13px mono "i" it is everywhere else. */
  .lg-i {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 44px; min-height: 44px;
    padding: 0;
  }
  .lg-when { margin-top: 16px; padding-top: 14px; }
  .legend-filter-note { width: 100%; text-align: left; }
  .legend .lg-about { margin-top: 14px; padding-top: 14px; min-height: 44px; display: flex;
                      align-items: center; }

  /* The four "i" explanation cards open OVER the sheet's own body, anchored to its bottom, so
     a card is always within thumb reach of the "i" that opened it. */
  .lg-cards {
    position: absolute;
    /* Below the grip, so the sheet can still be dragged shut while a card is open — the card
       fills everything under it rather than floating at the bottom with a band of empty plate
       above it (which is what `bottom: 0` alone gave: a 315px card in a 433px sheet). */
    top: 46px; bottom: 0; left: 0; right: 0;
    z-index: 6;
    /* MUST be re-asserted here. .lg-cards is a direct child of .legend, so the column's own
       `.legend > * { pointer-events: auto }` rule catches it — harmless on desktop, where this
       overlay is a zero-height box under the header, but here it is stretched over the WHOLE
       sheet and it silently swallowed every pointerdown on the grip: the sheet opened from its
       button and then could not be dragged shut. The open CARD opts back in on its own
       (.lg-card.is-open), which is the only part of this layer that should ever take a touch. */
    pointer-events: none;
  }
  /* The desktop plate unpins from its column and is a fixed 324px wide; the sheet is the whole
     screen and the card fills it, so the desktop width MUST be undone here or a 390px phone
     would get a 324px card floating against its right edge. */
  .lg-card {
    top: 0; bottom: 0; left: 0; right: 0;
    width: auto;
    max-height: none;               /* the sheet already bounds it here; the desktop cap is not wanted */
    overflow-y: auto;
    padding: 16px 46px 22px 16px;   /* right pad clears the × at its 44px target size */
  }
  /* NOTHING TO OVERRIDE FOR THE DRAWING ANY MORE, and that is the point. The cone is capped at
     272 CSS px globally, and the desktop card was widened to 324 precisely so its content width
     is that same 272 — so the geometry is drawn at ONE size on both surfaces instead of 216
     here and 272 there. The swatch is absolute px by contract and the Sun is a fixed height, so
     neither has a width to re-state. Nothing inside the figure is type; the 13px floor is
     untouched by any of it.
     WHAT DOES GET TRIMMED HERE is the air around the row and the plate's own bottom padding.
     The sheet's height is its own content's, not the card's (.lg-cards is absolutely positioned
     over it), so a card taller than the sheet scrolls inside itself — which is fine and already
     how a short desktop window behaves, but it should cost as little as possible. Measured on
     390x844: the tallest card is 405 against 387 of sheet, so the last line of the caveat is
     the only thing under the fold. Trading any of the cone's size for those 18px would be
     trading the explanation for the footnote. */
  .ex-sun { height: 58px; }
  .ex-row { margin: 8px 0 10px; }
  .lg-card { padding-bottom: 16px; }
  .lg-card-close { top: 4px; right: 4px; width: 44px; height: 44px; font-size: 17px; }

  /* ========================================================================================
     SHEET 2 — THE CITY CARD
     "городу нужна карточка которая будет снизу на плашке. пару строк видно сразу. график хотя
     бы полных затмений. остальное, если вывести плашку наверх."
     COLLAPSED: the grip's line, the hero (which city, and the next eclipse there), and the
     chart cut down to the rare lanes — Total/Annular/Hybrid — see COMPACT_LANES in
     src/timeline.js. EXPANDED, dragged up: the full chart, its tally line, "Totality here"
     last/next, and the way into City Sky.
     Gated on body.panel-open: with no city chosen this box is genuinely empty and an empty
     plate would be exactly the stray chrome the house rule forbids.

     IT SITS ON THE SCREEN EDGE AGAIN (3 Aug 2026, third pass). The picker moved back to the top
     rail — see "THE PICKER GOES BACK TO THE TOP", far above — so the bottom edge it used to
     share with a stacked rail is now free, and this sheet docks straight at `bottom: 0`, the
     same edge the year scrubber owns while no city is chosen (see "THE BOTTOM EDGE HAS ONE
     LAW"). Nothing stacks under it any more; it simply replaces the scrubber on the same edge. */
  body.panel-open .panel-body {
    position: fixed;
    left: 0; right: 0; bottom: 0; top: auto;
    width: auto;
    margin-top: 0;
    z-index: 18;
    /* The box is laid out ONCE at min(content, this cap) and a transform hides all but the
       detent's worth of its top — so a finger drag moves a composited layer and the chart inside
       is never re-laid mid-gesture. See mountSheet in src/timeline.js.
       THE CAP CLEARS BOTH ENDS OF THE TOP RAIL, NOT JUST THE BODY-LEVEL CHROME (3 Aug 2026).
       112px is TOP_CHROME_PX in src/main.js — the masthead/LEGEND/MUSIC row's own bottom edge,
       measured 108.3px at every phone width tried. The picker's OWN rail (this file's
       `.panel-picker`, `top: 116px`) sits directly under that and is NOT body-level chrome, so
       a cap of 112 alone would let an expanded card climb up underneath the field and CLEAR —
       both z-index 18 against the rail's 19, so the rail would print over the card's own plate.
       172 is 112 + the rail's own measured height once a city is chosen (pick-row alone, "Where
       I am now" hidden — see `body.panel-open .panel-picker .sc-here`) + a few px of daylight,
       so the expanded card's own top edge lands just clear of the field it shares the screen
       with. Measured, not assumed: dev/toprail_probe.mjs at 390/412/576/360. */
    max-height: calc(100vh - 172px);
    /* THE SAFE-AREA INSET IS BACK IN THIS BOX'S OWN PADDING (3 Aug 2026). While the picker rail
       held the bottom edge, IT reserved the device's safe area and this sheet did not touch the
       physical edge at all. Now that the sheet docks straight to `bottom: 0`, it is the one
       thing sitting on that edge and has to hold the inset open itself, the same way the legend
       sheet already does.
       (16px is also load-bearing arithmetic: src/personal.js's measurePeek() reads this box's
       own computed padding-bottom LIVE, so the number does not have to be kept in sync by hand —
       on a device with no safe-area inset `env(..., 0px)` still resolves to plain 16px, so
       nothing about the collapsed detent's own height changes anywhere the inset is zero.) */
    padding: 0 16px calc(16px + env(safe-area-inset-bottom, 0px));
    background: linear-gradient(var(--panel-bg), var(--panel-bg)), var(--ground);
    overflow: hidden;
    transform: translateY(var(--sheet-hidden, 0px));
    /* The sheet owns the touches inside its own bounds; the globe owns every pixel outside it,
       which is what keeps drag-to-turn and pinch-to-zoom alive around the sheet. */
    pointer-events: auto;
  }
  /* Expanded, the card may be taller than the box and scrolls inside it. The drag never starts
     in this scrolling region — only on the grip and the hero above it — so the two gestures can
     never fight over the same finger. */
  body.panel-open.card-expanded .panel-body { overflow-y: auto; overscroll-behavior: contain; }
  body.panel-open .panel-body > * { pointer-events: auto; }

  /* PHONE READING ORDER. The chart comes straight after the hero, because those three — which
     city, the next eclipse, the chart — are what the collapsed sheet has to carry. "Totality
     here" and the way into City Sky are what dragging it up is for. */
  body.panel-open .panel-body .sheet-grip { order: 0; }
  body.panel-open .panel-deep  { order: 1; }
  body.panel-open .timeline    { order: 2; }
  body.panel-open .cd-actions  { order: 2; }   /* tied with the chart; see the desktop rule's own note on why a tie plus DOM order is "under the chart" rather than a fixed number */
  body.panel-open .panel-next  { order: 3; }
  body.panel-open .panel-result{ order: 5; }
  body.panel-open .panel-note  { order: 6; }
  /* .sky-enter has no order here: at phone width it is not a child of .panel-body at all — it
     lives inside the grip row (see the rule above). */

  /* The hero is part of the grip: the whole block a thumb naturally lands on drags the sheet.
     Its old "tap for the timeline and detail" line is gone — the grip's own cue says it now,
     at the top of the sheet where the finger actually is. */
  .panel-deep {
    padding-top: 0;
    border-top: none;
    touch-action: none;
    cursor: grab;
  }
  .panel-deep::after { content: none; }
  .hero-kicker { margin-bottom: 4px; }
  .hero-sub { margin-top: 6px; }
  /* The chart sits tight under the hero in the collapsed sheet; its own caption belongs to the
     expanded state, where there is room for a sentence. */
  .timeline { margin-top: 14px; }
  body.panel-open:not(.card-expanded) .tl-head { display: none; }

  /* ---------- the year scrubber ---------- */
  .scrubber { padding: 0 16px; }
  .scrubber .scrub-track { left: 16px; right: 16px; }
  .scrubber .scrub-readout { font-size: 14px; }
  .scrubber .scrub-tick-label { font-size: 13px; letter-spacing: -0.05em; }
  /* Six labelled stations need ~340px of clear run and there are 343. They collided
     ("2000 BCE" ran into "1000 BCE"). The TICKS all stay — only three of the six keep a
     LABEL: the two ends and year 1. Floor is 13px, so thinning is the only lever. */
  .scrubber .scrub-tick:nth-child(2) .scrub-tick-label,
  .scrubber .scrub-tick:nth-child(4) .scrub-tick-label,
  .scrubber .scrub-tick:nth-child(5) .scrub-tick-label { display: none; }
  /* ...and it stands down entirely once a place is chosen: the card sheet is docked exactly
     where it sat, and for a city the card's own chart is the year instrument. Hidden, not
     removed — src/scrubber.js keeps its state and it comes straight back on Clear. */
  body.panel-open .scrubber { opacity: 0; visibility: hidden; pointer-events: none; }

  .yearcount { font-size: 32px; bottom: 56px; letter-spacing: -0.03em; }

  /* "Where I am now" — geolocation is the one entry that is BETTER on a phone than on a desktop,
     so it gets a real thumb target here rather than the desktop's inline line of text. Still no
     plate, no border: the height is spacing. */
  .sc-here { min-height: 44px; display: inline-flex; align-items: center; }
  .skyview-overlay .sky-title { top: 58px; font-size: 19px; }
  .skyview-overlay .sky-exit { top: 18px; left: 16px; }
  .skyview-overlay .sky-caption { width: calc(100vw - 32px); bottom: 34px; font-size: 13.5px; }
}

/* The two sheets never animate for a reader who asked for less motion — they simply are where
   they are asked to be. */
@media (prefers-reduced-motion: reduce) {
  .sheet-anim { transition: none; }
}

/* =====================================================================================
   CITY SKY MODE ("Небо города") — the entry button in the city card + the immersive
   first-person overlay. House rule holds: NO borders, NO rounding, NO plates. The CTA
   reads as a real, inviting button through weight, muted-gold colour and size.
   ===================================================================================== */

/* The prominent CTA in the picker column, once a city is chosen. A serif, gold, sizeable
   line with an arrow and a gold underline that thickens on hover — a button by weight and
   colour, not a box. */
.sky-enter {
  display: block;
  width: 100%;
  margin: 18px 0 2px;
  padding: 10px 14px 11px;
  /* FILLED AT REST (director, 30 Jul 2026: "кнопку про step Into наоборот сделай с заливкой,
     чтобы привлекала больше внимания"). It was an outline that filled on hover; the fill has
     moved to the resting state, so the one call to action on this screen carries weight the
     moment the card opens instead of waiting to be found. Hover now lightens it (below) —
     the same two tokens, the other way round. */
  background: var(--gold);
  /* AN OUTLINED BUTTON (director: "step into paris sky нужно с обводкой - как кнопку").
     The one sanctioned outline in the piece: this is the call to action, and it has to read
     as pressable rather than as another line of gold text. Square, per the standing rule.
     Kept on the filled state so the box still has a defined edge against the paper. */
  border: 1.5px solid var(--gold);
  border-radius: 0;
  text-align: center;
  color: var(--ground);
  font-family: var(--grot);
  font-size: 17px;
  font-weight: 600;
  letter-spacing: -0.018em;
  line-height: 1.2;
  cursor: pointer;
  pointer-events: auto;
  transition: color 0.2s, border-color 0.2s;
  /* LONG-NAME HARDENING (director: "если название города крупно, кнопку расфигачивает").
     "Step into <city>'s sky" carries whatever the GeoNames record calls the place — some
     run to 30+ characters, occasionally as a single hyphen-free token. The box already
     wraps naturally at word breaks; this adds a break opportunity INSIDE a word when the
     word alone cannot fit, so no single token can ever force the box wider than its column
     and bleed over the globe. The button simply grows taller and stays square and centred —
     "wraps cleanly", never overflows or clips, no font-size hack needed.

     THE ARROW USED TO END UP ALONE ON A LINE (2 Aug 2026, director with a Saint Petersburg
     screenshot: «про длинные названия городов говорил уже как-то. неаккуратно.»). Measured
     at 1280x900, dev/longname_probe.mjs: "Step into Saint Petersburg's sky" exactly filled
     line one and "→" sat by itself on line two under it.
     THE CAUSE WAS THIS RULE, not the label. src/personal.js already binds the arrow to the
     word before it with a NO-BREAK SPACE ("sky&nbsp;<span class=se-arrow>&rarr;</span>"),
     which is the correct way to say "these two travel together" — but `overflow-wrap:
     anywhere` (and `word-break: break-word`, which Chrome treats as the same thing)
     licenses a break at ANY position that helps a line fit, INCLUDING one a no-break space
     forbids. So the browser split "sky→" rather than moving the pair down whole.
     `overflow-wrap: break-word` keeps the protection that pass was actually after — an
     unbreakable sequence longer than the line still gets broken — but only when there is no
     acceptable break point at all, which leaves the no-break space standing. Re-measured:
     the arrow now rides down with "sky" on every name in data/cities.json.
     `text-wrap: pretty` is the belt to that brace: it asks the browser not to leave a short
     last line in the first place, for the ordinary orphan case a no-break space cannot
     reach (a two-word city whose second word lands alone). Purely an improvement where it
     is supported and inert where it is not. */
  overflow-wrap: break-word;
  hyphens: auto;
  text-wrap: pretty;
}
/* BUGFIX (27 Jul 2026): `display: block` above is an AUTHOR rule, and author rules beat
   the user-agent `[hidden]{display:none}` regardless of specificity — so the `hidden`
   attribute personal.js sets/clears on this button (id skyEnterBtn) was being silently
   ignored and the CTA stayed on screen after × CLEAR. Same fix pattern used below for
   .skyview-overlay and for .sc-here/.sc-msg above; re-assert it here. */
.sky-enter[hidden] { display: none; }
.sky-enter .se-arrow { display: inline-block; transition: transform 0.2s; margin-left: 2px; }
/* HOVER DEEPENS THE FILL (30 Jul 2026). The invert he asked for earlier the same day — ink
   fill, paper label — is now the RESTING state's job, since he asked for the button to carry
   its weight unprompted. So hover can no longer be "fill it in"; it goes the other way and
   presses the fill down to the piece's own ink, keeping the paper label. Two tokens, no new
   hex, and the 1.5px --gold outline (the sanctioned exception above) stays on both states, so
   the box keeps a defined edge whichever tone is inside it. */
.sky-enter:hover,
.sky-enter:focus-visible {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--ground);
  outline: none;
}
.sky-enter:hover .se-arrow,
.sky-enter:focus-visible .se-arrow { transform: translateX(4px); }

/* ---------- "Where I am now": geolocation, in the picker ----------
   REMOVED (30 Jul 2026): .sky-empty-head / .se-title / .se-lede — the "Stand somewhere and look
   up" invitation. It was the empty state of the City Sky TAB, and the tab is gone, so the state
   cannot arise. Its markup went from index.html with it.
   The geolocation control survives and is now a permanent, ordinary part of the picker: a
   second way to name a place, which on a phone is the better one. It commits a city exactly as
   typing one does.
   THE GOLD UNDERLINE IS GONE (30 Jul 2026). It was defensible while this sat inside a bordered-
   off empty state; permanently in the picker it printed a second 1.5px rule 20px under
   #citySearch's own hairline, and the column read as a form with two fields rather than one
   input and a quiet alternative. The standing rule is no borders, and there was no case left
   for the exception: gold ink at 15px/600 with an arrow is exactly how .lg-about and .pick-clear
   already say "this is pressable" without a line under it. */
.sc-here {
  display: inline-block;
  margin: 12px 0 0;
  padding: 3px 0 5px;
  background: none;
  border: none;
  border-radius: 0;
  color: var(--gold);
  font-family: var(--grot);
  font-size: 15px;
  font-weight: 600;
  letter-spacing: -0.01em;
  cursor: pointer;
  transition: color 0.2s;
}
.sc-here[hidden] { display: none; }
/* The locate icon: currentColor means it needs no hover rule of its own — .sc-here's own
   `color` transition (gold -> ink below) carries the icon along with the label. Quiet by
   design (director: "keep it quiet") — no scale/translate on hover, just the same ink move
   the label text makes. */
.sc-here .sc-locate { display: inline-block; vertical-align: -2.5px; margin-left: 6px; }
.sc-here:hover, .sc-here:focus-visible { color: var(--ink); outline: none; }
.sc-msg {
  margin-top: 8px;
  font-family: var(--grot);
  /* 13px, the project floor. This was 12.5px — under it, and the only place in the sheet that
     still was. */
  font-size: 13px;
  line-height: 1.5;
  color: var(--gold);
}
.sc-msg[hidden] { display: none; }

/* While in the sky, the globe's chrome recedes entirely (the sky overlay owns the screen). */
body.sky-open .legend,
body.sky-open .panel,
body.sky-open .scrubber,
body.sky-open .hint,
body.sky-open .titleblock,
body.sky-open .famous-labels,
body.sky-open .arclabel,
body.sky-open .tooltip,
/* The globe's own personal year labels were MISSING from this list, so ~15 of them drew
   over City Sky at globe screen coordinates — the "year labels piling up over the sky"
   the director saw. This selector is the real fix (skyview was patching it at runtime). */
body.sky-open .personal-labels,
body.sky-open .yearcount { opacity: 0; pointer-events: none; visibility: hidden; }

/* ---------- City layer (clickable places on the globe) ----------
   Quiet warm-ink names beside near-constant-size dots: the cities must never compete
   with the eclipse glyphs, which are the subject. Hidden outright during the opening
   and inside City Sky (which draws its own scene, so the per-frame placer never runs
   there and would otherwise leave stale names on screen). */
body.sky-open .city-labels,
body.intro-play .city-labels { display: none; }
/* HOVER FOCUS. Resting on one eclipse should pick it out of the whole field — "при наведении на
   любое затмение, оно вообще должно сильно хайлатиться на глобусе. сейчас ничего не становится
   бледнее, ни другие затмения, ни плашки, ни города, ничего". The glyph field and the band web
   recede in the shader (see pointsApi.setFocus / setDim and pathsApi.setBandFade in src/main.js);
   the DOM layers on the globe recede here, together and on the same easing, so the hovered
   eclipse — which alone keeps its mark, its path and its tooltip — is the only thing at full
   strength. Not display:none: they stay legible, just quiet, so the frame does not flicker. */
/* 0.34, not 0.18: on a Total the scene's own inversion darkens these too, and the two together
   took them off the screen entirely — "бледнее - да, но не надо всё убирать". */
/* LOOK TUNER hook: --eclipse-focus-recede, set on :root only by src/tuner.js (Hover group);
   unset in the normal path, so the 0.34 fallback wins — same convention as --iso-label-size. */
body.eclipse-focus .city-labels,
body.eclipse-focus .famous-labels,
body.eclipse-focus .personal-labels { opacity: var(--eclipse-focus-recede, 0.34); }
.city-labels, .personal-labels { transition: opacity 260ms ease; }
/* NOTE: src/personal.js writes these two rules INLINE as well (CITY_LABEL_CSS and the
   home label's cssText), and an inline declaration beats this sheet. Both of its inline
   strings reference var(--serif)/var(--sans), which are now aliases of --grot, so the
   FAMILY comes across on its own; the sizes and tracking below only take effect if that
   file's inline font declarations are removed. See the follow-up note at the foot. */
/* Sentence case, deliberately: these must never compete with the eclipse glyphs, which
   are the subject, and setting ~40 of them in tracked caps made the globe read as an
   atlas instead. Only the chosen city speaks up (below). */
.city-labels .cl-name {
  font-family: var(--grot);
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 0.06em;
  text-transform: none;
  color: var(--ink-soft);
}
/* The chosen place is the one city that speaks up — gold, larger, at display weight, so
   it reads as "your place" rather than as one more dot. */
/* `.city-labels .cl-name.is-home` stood here — the chosen city's name printed in gold beside its
   own standpoint on the globe. The word is gone (director, 2 Aug 2026: "слово париж возле
   локатора тоже не надо подписывать"); the lozenge stays and the place is still clickable,
   because that pick is geometric and never depended on the label. src/personal.js no longer emits
   the class, so this rule had nothing left to style. Removed rather than left dark, so the next
   reader does not have to prove it is dead a second time. */

/* The immersive overlay: labels + chrome drawn over the sky canvas. Pointer-transparent by
   default so drags to look around pass through; interactive children re-enable pointers. */
.skyview-overlay {
  position: fixed;
  inset: 0;
  z-index: 40;
  pointer-events: none;
  font-family: var(--grot);
  color: var(--ink);
}
.skyview-overlay[hidden] { display: none; }

.skyview-overlay .sky-exit {
  position: absolute;
  top: 22px; left: 24px;
  background: none; border: none; border-radius: 0; padding: 4px 2px;
  color: var(--gold);
  font-family: var(--grot);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  cursor: pointer;
  pointer-events: auto;
  transition: color 0.2s;
}
.skyview-overlay .sky-exit:hover,
.skyview-overlay .sky-exit:focus-visible { color: #fff2cf; outline: none; }

/* The place name — the sky's masthead. Display weight, tight, the same voice as the
   piece's own title. */
.skyview-overlay .sky-title {
  position: absolute;
  top: 20px; left: 0; right: 0;
  text-align: center;
  font-family: var(--grot);
  font-size: 22px;
  font-weight: 600;
  color: var(--gold);
  letter-spacing: -0.024em;
  line-height: 1.05;
}
.skyview-overlay .sky-caption {
  position: absolute;
  bottom: 30px; left: 50%; transform: translateX(-50%);
  width: min(640px, 84vw);
  text-align: center;
  font-family: var(--grot);
  font-size: 14px;
  line-height: 1.62;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.skyview-overlay .sky-caption b { color: var(--gold); font-weight: 600; }
.skyview-overlay .sky-hint {
  position: absolute;
  bottom: 12px; left: 50%; transform: translateX(-50%);
  font-family: var(--grot);
  font-size: 13px;
  font-weight: 600;
  color: var(--ink-faint);
  letter-spacing: 0.18em;
  text-transform: uppercase;
}

/* Cardinal marks (N/E/S/W) — quiet, positioned each frame at the horizon. */
.skyview-overlay .sky-card {
  position: absolute;
  transform: translate(-50%, -50%);
  font-family: var(--grot);
  font-size: 14px;
  font-weight: 700;
  color: var(--ink-soft);
  letter-spacing: 0.14em;
}

/* Year labels hanging where each eclipse's Sun stood. Above-horizon = crisp muted gold;
   below-horizon = greyed and dimmer (honest — they were below the ground). */
/* A YEAR is a number: the number face, tabular, tracked in tight so Mono's wide set does
   not make these hang wider than the Suns they label. */
.skyview-overlay .sky-year {
  position: absolute;
  transform: translate(-50%, -160%);
  font-family: var(--mono);
  font-size: 13.5px;
  font-weight: 700;
  color: var(--gold);
  letter-spacing: -0.04em;
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
  white-space: nowrap;
  cursor: pointer;
  pointer-events: auto;
  touch-action: none;   /* a tap here selects (see src/skyview.js); never a scroll gesture */
  transition: color 0.15s;
}
.skyview-overlay .sky-year:hover { color: #fff2cf; }
.skyview-overlay .sky-year.is-below {
  color: var(--ink-faint);
  font-weight: 400;
  opacity: 0.72;
}
.skyview-overlay .sky-tip {
  position: absolute;
  transform: translate(-50%, 12px);
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 0;
  font-family: var(--grot);
  font-size: 13px;
  font-weight: 500;
  color: var(--ink);
  text-align: center;
  pointer-events: none;
  font-variant-numeric: tabular-nums;
}
/* The bold line in a sky tip is the eclipse's DATE -> the number face. */
.skyview-overlay .sky-tip b {
  color: var(--gold);
  font-family: var(--mono);
  font-weight: 700;
  font-size: 13.5px;
  letter-spacing: -0.03em;
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}
.skyview-overlay .sky-tip span { color: var(--ink-soft); }

/* ---------- City Sky, Mapbox mode (src/citysky_mapbox.js) — the PRIMARY City Sky, real 3D
   buildings + eclipse glyphs at true sky positions, sepia-graded. src/skyview.js's own
   .skyview-overlay (above) becomes the OFFLINE FALLBACK, used only when Mapbox fails to load.
   Same fixed full-frame overlay pattern as .skyview-overlay, same z-index band (40) and the
   SAME body.sky-open rule (styles.css ~1824) hides the rest of the chrome — this overlay does
   not need its own visibility toggle for that. The 45 that used to sit above it was the
   [Globe | City Sky] tab bar, kept lit so it could be the way back; the bar is gone and this
   screen carries its own way out (.cmb-leave), so nothing needs to float over it any more. */
.citysky-mapbox-overlay {
  position: fixed;
  inset: 0;
  z-index: 40;
  /* ITEM 1, ROUND 3 (29 Jul 2026, director: "можно сделать кастомный мод в мапбокс, где небо
     будет полностью прозрачным, тогда поставим своё, и стык не будет выглядеть плохо"). Rounds 1
     and 2 both tried to make Mapbox's OWN fog agree with this CSS backdrop at whatever line
     divided them (a solid fill matching fog's own flattened zenith tone, in round 2) — and it
     still read as a seam, structurally: fog and a CSS gradient trace different curves even at
     matching endpoints. This round removes the OTHER side of the join instead: citysky_mapbox.js's
     killMapboxAtmosphere() now calls map.setFog() with no argument (a real "remove fog" API,
     mapbox-gl-js 3.26.0) AND forces every background-type style layer fully transparent
     (background-opacity 0) on 'style.load' — confirmed independent of the 3D buildings/landmark
     models, which live on an entirely separate render pass (see that function's own header for the
     evidence). Mapbox now draws NO sky pixels anywhere in the frame, at rest or looking up, so
     this CSS gradient is back to being the WHOLE sky (both stops, horizon low -> zenith high) —
     there is no second system left to disagree with it, which is the actual fix for the seam two
     rounds of colour-matching could not reach. Still travels day -> night with the eclipse
     (updateSkyBackdrop() in citysky_mapbox.js rewrites both custom properties every frame
     nightAmount changes). */
  /* Fallback stops match SKY_DAY_LOW/HIGH in citysky_mapbox.js (29 Jul 2026 lighten pass) — only
     used for the first paint before JS sets the real custom properties. */
  background: linear-gradient(to top, var(--cs-sky-low, #faf3e0), var(--cs-sky-high, #d8cdb1));
  overflow: hidden;
}
.citysky-mapbox-overlay[hidden] { display: none; }
/* Already standing in a city's sky? Then "Step into <city>'s sky" is an invitation into the mode
   you are in — picking another city now just walks you there (see enterCitySky in src/main.js).
   "мы блин и так в нем, должен сразу грузиться новый город". */
body.sky-open.citysky-mapbox .sky-enter { display: none; }
/* ITEM 7a REWRITE (28 Jul 2026, director: "выбираю город... перекидывает на глобус, потом
   какую-то показывает картинку... надо сразу ставить город"). The overlay ITSELF is now always
   opaque the moment it is mounted (no more overlay-wide opacity:0/fade) — it can never again go
   see-through and reveal the still-rendering globe behind it during a city switch. Only the MAP
   CONTENT below (.cmb-map/.cmb-skylayer/.cmb-scrubber) and the .cmb-loading glyph cross-fade
   against EACH OTHER, gated by .cmb-ready — see citysky_mapbox.js's scheduleReveal()/
   ensureDom()/destroy(). pointer-events on the map content stay off until ready so a stray tap
   during the (usually sub-second) wait can't hit anything. */
.citysky-mapbox-overlay .cmb-map,
.citysky-mapbox-overlay .cmb-skylayer,
.citysky-mapbox-overlay .cmb-scrubber {
  opacity: 0;
  pointer-events: none;
  transition: opacity 380ms ease;
}
.citysky-mapbox-overlay.cmb-ready .cmb-map,
.citysky-mapbox-overlay.cmb-ready .cmb-skylayer,
.citysky-mapbox-overlay.cmb-ready .cmb-scrubber {
  opacity: 1;
  pointer-events: auto;
}
/* .cmb-skylayer's own pointer-events stay off even when ready — individual .ecl-marker children
   opt back in (see below); the layer itself must never block the drag-to-look surface under it. */
.citysky-mapbox-overlay.cmb-ready .cmb-skylayer { pointer-events: none; }
/* touch-action: none — the look-around drag is our OWN pointer handler (citysky_mapbox.js's
   onPointerDown/Move/Up), not a native scroll/pan/zoom gesture; without this a touch drag here
   fights the browser's default touch handling (page scroll, pinch-zoom) on mobile. */
.citysky-mapbox-overlay .cmb-map { position: absolute; inset: 0; cursor: grab; touch-action: none; }
.citysky-mapbox-overlay .cmb-map.dragging { cursor: grabbing; }
.citysky-mapbox-overlay .cmb-skylayer {
  position: absolute; inset: 0; z-index: 3;
}
/* ITEM 7a — the quiet loading state: a small Sun with a Moon sliding across and back, in the
   piece's own sepia palette, no spinner chrome. Visible by default (the base, pre-.cmb-ready
   state); fades out once the new city's frame is actually ready, at the same time the map content
   above fades in — so the two are always exact opposites, never both visible or both hidden. */
.citysky-mapbox-overlay .cmb-loading {
  position: absolute; inset: 0; z-index: 8;
  display: flex; align-items: center; justify-content: center;
  background: var(--ground);
  opacity: 1;
  transition: opacity 320ms ease;
}
.citysky-mapbox-overlay.cmb-ready .cmb-loading { opacity: 0; pointer-events: none; }
.citysky-mapbox-overlay .cmb-loading-sun {
  position: relative;
  width: 76px; height: 76px;
  border-radius: 50%;
  background: radial-gradient(circle, #fffaf0 0%, #e7d9b8 62%, rgba(231,217,184,0) 100%);
  overflow: hidden;
}
.citysky-mapbox-overlay .cmb-loading-moon {
  position: absolute;
  top: 0; left: -76px;
  width: 76px; height: 76px;
  border-radius: 50%;
  background: var(--ink);
  animation: cmb-moon-slide 2.8s ease-in-out infinite alternate;
}
@keyframes cmb-moon-slide {
  0% { transform: translateX(0); }
  100% { transform: translateX(152px); }
}
@media (prefers-reduced-motion: reduce) {
  .citysky-mapbox-overlay .cmb-loading-moon { animation-duration: 5.6s; }
}
.citysky-mapbox-overlay .ecl-marker {
  position: absolute; top: 0; left: 0; transform: translate(-999px,-999px);
  pointer-events: auto; cursor: pointer; text-align: center; user-select: none;
  touch-action: none;   /* a tap here is a selection, never a scroll/double-tap-zoom gesture */
}
.citysky-mapbox-overlay .ecl-glyph-wrap { position: relative; width: 64px; height: 64px; margin: 0 auto; }
.citysky-mapbox-overlay .ecl-glyph-wrap canvas.glyph { display: block; }
.citysky-mapbox-overlay .ecl-glyph-wrap canvas.corona {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  pointer-events: none; opacity: 0; transition: opacity 260ms ease;
}
/* ITEM 5 (28 Jul 2026): the corona canvas is TOTAL/HYBRID-only (see citysky_mapbox.js's
   buildMarkersForItems — an annular/partial marker never even gets a corona canvas appended) and
   its opacity is driven imperatively, frame by frame, from the Moon-crossing passage itself
   (paintAt() in that file) rather than by a CSS hover class, so there is no ".corona-active" rule
   here any more — a stale, unused class here previously implied every eclipse type could bloom a
   corona on hover, which was itself part of the "annular turns into a total" bug. */
/* ITEM 4 (29 Jul 2026) — NO TEXT-SHADOWS. The house rule, stated plainly after "заебало черная
   тень на всех подписях", and every label in City Sky was breaking it. The halos were not
   decoration though: measured against the real composited backdrop, the ink ALONE scored 1.2:1 to
   2.7:1 here, so simply deleting them would have brought back the complaint they were added for
   ("illegible over Mapbox's own map"). What replaces the halo is the RIGHT INK for whatever is
   actually behind that particular label — this piece's own deep sepia over the sunlit city and the
   bright end of the day sky, its own cream over the dark zenith and over a city at totality —
   chosen per label, per frame, from the measured backdrop. See citysky_mapbox.js's inkAt()/
   sampleCityLum() for how each value is arrived at; the --yr-ink / --cs-ink-* custom properties
   below are where it lands. The fallbacks are the daylight answer, so the labels are legible even
   in the first frame before any measurement exists. */
.citysky-mapbox-overlay .ecl-marker .yr {
  margin-top: 2px; font-family: var(--grot); font-size: 13px; font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--yr-ink, rgb(40,30,20));
  /* Long enough to ride out a Moon passage's own darkening without flickering, short enough that a
     marker crossing the horizon is not caught wearing the wrong ink. */
  transition: color 220ms ease;
  white-space: nowrap;
}
/* Below the horizon is quieter, but it is the SAME ink — a second colour here would be a second
   thing to keep legible. Dimmed by opacity only, and not so far that it stops reading. */
/* 0.85, not lower: measured against a sunlit building face behind it, 0.72 dropped the worst patch
   to 3.2:1 — under the floor the ink switch is there to hold. The glyph itself already carries most
   of the "this one was under the horizon" signal (drawGlyphCanvas paints those in cold grey), so
   the label does not have to fade far to say it. */
.citysky-mapbox-overlay .ecl-marker.below .yr { opacity: 0.85; }
/* Mapbox's required attribution/logo control — must stay on screen; lift above the sky layer. */
.citysky-mapbox-overlay .mapboxgl-ctrl-bottom-left,
.citysky-mapbox-overlay .mapboxgl-ctrl-bottom-right { z-index: 6; }

/* ---------- ITEM 3 (28 Jul 2026, director: "почему пропал весь обвес? почему пропал выбор
   города?" / "на десктопе слева можно спокойно оставлять карточку города и выбор города") ----------
   body.sky-open normally fades .panel (the shared left column: the city picker + the city card)
   to nothing for EVERY sky mode (styles.css ~1828) — correct for skyview.js's own full-chrome
   terrain fallback, wrong for Mapbox, which the director wants to keep the column showing
   alongside. 'citysky-mapbox' is a SEPARATE body class, added/removed only for the Mapbox mode
   (src/main.js's enterCitySky/exitCitySky) — so this override cannot affect the fallback. Three
   classes beats the plain two-class `body.sky-open .panel` rule on specificity alone, so this
   does not depend on cascade/source order. DESKTOP ONLY: on a phone there is no room beside a
   first-person sky for a 300px column, so this only fires above the phone breakpoint (620px,
   the same cutoff the rest of the sheet's "(B) phone" rules already use) — .panel's own
   positioning at every width is untouched, only its VISIBILITY here. z-index lifted to sit above
   the Mapbox overlay itself (40); .panel's resting z-index (12) only ever had to clear the
   WebGL globe canvas, not a full-screen fixed overlay. */
@media (min-width: 621px) {
  body.sky-open.citysky-mapbox .panel {
    opacity: 1;
    visibility: visible;
    z-index: 45;
  }
}

/* ---------- ITEMS 1/2 (28 Jul 2026, director: "где внизу таймлайн в сити скай... должен быть
   таймлайн внизу, к нему привязано") ----------
   City Sky's OWN bottom year-range scrubber ("the timeline", in the director's own words for
   this control) — built by the SAME src/scrubber.js component the globe's #scrubber uses (see
   src/citysky_mapbox.js's applyYearFilter()), a separate instance over just this city's own item
   set. The root class is .cmb-scrubber, NOT .scrubber, on purpose: body.sky-open's blanket
   `.scrubber { opacity:0 }` rule (~1828) exists to hide the GLOBE's #scrubber while ANY sky mode
   is open — reusing that exact class here would have that rule catch this one too.
   ITEM 3 REPAINT (29 Jul 2026, director: "таймлайн... его подписи обрезаются краем кадра, и он
   пересекается с подписями самого города"). Two real, separate faults, both fixed here:
   1. "cropped... cut off by the frame edge" — this box was copied verbatim from the GLOBE's own
      #scrubber (72px, anchored flush to bottom:0), which sits over a plain page background with
      nothing below the true viewport edge to worry about. Over a REAL device this same flush
      anchor can sit under the browser's own bottom chrome/safe area, clipping the last pixels of
      its lowest text. FIX: taller box (88px, was 72) and `padding-bottom: env(safe-area-inset-
      bottom)` so the box's true bottom edge — and everything drawn near it — never coincides with
      the literal edge of the screen.
   2. "collides with the city's own labels" — this control's text (.scrub-readout/.scrub-tick-
      label) was copied with the GLOBE's own plain `color: var(--ink)`, meant for flat page
      background. Over Mapbox's own photographed city (real street/place labels, building facades,
      light and dark in the same frame) that plain ink has no guaranteed contrast — it visually
      merges with whatever Mapbox draws directly behind it. FIX, ROUND 1 (SUPERSEDED): a soft dark
      text-shadow halo behind the control's own text.
      FIX, ROUND 2 (29 Jul 2026, item 4): the halo is GONE — it broke the house rule outright, and
      it was needed only because the ink was the wrong one. Measured over the sunlit ground this
      control actually sits on, its cream text scored 1.2:1. The whole control (text AND marks) now
      inherits --cs-ink-scrub, the ink citysky_mapbox.js picks from the city's own MEASURED
      brightness right there — the piece's deep sepia over a daylit city, its cream once the city
      goes dark at totality. Gold stays gold: it is the piece's accent and it reads against both. */
.citysky-mapbox-overlay .cmb-scrubber {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  height: 88px;
  color: var(--cs-ink-scrub, rgb(40,30,20));
  transition: color 220ms ease;
  padding: 0 clamp(24px, 5vw, 80px) env(safe-area-inset-bottom, 0px);
  z-index: 9;
  pointer-events: none;
  user-select: none;
  -webkit-user-select: none;
}
.citysky-mapbox-overlay .cmb-scrubber .scrub-readout {
  position: absolute;
  top: 8px; left: 0; right: 0;
  text-align: center;
  font-family: var(--mono);
  font-size: 15px;
  font-weight: 700;
  letter-spacing: -0.01em;
  color: inherit;   /* --cs-ink-scrub, see the section header above */
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}
.citysky-mapbox-overlay .cmb-scrubber .scrub-track {
  position: absolute;
  left: clamp(24px, 5vw, 80px);
  right: clamp(24px, 5vw, 80px);
  top: 40px;
  height: 22px;
  pointer-events: auto;
  touch-action: none;
}
.citysky-mapbox-overlay .cmb-scrubber .scrub-line {
  position: absolute;
  left: 0; right: 0; top: 0;
  height: 2px;
  /* The MARKS follow the same rule as the text now: the ink is chosen for the backdrop, so the
     line simply takes the control's own colour and needs no drop-shadow to survive on it. (A pale
     fixed cream plus a black drop-shadow was the same halo trick as the text-shadows, one element
     down, and just as invisible over a sunlit street without it.) */
  background: currentColor;
  opacity: 0.55;
}
.citysky-mapbox-overlay .cmb-scrubber .scrub-sel {
  position: absolute;
  top: -1px; height: 4px;
  background: var(--gold);
  opacity: 1;
}
.citysky-mapbox-overlay .cmb-scrubber .scrub-ticks {
  position: absolute;
  left: 0; right: 0; top: 0;
  height: 22px;
}
.citysky-mapbox-overlay .cmb-scrubber .scrub-tick {
  position: absolute;
  top: 0;
  width: 1px; height: 6px;
  background: currentColor;   /* as .scrub-line above: the ink, not a halo */
  opacity: 0.7;
  transform: translateX(-0.5px);
}
.citysky-mapbox-overlay .cmb-scrubber .scrub-tick-label {
  position: absolute;
  top: 8px; left: 50%;
  transform: translateX(-50%);
  font-family: var(--mono);
  font-size: 13px;
  /* 500, not the globe's 400: this one is read over a photographed city rather than a flat page,
     and weight is the other half of legibility now that no halo is doing the work. */
  font-weight: 500;
  letter-spacing: -0.03em;
  color: inherit;      /* --cs-ink-scrub, see the section header above */
  opacity: 0.85;       /* quieter than the readout, without changing what colour it is */
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}
.citysky-mapbox-overlay .cmb-scrubber .scrub-ticks .scrub-tick:first-child .scrub-tick-label {
  left: 0; transform: none;
}
.citysky-mapbox-overlay .cmb-scrubber .scrub-ticks .scrub-tick:last-child .scrub-tick-label {
  left: auto; right: 0; transform: none;
}
.citysky-mapbox-overlay .cmb-scrubber .scrub-handle {
  position: absolute;
  top: -13px;
  width: 10px; height: 28px;
  margin-left: -5px;
  background: var(--gold);
  pointer-events: auto;
  cursor: ew-resize;
  touch-action: none;
  transition: filter 0.15s, background-color 0.15s;
}
.citysky-mapbox-overlay .cmb-scrubber .scrub-handle::before {
  content: "";
  position: absolute;
  top: -8px; bottom: -8px; left: -11px; right: -11px;
}
.citysky-mapbox-overlay .cmb-scrubber .scrub-handle:hover,
.citysky-mapbox-overlay .cmb-scrubber .scrub-handle:focus-visible,
.citysky-mapbox-overlay .cmb-scrubber .scrub-handle:active {
  background: var(--gold);
  filter: brightness(1.2);
}
@media (max-width: 620px) {
  /* Phone is exactly where a real safe-area inset is most likely — the 3-value form keeps the
     base rule's env(safe-area-inset-bottom) pad alive here instead of the 2-value shorthand
     silently zeroing it back out (top/bottom would both collapse to plain 0 otherwise). */
  .citysky-mapbox-overlay .cmb-scrubber { padding: 0 16px env(safe-area-inset-bottom, 0px); }
  .citysky-mapbox-overlay .cmb-scrubber .scrub-track { left: 16px; right: 16px; }
  .citysky-mapbox-overlay .cmb-scrubber .scrub-readout { font-size: 14px; }
  .citysky-mapbox-overlay .cmb-scrubber .scrub-tick-label { font-size: 13px; letter-spacing: -0.05em; }
  .citysky-mapbox-overlay .cmb-scrubber .scrub-ticks .scrub-tick:nth-child(2) .scrub-tick-label,
  .citysky-mapbox-overlay .cmb-scrubber .scrub-ticks .scrub-tick:nth-child(4) .scrub-tick-label,
  .citysky-mapbox-overlay .cmb-scrubber .scrub-ticks .scrub-tick:nth-child(5) .scrub-tick-label { display: none; }
}

/* ITEM 3 (29 Jul 2026, director: the "Standing near the middle of Paris — 48.85°N, 2.35°E" line
   "should not be written at all"). .cmb-standpoint is GONE — the locator below already answers
   "where am I" as an instrument; this was a redundant second answer, in the exact corner the
   locator itself now occupies. See src/citysky_mapbox.js's own note by ensureDom()/drawLocator(). */

/* ---------- ITEM 4 (29 Jul 2026, director: "ты потерял локатор в правом углу") — REPAINTED
   again the same day (director: "хочу его сделать больше и перенести в правый нижний угол — над
   таймлайном, не поверх него") ----------
   The locator itself (see src/citysky_mapbox.js's drawLocator()): a compact top-down plan — the
   city's real outline where one is published, else a bare cardinal crosshair — with the
   standpoint's true position and a gold bearing needle. BOTTOM-RIGHT now, sized up (LOC_CS 96 ->
   132, see citysky_mapbox.js), sitting ABOVE .cmb-scrubber ("the timeline") rather than over it:
   the scrubber's own box is 88px tall plus a safe-area inset (see that section's own header), so
   this stacks at bottom: 88px + that same safe-area inset + a 14px gap, never touching it at any
   viewport height. Backed by a LOC_CS*dpr px raster (see ensureDom()) but always DISPLAYED at
   exactly LOC_CS px regardless of device pixel ratio. No plate, no border, no rounded corners —
   an instrument, not a widget. Fades in with the rest of the ready content (.cmb-ready). */
.citysky-mapbox-overlay .cmb-locator {
  position: absolute;
  bottom: calc(88px + env(safe-area-inset-bottom, 0px) + 14px);
  right: 24px;
  width: 132px; height: 132px;
  z-index: 5;
  pointer-events: none;
  opacity: 0;
  transition: opacity 380ms ease;
}
.citysky-mapbox-overlay.cmb-ready .cmb-locator { opacity: 1; }
@media (max-width: 620px) {
  .citysky-mapbox-overlay .cmb-locator {
    bottom: calc(88px + env(safe-area-inset-bottom, 0px) + 10px);
    right: 16px; width: 112px; height: 112px;
  }
}

/* ---------- ITEM 3 (29 Jul 2026, director: "наверное надо дать какую-то стрелку наверх с
   подписью, что часть затмений может быть там") ----------
   The upward affordance itself (see src/citysky_mapbox.js's refreshLookUpHint()): a small upward
   mark plus a few quiet words, top-centre, shown only while there is something above the resting
   frame to point at AND the viewer has not already discovered the look-up drag this city — see
   that function's own header for exactly when hint-show is toggled. Two independent opacity
   gates, both required: .cmb-ready (the shared loading crossfade, so it can never appear over the
   loading glyph) AND .hint-show (the affordance's own condition) — see the selector below. No
   plate, no border, no rounded corners, nothing under 12px (house rule) — 13px, the same size
   this piece uses everywhere else for a quiet line of over-the-city text. */
.citysky-mapbox-overlay .cmb-lookup-hint {
  position: absolute;
  top: 20px; left: 50%;
  transform: translateX(-50%);
  z-index: 5;
  text-align: center;
  pointer-events: none;
  font-family: var(--grot);
  font-size: 13px;
  /* 600: this line sits high in the frame against the day sky's own darkest band, where the
     contrast is thinnest — see item 4's note by .ecl-marker .yr. */
  font-weight: 600;
  letter-spacing: 0.01em;
  color: var(--cs-ink-hint, rgb(40,30,20));
  opacity: 0;
  transition: opacity 420ms ease, color 220ms ease;
}
.citysky-mapbox-overlay.cmb-ready .cmb-lookup-hint.hint-show { opacity: 1; }
.citysky-mapbox-overlay .cmb-lookup-hint .arrow {
  display: block;
  font-size: 15px;
  margin-bottom: 2px;
  animation: cmb-hint-bob 1.8s ease-in-out infinite;
}
@keyframes cmb-hint-bob {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-4px); }
}
@media (prefers-reduced-motion: reduce) {
  .citysky-mapbox-overlay .cmb-lookup-hint .arrow { animation: none; }
}
@media (max-width: 620px) {
  .citysky-mapbox-overlay .cmb-lookup-hint { top: 16px; font-size: 12.5px; }
}

/* =============================================================================================
   CITY SKY ON A PHONE: ONLY SKY (2 Aug 2026)
   ---------------------------------------------------------------------------------------------
   Director, on a phone: «по дефолту там нужно только небо! карточка снизу по клику на затмение,
   а карточка города — не знаю куда, но тоже кнопкой, по дефолту — её нет.»

   The screen's argument is that you are standing in a place looking up, and everything printed
   over it is a claim on the same pixels the sky is trying to fill. So AT REST, at phone width,
   this screen carries: the sky and the city; the eclipse marks and the twelve captions that
   survive the budget; the Sun's-ceiling wash and its one centred line (both are drawings OF the
   sky, not furniture over it); the way out; and ONE control, the city's name, which summons the
   card. Nothing else.

   Measured at 390x844 standing in Paris (dev/citysky_phone_rest.mjs, which counts the union of
   every on-screen interface box rather than asserting anything): the resting furniture went from
   44,263 px² — 13.4% of the frame, the 390x88 timeline plus the sound control's two-line label
   plus the way out — to 9,962 px² (3.0%). Sky at rest: 86.6% -> 97.0%.

   (NOT LISTED, because it is not there to list: Mapbox's own attribution and logo. They exist and
   are `visibility: visible`, but they are children of .cmb-map, which this screen TRANSLATES to
   put the horizon low and the sky high — measured, that puts them at y 1034–1078 on an 844-tall
   viewport, i.e. off the bottom of the screen. That is a pre-existing condition of the look-up
   transform, not of this pass, and it predates every rule below; flagged separately.)

   THREE RULES LIVE HERE; the fourth (the timeline standing down) is in src/citysky_mapbox.js,
   with the rest of that screen's own instruments, for the reason its header gives.
   EVERYTHING BELOW IS PHONE-ONLY AND CITY-SKY-ONLY. `.citysky-mapbox` is the body class
   src/main.js adds for the Mapbox mode alone, so the terrain fallback — which draws its own full
   chrome — is untouched, and so is every width above 620px.
   ============================================================================================= */
@media (max-width: 620px) {

  /* ---- 1. THE ECLIPSE CARD ARRIVES FROM THE BOTTOM EDGE -------------------------------------
     «карточка снизу по клику на затмение». THE SAME CARD — this is `.tooltip`, built by
     src/main.js's showTooltip(rec), the one component both screens raise, and not one word of its
     content or its type rules is restated here. Only where it is put changes, and only on a phone
     inside City Sky (src/main.js adds .cmb-card-sheet exactly there).
     Measured, the old placement put a 337x242 box in the MIDDLE of a 390x844 frame — across the
     sky it was describing and frequently across the eclipse itself, which is the same complaint
     that produced click-to-dismiss a day earlier. Docked to the bottom edge it takes a band at the
     foot of the frame, the sky above it stays whole, and it matches the one physical rule this
     phone layout already has: every surface here comes up from the bottom edge.
     THE PLATE is the same two composited tokens the legend sheet and the city card use — the
     translucent --panel-bg laid over --ground, the piece's current sky colour, which src/main.js
     interpolates toward night with everything else. Opaque without inventing a colour, and it
     still inverts at totality. The card's own feathered ::before scrim is dropped: the sheet IS
     the surface now, and two plates behind one card is one too many.
     Still no border, no rounded top edge, no shadow, no text-shadow, nothing under 13px. */
  /* ---- IT DOCKS AT THE EDGE ON BOTH SCREENS (2 Aug 2026, the coherence pass) ----------------
     `bottom: var(--tapcard-bottom, 0px)` used to put the GLOBE's copy of this card on top of
     whatever furniture the bottom edge already had (the picker deck, or the collapsed city card
     once a place was chosen) while City Sky's copy sat flush at 0. Same card, same tap, two
     different edges — which is precisely the «связь между экранами» the director is naming.
     It is a MODAL surface by the law above: summoned by a tap, dismissed by a tap outside (or
     its own ×, or Escape). Modal surfaces cover. So it docks at 0 on both screens, over the rail
     and over the city card, and putting it down gives them straight back.
     THIS ALSO CLOSES A REAL BUG. --tapcard-bottom is written by src/main.js only when the GLOBE
     raises this card, and never cleared — so entering City Sky after tapping one eclipse on the
     globe left the sky's own card floating that many pixels off the bottom of the frame, on a
     screen with nothing underneath it. The variable is no longer read by anything; see the note
     to src/main.js's owner. */
  body.sky-open.citysky-mapbox .tooltip.cmb-card-sheet,
  body:not(.sky-open) .tooltip.tap-card-sheet {
    left: 0; right: 0; bottom: 0; top: auto;
    width: auto; max-width: none; min-width: 0;
    padding: 16px 16px calc(18px + env(safe-area-inset-bottom, 0px));
    background: linear-gradient(var(--panel-bg), var(--panel-bg)), var(--ground);
    /* Deep enough for the tallest card this screen can raise (a total, with its duration, path
       width, altitude, shadow speed and obscuration rows) and no deeper: past this it scrolls
       inside itself rather than eating the sky. */
    max-height: 62vh;
    overflow-y: auto;
    overscroll-behavior: contain;
    transform: none;
    /* It rises. The animation runs off the element becoming displayed (`hidden` is toggled by
       showTooltip/hideTooltip), so nothing has to time a class change in script. */
    animation: cmb-card-rise 260ms cubic-bezier(0.22, 0.61, 0.36, 1);
  }
  body.sky-open.citysky-mapbox .tooltip.cmb-card-sheet::before,
  body:not(.sky-open) .tooltip.tap-card-sheet::before { content: none; }
  /* The card is `pointer-events: none` by default — correct for a thing that follows a pointer,
     wrong for a surface a thumb will land on while reaching for the sky behind it. Taking the
     touch (and going nowhere with it) is what stops a tap meant for the card from turning into
     the first sample of a look-around drag on the map underneath. */
  body.sky-open.citysky-mapbox .tooltip.cmb-card-sheet,
  body:not(.sky-open) .tooltip.tap-card-sheet { pointer-events: auto; }

  /* ---- THE GLOBE'S OWN TAP CARD, DOCKED (director, 2 Aug 2026) -------------------------------
     «плашка на глобусе при тапе на затмение тоже выглядит странно, я бы её вниз прибивал, и делал
     крестик в углу (плюс сброс тапом в любое место).»
     THE SAME RULE, NOT A SECOND ONE. Every declaration that docks this card is the block above —
     the globe's selector simply joined it. This phone layout has one physical rule and every
     surface obeys it (the legend sheet, the city card, the picker deck and City Sky's own eclipse
     card all arrive from the bottom edge); the globe's hover card was the last thing still popping
     up under the finger, which is exactly why it "выглядит странно". Now there is one bottom-sheet
     treatment for one element and two screens select it.
     NOTHING IS DIFFERENT ANY MORE, INCLUDING WHERE THE BOTTOM IS (2 Aug 2026, the coherence
     pass). This card used to dock at `--tapcard-bottom` on the globe and at 0 in the sky — the
     right answer while the globe's card was a persistent surface stacking on the furniture below
     it. It is not one: it is summoned by a tap and dismissed by a tap, which makes it MODAL under
     the law at the head of this file, and modal surfaces cover. So both screens dock it at 0, and
     src/personal.js's own bridge puts the city sheet away underneath it (see "ONE SHEET AT A
     TIME, ON THE GLOBE" there) rather than leaving two plates stacked with a stripe between them.
     `--tapcard-bottom` is still WRITTEN by src/main.js and is no longer read by anything; see the
     note to that file's owner. */
  /* Room for the close control in the corner, so a long date never runs under it. The selector
     matches the dock rule's own weight and sits after it, which is what lets a longhand override
     the shorthand `padding` that rule sets. */
  body:not(.sky-open) .tooltip.tap-card-sheet { padding-right: 56px; }
  /* THE CLOSE CONTROL. A real <button>, so Tab and Enter already work and nothing about focus has
     to be invented. No border, no rounding, no shadow (house rules), and no glyph under 13px —
     this is 22px, sized as a 44px touch target the way the rest of the phone furniture is. It is
     ink on the sheet, nothing else. */
  .tt-close {
    position: absolute;
    top: 6px;
    right: 6px;
    width: 44px;
    height: 44px;
    padding: 0;
    border: 0;
    border-radius: 0;
    background: none;
    box-shadow: none;
    color: var(--ink-soft);
    font: 22px/1 var(--mono);
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
  }
  .tt-close:hover, .tt-close:focus-visible { color: var(--ink); }

  /* ---- 2. THE CITY'S CARD, SUMMONED ---------------------------------------------------------
     «а карточка города — не знаю куда, но тоже кнопкой, по дефолту — её нет.»
     ONE COMPONENT, NOT TWO. The card is already a bottom sheet at this width with a collapsed and
     an expanded detent (src/personal.js mounts it, mountSheet in src/timeline.js drives it); the
     only reason it is not on screen in City Sky is `body.sky-open .panel`, which fades the whole
     left column for EVERY sky mode. Nothing is forked and nothing is rebuilt — this un-fades the
     sheet that is already mounted, and only while the screen's own control says so.
     Four classes and an element beats that two-class rule on specificity alone, so this does not
     depend on source order. z-index 45 clears the Mapbox overlay's own 40, the same number the
     desktop override already uses.
     THE PICKER STAYS DOWN. The control was asked to summon the CARD, not the column: a text field
     floating over a first-person sky is exactly the furniture this pass is removing, and City Sky
     already carries its own way out to the screen where the picker lives. */
  body.sky-open.citysky-mapbox.citysky-card-open .panel {
    opacity: 1;
    visibility: visible;
    z-index: 45;
  }
  body.sky-open.citysky-mapbox.citysky-card-open .panel-picker { display: none; }
  /* .panel is `pointer-events: none` and each child opts back in; the sheet has to opt in again
     here because the rule that does that on the Globe screen is keyed on .panel-body's children,
     not on .panel-body itself. */
  body.sky-open.citysky-mapbox.citysky-card-open .panel-body { pointer-events: auto; }

  /* ---- 2b. THE SCRUBBER RIDES THE SHEET, NOT BEHIND IT (3 Aug 2026) -------------------------
     Director: "не забыть снизу ползунки времени" — the year sliders must still be reachable
     with the city's card up. src/citysky_mapbox.js's applyScrubberVisibility() already HIDES
     .cmb-scrubber outright the moment the card opens (inline opacity/visibility/pointer-events),
     and correctly so, by its own account: the card sheet sits at z-index 45, above this control's
     overlay at 40, so an UNhidden scrubber would have been a live full-width slider standing
     behind an opaque plate — worse than hidden, because it would still be in the tab order. That
     same comment names the fix and says it cannot be made from that file: the sheet belongs to
     src/personal.js/src/timeline.js, which citysky_mapbox.js does not own. This is that fix, from
     the file that does.
     RAISED ABOVE THE SHEET, not moved into it: reparenting a live src/scrubber.js instance into
     the card's own DOM would make the card own a control it does not otherwise know exists, and
     the card sheet's own height is not fixed (it is the collapsed peek at rest, the full detent
     dragged open) — a scrubber actually built INTO that flow would jump every time the sheet's
     height changed under it. Raising it to clear the sheet's z-index instead keeps it exactly
     where the rest of this screen already expects it (flush to the true bottom edge, the same
     `bottom: 0` position it holds with no card up) and reads as "the timeline stacked on the
     sheet's own foot", which is where the brief asked for it, without a second copy of the sheet's
     own layout math.
     `!important` is deliberate and scoped tightly (this exact four-class-plus-descendant state) —
     the one place in this stylesheet reaching past properties a DIFFERENT file sets inline, which
     an ordinary specificity rule cannot outrank. */
  body.sky-open.citysky-mapbox.citysky-card-open .cmb-scrubber {
    opacity: 1 !important;
    visibility: visible !important;
    pointer-events: auto !important;
    z-index: 46;                 /* clears the sheet's own 45 (see ITEM 2's rule, just above) */
  }
  /* THE ATTRIBUTION WAS NEVER HIDDEN BY JAVASCRIPT — it is simply PAINTED UNDER the sheet, because
     its `z-index: 6` (far above, "Mapbox's required attribution/logo control") is LOCAL to
     .citysky-mapbox-overlay's own stacking context (that rule's own z-index:40 on the overlay),
     and a number local to one context cannot out-rank a sibling context's 45. Same fix, same
     reason, no !important needed here (nothing sets these inline): a plain z-index that clears 45
     inside this state alone. Losing the licence-required mark under a UI plate would be the kind
     of "not reached into" gap this pass exists to close, not a cosmetic one. */
  body.sky-open.citysky-mapbox.citysky-card-open .mapboxgl-ctrl-bottom-left,
  body.sky-open.citysky-mapbox.citysky-card-open .mapboxgl-ctrl-bottom-right {
    z-index: 46;
  }

  /* ---- 3. THE SOUND CONTROL KEEPS ITS REACH AND LOSES ITS LABEL -----------------------------
     It may NOT simply go: «кнопку выключения звука надо добавить, причем где-то уже на заставке»
     (31 Jul) means a viewer must be able to kill the music from wherever they are standing, and
     standing inside a sky is a where. Its own note in index.html puts it plainly — present and
     hittable on every screen — and that is not being regressed.
     What it does not need HERE is 128x48 px of two-line label. «Music on / starts on touch» was
     written for the globe screen, where this control sits at the foot of the legend's rail among
     other words; over a first-person sky it is the second-largest piece of type in the frame and
     it is talking about the interface. So on this screen it is the MARK alone — the conventional
     speaker, struck through when off, the same drawing at the same size, which is how every media
     control states this and needs no sentence to do it. The words are not deleted, they are
     UNPRINTED: the button keeps its aria-label and aria-pressed, so a screen reader is told
     exactly what it was told before, and the label comes straight back on leaving the sky.
     44px of touch target, made of padding, as everywhere else. */
  /* ...AND IT DOES NOT MOVE WHILE IT LOSES THEM (2 Aug 2026, the coherence pass). This rule
     used to re-place the control at `top: 14px` with an 11px pad all round, which put the
     speaker at x 349–371, y 25–47 — against x 360–382, y 67–89 on the globe. Same control, same
     corner, two positions, and the only screen it moved on was the one where it had also lost
     its label, so there was nothing left to recognise it by. The numbers below are chosen to
     land the MARK on the globe's own rectangle to the pixel: no horizontal padding (the mark
     itself takes the 8px right margin, as it does on the globe now that the row is reversed),
     and `top: 56px` = the globe's mark top of 67 minus this rule's own 11px of vertical pad.
     The 44px target survives as a transparent ::before rather than as padding, which is the
     same "invisible wider grab target" the year scrubber's own handle already uses — it grows
     the box a thumb answers to without moving the ink inside it. */
  body.sky-open.citysky-mapbox .eclipse-sound {
    top: 56px; right: 8px;
    padding: 11px 0;
    min-height: 44px; min-width: 0;
    gap: 0;
  }
  body.sky-open.citysky-mapbox .eclipse-sound::before {
    content: "";
    position: absolute;
    top: 0; bottom: 0; left: -11px; right: -11px;
  }
  body.sky-open.citysky-mapbox .eclipse-sound .es-text { display: none; }
}

/* Top level, because @keyframes cannot be nested in the phone media query that uses it. */
@keyframes cmb-card-rise {
  from { transform: translateY(100%); opacity: 0; }
  to   { transform: translateY(0); opacity: 1; }
}
/* The card does not slide for a reader who asked for less motion — it is simply there, which is
   the same dispensation the two draggable sheets already take (.sheet-anim, far above).
   THE SELECTOR IS THE FULL ONE ON PURPOSE. `.tooltip.cmb-card-sheet` alone is two classes; the
   rule that sets the animation is `body.sky-open.citysky-mapbox .tooltip.cmb-card-sheet`, four —
   so the short form loses on specificity and a reduced-motion reader would have got the slide
   anyway. Being later in the file does not help; only specificity decides between two rules that
   both match. */
@media (prefers-reduced-motion: reduce) {
  body.sky-open.citysky-mapbox .tooltip.cmb-card-sheet,
  body:not(.sky-open) .tooltip.tap-card-sheet { animation: none; }
}
