/* ============================================================
   Ari & Dot — table of contents + deep-dive mini-book shell.

   Styles for `app/deep-dive.js`. Loaded by any main page that opts in; adds
   nothing to a page that does not.

   WHY EVERYTHING IS NAMESPACED `dd-` / `toc-`.

   The twenty story pages are self-contained documents with their own inline
   <style>, written at different times, and between them they already use
   `.close`, `.card`, `.chip`, `.facts`, `.modal`, `.panel` and `.tools`. A
   shared stylesheet that claimed any of those names would silently restyle the
   page underneath it — the page would still work, and would look wrong, which
   is the hardest kind of breakage to attribute. So every selector in this file
   is prefixed, and none of them is an element selector.

   WHY THE OVERLAY IS `position: fixed` AND SIZED BY SCRIPT.

   Each page draws a fixed 3:2 stage, letterboxed inside the viewport:

       width: min(100vw, 150vh, <cap>);  aspect-ratio: 3/2;

   ...and the cap is not the same on every page (1400px on page 1, 1536px on
   pages 7 and 9). Reproducing that arithmetic here would mean a second copy of
   a formula that is already inconsistent, and getting a fixed-aspect stage
   wrong on one axis crops the art without any error. So the script measures the
   real `#stage` rectangle and writes it into the four custom properties below.
   The overlay then covers the stage exactly, on every page, at every viewport,
   with no arithmetic of its own.

   TAP TARGETS. Nothing interactive in this file is under 48px on its short
   side. The book's own page-navigator runs 32px and is deliberately left alone;
   this is new UI, and new UI gets the full target.
   ============================================================ */

.dd-root {
  --dd-navy: #06152d;
  --dd-panel: #0b1f3d;
  --dd-line: #71d6e966;
  --dd-blue: #8de8ff;
  --dd-gold: #ffe27d;
  --dd-cream: #fff8df;
  --dd-ink: #f3f7ff;

  /* Written by deep-dive.js from the measured #stage rectangle. The fallbacks
     cover a page with no stage at all — the overlay then fills the viewport,
     which is ugly rather than broken. */
  --dd-left: 0px;
  --dd-top: 0px;
  --dd-w: 100vw;
  --dd-h: 100vh;
}

/* `hidden` has to win here. Every interactive thing in this file is given an
   explicit `display`, and an author `display` beats the user agent's
   `[hidden]{display:none}` — so a hidden Back button or a hidden footer stays on
   screen, doing nothing, looking like a control. Scoped to this overlay so it
   cannot change how a page hides its own things. */
.dd-root [hidden] { display: none !important; }

.dd-scrim {
  position: fixed;
  inset: 0;
  z-index: 900;
  background: rgba(3, 7, 18, .72);
  backdrop-filter: blur(3px);
}
.dd-scrim[hidden] { display: none; }

.dd-panel {
  position: fixed;
  left: var(--dd-left);
  top: var(--dd-top);
  width: var(--dd-w);
  height: var(--dd-h);
  z-index: 901;
  display: grid;
  grid-template-rows: auto 1fr auto;
  overflow: hidden;
  border-radius: 14px;
  background:
    radial-gradient(120% 90% at 78% 14%, #17325c 0%, #0d1f3e 46%, #071429 78%, #04101f 100%);
  color: var(--dd-ink);
  font-family: ui-rounded, "Avenir Next", "Segoe UI", system-ui, sans-serif;
  box-shadow: 0 24px 80px rgba(0, 0, 0, .6);
}
.dd-panel[hidden] { display: none; }

/* ---- Top bar: Back on the left, Close on the right (§4) ----
   They are different controls with different jobs, so they are given different
   shapes as well as different corners: Back is an outlined arrow that moves
   inside the mini-book, Close is a filled gold pill that leaves it. A child who
   learns one does not have to relearn the other. */
.dd-bar {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  border-bottom: 1px solid var(--dd-line);
  background: rgba(4, 12, 26, .6);
}
/* Placed explicitly, so that hiding Back for the contents overlay leaves an
   empty first column and the title stays centred, instead of every item
   sliding one column left. */
.dd-bar > .dd-btn-back  { grid-column: 1; }
.dd-bar > .dd-head      { grid-column: 2; }
.dd-bar > .dd-btn-close { grid-column: 3; }

.dd-btn {
  min-height: 48px;
  min-width: 48px;
  padding: 0 16px;
  border-radius: 12px;
  border: 2px solid var(--dd-line);
  background: #17496b;
  color: #fff;
  font: inherit;
  font-weight: 800;
  font-size: clamp(13px, 1.6vw, 17px);
  cursor: pointer;
  display: inline-grid;
  grid-auto-flow: column;
  align-items: center;
  gap: 6px;
}
.dd-btn:hover { background: #24659a; }
.dd-btn:focus-visible { outline: 3px solid var(--dd-blue); outline-offset: 3px; }

/* Back on the deep dive's FIRST page is dimmed and inert, not silently
   re-pointed at Close. Deliberate, and it copies the book's own page navigator,
   where Previous on page 1 sits in place greyed out rather than disappearing or
   doing something else. The target keeps its full 48px: a disabled control that
   also shrinks moves every neighbour when it re-enables. */
.dd-btn[aria-disabled="true"] { opacity: .34; pointer-events: none; }

.dd-btn-close {
  background: var(--dd-gold);
  border-color: var(--dd-gold);
  color: #17203a;
}
.dd-btn-close:hover { background: #ffd14f; }

.dd-head { min-width: 0; text-align: center; }
.dd-head h2 {
  margin: 0;
  font-size: clamp(15px, 2.2vw, 24px);
  line-height: 1.15;
  color: var(--dd-gold);
  overflow-wrap: anywhere;
}
.dd-head p {
  margin: 2px 0 0;
  font-size: clamp(11px, 1.4vw, 15px);
  color: var(--dd-blue);
  opacity: .9;
}

/* ---- Content area ---- */
.dd-body {
  overflow: auto;
  -webkit-overflow-scrolling: touch;
  padding: 14px 18px;
  display: grid;
  /* `safe` is the whole reason this is not plain `center`. Held upright, a
     768x1024 iPad gives this panel the page's full-height stage, and a
     start-aligned mini-book page then sits in the top third with two thirds of
     black under it. Centred, it reads as a page. `safe` is what stops that
     centring from cutting the top off a page whose content is taller than the
     panel — it falls back to start exactly when it overflows, which is the one
     case where centring would put content above the scroll origin, out of
     reach. The contents overlay keeps `start` (see .toc-body): a list of
     twenty cards always overflows and should begin at the top. */
  align-content: safe center;
  gap: 12px;
}
.dd-copy {
  margin: 0;
  font-size: clamp(13px, 1.7vw, 19px);
  line-height: 1.45;
  max-width: 62ch;
}
.dd-copy-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: 12px;
}
.dd-speak {
  min-height: 48px;
  padding: 8px 15px;
  border-radius: 999px;
  border: 2px solid var(--dd-gold);
  background: #173f6c;
  color: var(--dd-cream);
  font: inherit;
  font-size: clamp(12px, 1.5vw, 16px);
  font-weight: 900;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  white-space: nowrap;
  cursor: pointer;
}
.dd-speak:hover { background: #24659a; }
.dd-speak:focus-visible { outline: 3px solid var(--dd-blue); outline-offset: 3px; }

@media (max-width: 560px) {
  .dd-copy-row { grid-template-columns: 1fr; }
  .dd-speak { justify-self: start; }
}

/* The hero picture on a single-focus or compare page.
   `max-height` is in `dvh` against the overlay's own measured height rather
   than a fixed pixel cap: this panel is as tall as the page's stage, which on
   a 768x1024 iPad is a different shape from the same page in landscape, and a
   picture sized for one crowds the words out of the other. `object-fit:
   contain` because these are whole moons — cropping one is cropping the
   subject. */
.dd-hero {
  display: grid;
  place-items: center;
  border-radius: 16px;
  border: 1px solid var(--dd-line);
  background: radial-gradient(85% 110% at 50% 40%, #16325a 0%, #0a1b34 62%, #06111f 100%);
  padding: 10px;
  overflow: hidden;
}
.dd-hero img {
  max-width: 100%;
  max-height: min(38vh, calc(var(--dd-h) * .34));
  object-fit: contain;
}

/* HotspotScene (§10) — a scene with a few large, clearly labelled targets.
   The targets are drawn as soft discs on the panel; Phase 1B gave both
   overviews a picture, and the discs sit on top of it unchanged. */
.dd-scene {
  position: relative;
  border-radius: 16px;
  border: 1px solid var(--dd-line);
  background:
    radial-gradient(90% 120% at 20% 10%, #1d3f6d 0%, #0b1c36 60%, #071327 100%);
  padding: 14px;
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  justify-content: center;
  align-items: center;
  min-height: 120px;
}
/* The overview's picture. It is the scene's backdrop, so it is taken out of
   flow and the labelled discs lay out over it exactly as they did without one.
   Dimmed, because a disc has to stay readable against whatever part of a photo
   happens to fall behind it. */
.dd-scene-art {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: 16px;
  opacity: .62;
  pointer-events: none;
}
.dd-scene { min-height: min(34vh, calc(var(--dd-h) * .3)); }

.dd-hotspot {
  position: relative;             /* over .dd-scene-art */
  min-height: 64px;
  min-width: 120px;
  padding: 10px 20px;
  border-radius: 999px;
  border: 2px solid var(--dd-blue);
  background: rgba(14, 44, 78, .92);
  color: var(--dd-cream);
  font: inherit;
  font-weight: 800;
  font-size: clamp(13px, 1.7vw, 18px);
  cursor: pointer;
}
.dd-hotspot:hover { background: #17568f; }
.dd-hotspot:focus-visible { outline: 3px solid var(--dd-gold); outline-offset: 3px; }

/* Compare (§12) — 3 to 5 items, large labels, nothing crowded. */
.dd-compare {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 12px;
}
.dd-compare-item {
  border-radius: 16px;
  border: 1px solid var(--dd-line);
  background: rgba(10, 30, 58, .85);
  padding: 12px 14px;
  text-align: center;
}
.dd-compare-item strong {
  display: block;
  font-size: clamp(15px, 2.1vw, 22px);
  color: var(--dd-gold);
}
.dd-compare-item span {
  display: block;
  margin-top: 4px;
  font-size: clamp(12px, 1.5vw, 16px);
  line-height: 1.35;
}

/* FactStrip (§10) — the bottom ribbon idiom the main pages already use. */
.dd-facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
  gap: 10px;
  border-radius: 16px;
  border: 1px solid var(--dd-line);
  background: linear-gradient(#fffdf5, #fff6df);
  color: #15365f;
  padding: 10px;
}
.dd-fact { text-align: center; padding: 4px; }
.dd-fact span {
  display: block;
  font-size: clamp(11px, 1.3vw, 14px);
  font-weight: 800;
}
.dd-fact strong {
  display: block;
  margin-top: 2px;
  font-size: clamp(13px, 1.7vw, 18px);
  color: #a44e32;
}

/* ChipList (§10) — secondary labels. Tidy, and every chip is still a real
   target, because §7.5 has these becoming tappable pronunciations in Phase 1C. */
.dd-chips { display: flex; flex-wrap: wrap; gap: 10px; }
.dd-chip {
  min-height: 48px;
  padding: 8px 18px;
  border-radius: 999px;
  border: 2px solid var(--dd-line);
  background: rgba(12, 36, 66, .9);
  color: var(--dd-cream);
  font: inherit;
  font-weight: 800;
  font-size: clamp(13px, 1.6vw, 17px);
  line-height: 1.15;
  cursor: pointer;
  /* A chip carrying a written pronunciation is two stacked lines, so it is
     laid out rather than left to the button's default centring. `min-height`
     still governs: 48px is a floor, and the second line grows the chip past it
     rather than squeezing the text. */
  display: grid;
  align-content: center;
  gap: 1px;
  text-align: center;
}
.dd-chip:hover { background: #17568f; }
.dd-chip:focus-visible { outline: 3px solid var(--dd-gold); outline-offset: 3px; }

.dd-chip-name { display: block; }
.dd-chip-pron {
  display: block;
  font-size: clamp(10px, 1.2vw, 13px);
  font-weight: 700;
  color: var(--dd-blue);
  opacity: .92;
  letter-spacing: .01em;
}
/* The chosen chip, and the one panel its fact lands in. Gold marks the choice
   because gold is already what "you are here" means in this overlay — the
   contents card for the current page and the lit position dot both use it. */
.dd-chip[data-on="true"] {
  border-color: var(--dd-gold);
  background: #1c4677;
}
.dd-chip[data-on="true"] .dd-chip-name { color: var(--dd-gold); }
.dd-chip-note {
  border-radius: 16px;
  border: 1px solid var(--dd-line);
  background: linear-gradient(#fffdf5, #fff6df);
  color: #15365f;
  padding: 12px 14px;
  min-height: 44px;
  display: grid;
  align-content: center;
  gap: 3px;
}
.dd-chip-note strong {
  font-size: clamp(13px, 1.7vw, 18px);
  color: #a44e32;
}
.dd-chip-note strong:empty { display: none; }
.dd-chip-note span {
  font-size: clamp(12px, 1.5vw, 16px);
  line-height: 1.35;
}

/* ---- Foot: position inside the mini-book, and forward movement ---- */
.dd-foot {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  border-top: 1px solid var(--dd-line);
  background: rgba(4, 12, 26, .6);
}
.dd-pos {
  grid-column: 2;
  font-weight: 800;
  font-size: clamp(12px, 1.5vw, 16px);
  color: var(--dd-blue);
}
.dd-dots { grid-column: 1; display: flex; gap: 6px; align-items: center; }
.dd-dot {
  width: 10px; height: 10px; border-radius: 50%;
  background: rgba(141, 232, 255, .32);
}
.dd-dot[data-on="true"] { background: var(--dd-gold); }
.dd-next { grid-column: 3; justify-self: end; }

/* ============================================================
   TOC overlay (§7.1). Card UI is allowed HERE and only here.
   ============================================================ */
.toc-body { display: grid; align-content: start; gap: 14px; }
.toc-chapter {
  margin: 0;
  font-size: clamp(12px, 1.5vw, 16px);
  font-weight: 800;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--dd-blue);
  opacity: .85;
}
.toc-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(168px, 1fr));
  gap: 10px;
}
.toc-card {
  min-height: 68px;
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  border-radius: 14px;
  border: 1px solid var(--dd-line);
  background: rgba(10, 30, 58, .88);
  color: var(--dd-ink);
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.toc-card:hover { background: #17496b; }
.toc-card:focus-visible { outline: 3px solid var(--dd-gold); outline-offset: 3px; }
.toc-card[data-current="true"] { border-color: var(--dd-gold); background: #1c4677; }
.toc-num {
  display: grid;
  place-items: center;
  width: 34px; height: 34px;
  border-radius: 10px;
  background: var(--dd-gold);
  color: #17203a;
  font-weight: 900;
  font-size: 15px;
}
.toc-text { min-width: 0; }
.toc-text strong {
  display: block;
  font-size: clamp(12px, 1.5vw, 16px);
  line-height: 1.2;
  overflow-wrap: anywhere;
}
.toc-text span {
  display: block;
  margin-top: 2px;
  font-size: clamp(10px, 1.2vw, 13px);
  line-height: 1.25;
  color: var(--dd-blue);
  opacity: .88;
}

/* ============================================================
   The controls that live ON a main page.
   ============================================================ */

/* The book icon that opens the contents (§7.1). Top-left, under the back-to-hub
   arrow, which is where the page's other chrome already is. It is not a story
   action: the main page's two-CTA budget (§5) is for things in the story, and
   this sits with the navigation furniture. */
.toc-open {
  position: absolute;
  z-index: 60;
  left: 1.5%;
  top: calc(1.5% + 42px);
  width: 48px;
  height: 48px;
  display: grid;
  place-items: center;
  border-radius: 12px;
  border: 1px solid rgba(255, 255, 255, .2);
  background: #16345c;
  color: #fff;
  cursor: pointer;
  padding: 0;
}
.toc-open:hover { background: #285b87; }
.toc-open:focus-visible { outline: 3px solid #8eeaff; outline-offset: 2px; }
.toc-open svg { width: 26px; height: 26px; }

/* The deep-dive launch button a main page offers (§3's "Explore the moons").
   Phase 1A mounts it only for a preview entry; Phase 1B gives pages 10 and 12
   real ones. It is drawn as a secondary action so it cannot outshout Listen. */
.dd-open {
  min-height: 48px;
  padding: 0 18px;
  border-radius: 12px;
  border: 2px solid var(--dd-gold, #ffe27d);
  background: rgba(10, 30, 58, .9);
  color: #fff8df;
  font: inherit;
  font-weight: 800;
  font-size: clamp(13px, 1.6vw, 17px);
  cursor: pointer;
}
.dd-open:hover { background: #17496b; }
.dd-open:focus-visible { outline: 3px solid #8eeaff; outline-offset: 3px; }

/* Page 12's Jupiter-moon roster uses the asteroid menu's two-scale visual
   grammar, with photographic moon cutouts instead of SVG rocks. */
.dd-moon-roster {
  border: 1px solid var(--dd-line);
  border-radius: 16px;
  background: radial-gradient(90% 120% at 20% 10%, #1d3f6d, #071327 72%);
  padding: 12px;
  display: grid;
  gap: 14px;
}
.dd-moon-big-row,
.dd-moon-small-row {
  display: flex;
  align-items: flex-end;
  justify-content: space-evenly;
  gap: 10px;
  min-width: 0;
}
.dd-moon-zoom {
  border: 2px solid rgba(255, 216, 77, .8);
  border-radius: 16px;
  padding: 0 8px 10px;
  background: linear-gradient(180deg, rgba(33,70,118,.8), rgba(9,26,52,.9));
}
.dd-moon-cell {
  min-width: 64px;
  min-height: 64px;
  padding: 4px;
  border: 0;
  border-radius: 14px;
  background: transparent;
  color: var(--dd-ink);
  font: inherit;
  font-weight: 900;
  display: grid;
  justify-items: center;
  align-content: end;
  gap: 5px;
  cursor: pointer;
}
.dd-moon-cell:hover { background: rgba(142, 234, 255, .1); }
.dd-moon-cell:focus-visible { outline: 3px solid var(--dd-gold); outline-offset: 2px; }
.dd-moon-cell > img {
  width: var(--moon-size, 92px);
  height: var(--moon-size, 92px);
  max-width: 100%;
  object-fit: contain;
  filter: drop-shadow(0 5px 5px rgba(0,0,0,.35));
}
.dd-moon-cell-small { flex: 1 1 0; }
.dd-moon-cell-small > img { width: min(88px, 13vw); height: min(88px, 13vw); }
.dd-moon-name {
  min-height: 32px;
  display: grid;
  place-items: center;
  padding: 2px 8px;
  border-radius: 999px;
  background: rgba(16, 52, 89, .96);
  border: 1px solid rgba(142, 234, 255, .38);
  font-size: clamp(13px, 1.5vw, 16px);
}
.dd-moon-actual {
  min-height: 34px;
  display: grid;
  grid-template-columns: 12px minmax(0, 1fr);
  align-items: center;
  gap: 4px;
  color: var(--dd-blue);
  font-size: clamp(11px, 1.15vw, 13px);
  line-height: 1.08;
}
.dd-moon-dot {
  width: 12px; height: 12px;
  display: grid; place-items: center;
  border: 1px solid rgba(255,255,255,.35);
  border-radius: 50%;
}
.dd-moon-dot::after {
  content: "";
  width: var(--actual-size); height: var(--actual-size);
  min-width: 1px; min-height: 1px;
  border-radius: 50%;
  background: #e8d7bd;
}
@media (max-width: 820px) {
  .dd-moon-small-row { gap: 2px; }
  .dd-moon-cell-small { padding-inline: 1px; }
  .dd-moon-actual { grid-template-columns: 10px minmax(0,1fr); font-size: 10px; }
}
@media (orientation: portrait) and (max-width: 820px) {
  .dd-moon-small-row { flex-wrap: wrap; }
  .dd-moon-cell-small { flex: 1 1 30%; }
}

/* ---- Calm mode ----
   Every page carries a Calm mode button that sets `.reduced` on <body> and
   stops the page moving. The overlay has one transition and it honours it. */
.dd-panel { animation: dd-in .22s ease-out; }
@keyframes dd-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
body.reduced .dd-panel,
body.reduce-motion .dd-panel { animation: none; }
@media (prefers-reduced-motion: reduce) { .dd-panel { animation: none; } }

/* ============================================================
   The asteroid roster (§7.2), and the ten body pages behind it.

   The rules that matter here are about SIZE and about TAPPING, and both are
   explained at length in app/deep-dive.js above renderRoster(). In short:

     The six big worlds sit in one true-relative-scale row under the label
     "Real size comparison". Every true-scale rock is `diameter_km x --rk` and
     nothing else, so the six are drawn in honest proportion to one another.
     `--rk` is a single number written by fitRoster() after it measures the
     panel; on a short landscape viewport the row gives up height UNIFORMLY —
     one number for every body, so the comparison never bends.

     The four small worlds share one yellow-bordered "Zoomed in" panel
     (owner, 2026-09-24; re-titled in layout (b), 2026-09-25) whose header is
     a small yellow pill sitting on the panel's top-left edge like a tab. In
     landscape the four zoomed rocks aim ~20-25% larger than round-1's ~96px
     art (zoomTargets() in deep-dive.js); the belt row above keeps its true
     relative scale and gives up the vertical room. Under each enlarged rock's
     name is the true-scale dot that round 2's note replaced: a faint ring
     holding that same body at the top row's `--rk`, captioned with two short
     lines, "Actual size" / "same scale", stacked and centred under the ring
     (polish round 2026-09-25 — the caption is ≥13px and the ring is a little
     larger, while the dot itself stays true scale) — the honesty lives in the
     picture again. The round lenses, their handles, the leader lines and the
     old scale-dot leaders are all gone from this menu. Held upright on a
     768x1024 iPad the leftover vertical air is distributed intentionally
     inside the scene (fitRoster's portrait pass pushes the zoom panel down
     and grows the scene's bottom padding), so the roster fills the body and
     nothing scrolls. The body pages a7–a10 still show their small world
     through a lens (see .dd-focus-lens): this is the menu's rule only.

     A rock button is transparent to the pointer. The silhouette inside it is
     the target, widened evenly by a 28px non-scaling stroke that makes the
     smallest belt rock a 64px touch area — the owner's 2026-09-25 tap rule —
     because a rectangle over an irregular rock is mostly empty and eats its
     neighbour's taps.
   ============================================================ */

.dd-roster {
  position: relative;
  border-radius: 16px;
  border: 1px solid var(--dd-line);
  background:
    radial-gradient(90% 120% at 20% 10%, #1d3f6d 0%, #0b1c36 60%, #071327 100%);
  padding: 10px;                  /* was 14px: layout (b) and the polish round buy the zoom row's air from the panel's own chrome */
  display: grid;
  gap: 8px;                       /* was 14px: same trade, on the short landscape viewport */
  justify-items: center;
  /* Fallback only. fitRoster() overwrites it as soon as the panel is on
     screen and can be measured; this keeps the scene sane for the one frame
     before that, and on a browser that never fires the callback. */
  --rk: 0.21;
}

/* ---- the roster's one-line head: the label pill and the tap hint ----
   "Real size comparison" names what the top row does (owner, 2026-09-25) and
   sits on the panel's top-left border like the zoom pill's tab, only calmer
   (polish round 2026-09-25): sentence case, ~16-18px, faint, instead of the
   uppercase caps that read as a shout beside the pill. The optional yellow
   "Tap an asteroid" hint sits on the same line, top-right (owner: only if it
   fits and is not clutter). On-screen text only: nothing here is spoken
   (AUDIO-DIRECTION.md). */
.dd-roster-head {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  flex-wrap: wrap;
  /* Pull the head (and with it the label pill) up over the panel's top
     border, the same tab idiom the zoom pill uses (see .dd-roster-zoom-head).
     The fit math measures the scene live, so this is seen by the vertical
     budget. */
  margin-top: -12px;
}
.dd-roster-label {
  display: inline-flex;
  align-items: center;
  /* The pill's extra push over the border is a TRANSFORM, not a margin: a
     negative margin is cancelled by the head's `align-items: center` and
     would change the row height (costing the zoom row its budget), while a
     translate moves the painted pill without moving anything in layout. */
  transform: translateY(-10px);
  padding: 3px 12px;
  border-radius: 999px;
  border: 1px solid rgba(255, 216, 77, .55);
  background: rgba(10, 28, 54, .92);
  color: var(--dd-cream);
  font-weight: 800;
  letter-spacing: .02em;
  text-transform: none;           /* sentence case — the pill reads calmly */
  font-size: clamp(16px, 1.75vw, 18px);
  line-height: 1.15;
  white-space: nowrap;
}
.dd-tap-hint {
  display: inline-flex;
  align-items: center;
  padding: 3px 12px;
  border-radius: 999px;
  background: rgba(255, 216, 77, .92);   /* warm yellow, like the zoom pill */
  color: #17203a;
  font-weight: 800;
  font-size: clamp(15px, 1.6vw, 17px);
  line-height: 1.2;
  white-space: nowrap;
  opacity: .95;
}

/* ---- the six-body belt: one baseline, largest first ----
   The lineup is the whole size statement, so the rocks stand on a line rather
   than being centred in equal cells. */
.dd-roster-belt {
  position: relative;                 /* over .dd-scene-art */
  display: flex;
  align-items: flex-end;
  justify-content: safe center;
  width: 100%;
  flex-wrap: nowrap;                  /* one baseline is the size comparison */
  gap: 8px;                           /* ROCK_GAP in deep-dive.js — keep in step */
  overflow-x: auto;                   /* the 36px art floor wins on narrower panels */
}

/* ---- the shared "Zoomed in" panel (owner, 2026-09-24; layout (b) re-titled 2026-09-25) ----
   The four small worlds live in one rectangular panel that names the trick
   with its yellow "Zoomed in" tab; under each enlarged rock's name, the tiny
   true-scale dot (see .dd-rock-tiny) carries the "this much" honesty in the
   picture. The panel is a window onto four separate worlds, not a new row of
   the size comparison: the rocks are spaced apart and nothing runs between
   them, so it cannot read as a path or a sequence (the mistake the 2026-09-23
   lens-handle removal was about). */
.dd-roster-zoom {
  position: relative;                 /* over .dd-scene-art */
  width: 100%;
  /* The pill tab hangs above the panel's top border; this margin is the room
     that buys it (belt bottom → pill top ≈ 12px). The fit math measures the
     scene live, so this is seen by the vertical budget like any other chrome. */
  margin-top: 12px;                   /* was 18px: layout (b) buys its label and dots from the panel's air */
  border-radius: 16px;
  border: 2px solid rgba(255, 216, 77, .8);   /* warm yellow, per layout (b) */
  background:
    linear-gradient(180deg, rgba(33, 70, 118, .8) 0%, rgba(9, 26, 52, .9) 100%);
  /* The bottom padding is air, and the head gap is too: a floor-bound zoom row
     on a short viewport (landscape iPad) cannot shrink its targets, so the
     saved pixels are what stop the body from growing a scrollbar. The top is
     the head's own line — see .dd-roster-zoom-head. */
  padding: 0 12px 8px;
  display: grid;
  gap: 6px;
  justify-items: center;
}

/* ---- the panel's header: the yellow pill tab ----
   Owner, 2026-09-24: a small yellow "Zooming in" pill from the mockup, as an
   annotation sitting on the panel's top-left edge "like a tab/annotation
   overlapping the panel border" (re-titled "Zoomed in" in layout (b),
   2026-09-25). The head is pulled UP by its negative margin until the pill's
   top half hangs over the border (the head's zero top padding lets it). The
   note that sat beside the pill in rounds 1-2 is gone — the tiny true-scale
   dots (see .dd-rock-tiny) carry that honesty in the picture, and the short
   landscape viewport cannot hold both. The pill stays on-screen text only:
   nothing here is spoken (AUDIO-DIRECTION.md). */
.dd-roster-zoom-head {
  position: relative;
  width: 100%;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px 10px;
  margin-top: -12px;              /* pull the pill's top half over the border */
  padding-top: 0;
}
.dd-zoom-pill {
  flex: none;
  align-self: flex-start;         /* the tab pokes up from the head's top edge */
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 12px;
  border-radius: 999px;
  background: #ffd84d;            /* warm yellow, from the owner's mockup */
  color: #17203a;                 /* dark text on the pill */
  font-weight: 900;
  /* ~20-22px on the 9.7" iPad, in either orientation (polish round
     2026-09-25): the pill has to be readable at a glance, and it anchors the
     whole panel. */
  font-size: clamp(20px, 2.1vw, 22px);
  line-height: 1.15;
  white-space: nowrap;
  /* The pill reads as sitting ON the border against the dark art behind it. */
  box-shadow: 0 2px 6px rgba(4, 12, 26, .35);
}
/* The magnifier, drawn in the pill (polish round 2026-09-25 — not the
   emoji): one crisp glyph that renders identically on macOS, Windows and iOS
   Safari. Sized with the pill's text via `em`, painted currentColor so it
   matches the pill's dark ink. */
.dd-zoom-pill-icon {
  width: 1.05em;
  height: 1.05em;
  flex: none;
  color: #17203a;
}
.dd-zoom-pill-label {
  display: inline-block;          /* the label is a span; the icon sizes off it */
}
.dd-roster-zoom-row {
  position: relative;
  display: flex;
  align-items: flex-end;
  justify-content: safe center;
  gap: 20px;                          /* ZOOM_GAP in deep-dive.js — keep in step */
  width: 100%;
  overflow-x: auto;                   /* the ZOOM_MIN floor wins on narrow panels */
}

/* ---- one cell: a rock, and the name pill under it ----
   Two buttons, because they do two different things (see deep-dive.js's note
   above `buildRock`). The cell is the flex item and the column both of them
   line up in; `align-items: flex-end` on the row still lands every pill on one
   baseline, because the pill is the last thing in every cell. */
.dd-rock-cell {
  position: relative;
  display: grid;
  justify-items: center;
  gap: 4px;                        /* cellGap in deep-dive.js — keep in step; layout (b): was 6px */
}

/* ---- one rock ----
   `pointer-events: none` is the hit-shaping, not a mistake: it takes the
   button's rectangle out of the hit test and leaves only the silhouette,
   which switches it back on. The name pill is a separate button beside it and
   is unaffected. The button keeps its focus ring and its keyboard behaviour,
   because neither goes through hit testing. */
.dd-rock {
  position: relative;
  pointer-events: none;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  cursor: pointer;
  display: grid;
  justify-items: center;
  gap: 5px;
}
.dd-rock:focus-visible {
  outline: 3px solid var(--dd-gold);
  outline-offset: 4px;
  border-radius: 14px;
}

.dd-rock-svg {
  display: block;
  /* The honest size, from one measured scale. Nothing else sets it. */
  width:  calc(var(--km) * var(--rk) * var(--aw) * 1px);
  height: calc(var(--km) * var(--rk) * var(--ah) * 1px);
  /* The tap halo is a stroke that reaches past the drawing's own box, and an
     SVG clips to its viewport by default — without this the widened part of
     the target is invisible AND untappable, which is the worse half. */
  overflow: visible;
  transform-origin: 50% 100%;
  transition: transform .16s ease-out;
}
.dd-rock:hover .dd-rock-svg { transform: scale(1.06); }

/* The silhouette, painted invisible, as the target. `non-scaling-stroke` is
   what makes 28 mean 28 screen pixels on Ceres and on Gaspra alike — in user
   units it would be a 60px halo on one and a 42px halo on the other. The
   stroke widened from 14 on 2026-09-25: the owner's tap rule is 64×64 CSS px,
   and the smallest visible rock (Psyche, ROCK_MIN = 36) plus 28px of stroke
   is exactly that floor — round-1's 44px art plus the old 14px stroke was a
   58px target, which no longer clears the bar. The stroke is invisible and
   eats taps only where a target already sits. */
.dd-rock-hit > * {
  fill: transparent;
  stroke: transparent;
  stroke-width: 28;
  vector-effect: non-scaling-stroke;
  pointer-events: all;
  cursor: pointer;
}

/* ---- the name pill: a tap target, not a caption ----
   Owner brief, 2026-09-23: tapping it plays the body's name, and it has to be
   "large and easy to tap" for a preschooler — "avoid tiny icons as the only
   tap target". So the whole pill is the button and the speaker is a hint
   drawn inside it, never the target itself.

   64px is a floor and not a look. The book is designed for a 9.7" iPad at
   768px and its existing targets are around that size, so the pill matches
   them; `min-height` rather than a fixed height lets a long name wrap without
   the text escaping the box. The growth is all downward from the art, into
   the cell's own row, so the pill cannot reach up and steal a tap from the
   rock's shaped silhouette above it — the two targets touch but never
   overlap. */
.dd-rock-pill {
  pointer-events: auto;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
  min-height: 64px;
  min-width: 64px;
  margin: 0;
  /* Horizontal padding is deliberately tighter than the vertical. The six
     true-scale bodies must stand on ONE unbroken line — that lineup is the
     whole size lesson — and `fitRoster` gives up shrinking the rocks once a
     pill is the wider of the two, so every pixel of pill width comes straight
     out of Ceres. At 16px each side the row overflowed its own panel at 768px
     and grew a scrollbar between the two rows, which is the last thing a menu
     that just lost a connector line needs. The tap target does not suffer:
     64px of height is what a finger needs, and no pill is under 80px wide. */
  padding: 8px 9px;
  border-radius: 999px;
  border: 1px solid var(--dd-line);
  background: rgba(7, 22, 44, .9);
  color: var(--dd-cream);
  font-family: inherit;
  font-weight: 800;
  font-size: clamp(12px, 1.4vw, 15px);
  line-height: 1.25;
  white-space: nowrap;
  transition: background .16s ease-out, transform .16s ease-out;
  -webkit-tap-highlight-color: transparent;
}
.dd-rock-pill:hover { background: #17568f; }
.dd-rock-pill:focus-visible {
  outline: 3px solid var(--dd-gold);
  outline-offset: 3px;
}
.dd-rock-ear {
  width: 17px;
  height: 17px;
  flex: none;
  color: var(--dd-cream);
  opacity: .85;
}

/* A name the host page has no clip for. It keeps the old behaviour exactly —
   a label, with nothing that offers a sound it cannot make. */
.dd-rock-pill-mute {
  pointer-events: none;
  cursor: default;
  min-height: 0;
  min-width: 0;
  padding: 3px 11px;
  background: rgba(7, 22, 44, .9);
}

/* ---- the true-scale dot under each enlarged rock (owner, 2026-09-25) ----
   A faint ring holding the SAME body at the top row's `--rk` — its actual
   size, exactly as if it were a body of the "Real size comparison" — with a
   two-line caption, "Actual size" / "same scale", stacked and centred under
   the ring (polish round 2026-09-25: the caption was a single nowrap line at
   ~8px, too small to read; two short lines stay ≥13px without out-widthing
   the cell, and the ring is a little larger — ~26px — while the dot inside
   keeps its true scale). This is layout (b)'s honesty: the enlarged rock says
   "zoomed in", the dot says "this much", the caption says so in words. The
   ring must read as a CAREFUL LABEL, not a second magnifier: faint fill,
   thin border, no
   glass shine, and nothing runs between the four rocks. Purely informational
   — aria-hidden in deep-dive.js, `pointer-events: none` here, so it can
   never eat a tap and never looks like a button. */
.dd-rock-tiny {
  pointer-events: none;
  display: grid;                  /* ring on top, caption below — a column */
  justify-items: center;
  gap: 4px;
  margin-top: -2px;               /* pulls the strip up into the cell's air */
}
.dd-rock-tiny-ring {
  flex: none;
  position: relative;
  display: grid;
  place-items: center;
  width: 26px;                    /* was 24px: a little larger (polish round); 32px measured too tall for the 1024×768 zoom budget */
  height: 26px;
  border-radius: 50%;
  border: 1px dashed rgba(206, 230, 255, .55);   /* faint ring, fine line */
  background: rgba(206, 230, 255, .08);
  box-shadow: none;
}
/* The dot is the top row's scale clipped to the ring: `--rk` is written by
   fitRoster() from the SCENE's scale (floored at 3px of art), and the
   ring just contains what the formula draws — the max keeps a wide body
   (Ida & Dactyl) inside the circle without changing its scale. */
.dd-rock-tiny-ring .dd-rock-svg {
  max-width: 20px;                /* tracking the ring; the dot stays true scale */
  max-height: 20px;
}
.dd-rock-tiny-cap {
  display: flex;                 /* the two caption lines stack */
  flex-direction: column;
  align-items: center;
  gap: 1px;
  font-size: clamp(13px, 1.3vw, 15px);
  line-height: 1.15;
  color: var(--dd-blue);
  opacity: .9;
  text-align: center;
}

/* ---- playback feedback ----
   The tap has to answer visibly, or a child who cannot hear the clip yet taps
   again over the top of it. Gold rim, lifted background, and one slow breath
   of a pulse for as long as the clip runs. */
.dd-rock-pill[data-playing="true"] {
  background: #17568f;
  border-color: var(--dd-gold);
  animation: dd-pill-pulse 750ms ease-in-out 2;
}
@keyframes dd-pill-pulse {
  0%, 100% { transform: scale(1);    box-shadow: 0 0 0 0 rgba(242, 231, 200, .55); }
  50%      { transform: scale(1.06); box-shadow: 0 0 0 7px rgba(242, 231, 200, 0); }
}

/* ---- the magnifier, still on the four small bodies' OWN pages ----
   The roster menu swapped its four round lenses for one shared zoom panel on
   2026-09-24 (see .dd-roster-zoom above), but the body pages a7–a10 keep the
   magnifier: on a page about one world, "this drawing is a zoomed-in view" is
   exactly what a lens says before a word is read, and the true-scale dot
   beneath keeps the honesty. These rules therefore belong to .dd-focus-lens
   alone — nothing in the roster emits .dd-rock-lens any more. */
.dd-focus-lens { --gw: var(--glass); }
.dd-focus-lens[data-wide="true"] {
  /* Ida is drawn nearly twice as wide as she is tall. A round lens would cut
     her to 31px on the short axis, under the target floor. */
  --gw: calc(var(--glass) * 1.55);
}

.dd-rock-glass {
  position: relative;
  display: grid;
  place-items: center;
  width: var(--gw);
  height: var(--glass);
  border-radius: 50%;
  border: 5px solid #c6d4e6;
  background:
    radial-gradient(circle at 34% 26%, rgba(196,228,255,.30), rgba(10,28,52,.62) 68%);
  box-shadow: 0 2px 10px rgba(0, 0, 0, .45), inset 0 0 14px rgba(150, 200, 255, .18);
}
/* THE HANDLE IS GONE, AND IT IS NOT COMING BACK BY ACCIDENT (owner,
   2026-09-23). Each lens used to draw a pale bar out of its left side at 34°.
   One handle reads as a magnifying glass; four in a row, each ending at the
   edge of the lens before it, read as one white line threading Lutetia →
   Mathilde → Ida & Dactyl → Gaspra together. A line between things claims a
   relationship — a path, an order, a sequence — and these four have none: they
   are four separate worlds in no particular order. The roster settled this for
   good on 2026-09-24 by dropping the four lenses for the shared zoom panel,
   whose header carries the "enlarged" meaning in words and whose note carries
   the honesty the dots used to. Do not reinstate a decorative element that
   runs between neighbouring items here. */
.dd-focus-lens .dd-rock-svg {
  /* Contain the full SVG in 82% of the glass on BOTH axes. The previous rule
     fixed only its height, so Ida and Gaspra (both strongly elongated) spilled
     through the sides of the lens. `min()` chooses the limiting axis while
     preserving the drawing's aspect ratio. */
  width: min(calc(var(--gw) * .82),
             calc(var(--glass) * .82 * var(--aw) / var(--ah)));
  height: min(calc(var(--glass) * .82),
              calc(var(--gw) * .82 * var(--ah) / var(--aw)));
  transform-origin: 50% 50%;
}

/* The body at TRUE scale — the same formula the six big ones use, so the
   enlarged drawing above it can never quietly become the size claim. On a body
   page there is one lens and one dot: Lutetia's FOCUS dot really is eight
   times Gaspra's, which a child paging between those two worlds can see. */
.dd-rock-true {
  position: relative;
  width:  calc(var(--km) * var(--rk) * var(--sw) * 1px);
  height: calc(var(--km) * var(--rk) * var(--sh) * 1px);
  min-width: 2px;
  min-height: 2px;
  border-radius: 44%;
  background: #f2e7c8;
  box-shadow: 0 0 0 1px rgba(4, 12, 26, .75);
}
.dd-rock-true::before {
  content: "";
  position: absolute;
  left: 50%;
  bottom: 100%;
  margin-bottom: 3px;
  width: 2px;
  height: 11px;
  transform: translateX(-50%);
  background: linear-gradient(rgba(206, 230, 255, .08), rgba(206, 230, 255, .66));
}

/* ---- a body's own page ----
   One scale for all ten pages, taken from Ceres rather than from whichever
   body is showing: a scale that reset per page would make every world the same
   size again, which is the one thing the roster exists to prevent. */
.dd-focus {
  position: relative;
  display: grid;
  justify-items: center;
  gap: 6px;
  padding: 6px 0 2px;
  --rk: 0.3;
  --glass: 150px;
}
.dd-focus .dd-rock-svg,
.dd-focus .dd-rock-hit > * { pointer-events: none; cursor: default; }
.dd-focus .dd-rock-svg { transition: none; }

/* Reduced motion takes the movement away, never the feedback: the playing
   pill still changes colour and still takes the gold rim, because that is the
   part that answers the child's tap. Only the scaling breath goes. */
@media (prefers-reduced-motion: reduce) {
  .dd-rock-svg { transition: none; }
  .dd-rock:hover .dd-rock-svg { transform: none; }
  .dd-rock-pill { transition: background .16s ease-out; }
  .dd-rock-pill[data-playing="true"] { animation: none; transform: none; }
}
body.reduced .dd-rock-svg,
body.reduce-motion .dd-rock-svg { transition: none; }
body.reduced .dd-rock:hover .dd-rock-svg,
body.reduce-motion .dd-rock:hover .dd-rock-svg { transform: none; }
body.reduced .dd-rock-pill[data-playing="true"],
body.reduce-motion .dd-rock-pill[data-playing="true"] {
  animation: none;
  transform: none;
}
