.viz-root {
  color-scheme: light;
  /* Darkened off-white/soft-blue palette rather than near-white. Two earlier
     passes (~99%->~95%, then ~95%->~87% lightness) were both still "too
     bright" — this one targets ~50% lightness, a real mid-tone rather than
     a lightly-tinted white. At this depth text-secondary/text-muted/border
     also needed darkening to stay legible (see below) — everything else
     (accent colors, eliminated-fill) is untouched; eliminated-fill in
     particular is deliberately left alone regardless of any theme, map or
     background, since it's a fixed neutral shared with the legend swatch. */
  --surface-1:      #8f8d88;
  --page-plane:     #807c76;
  --text-primary:   #0b0b0b;
  /* Darkened along with the backgrounds above — the original secondary/muted
     values were tuned for a near-white page and became nearly illegible
     (measured ~1.9:1 and ~1.2:1 contrast) once the background itself dropped
     to ~50% lightness. */
  --text-secondary: #2a2925;
  --text-muted:     #46443f;
  --border:         rgba(11,11,11,0.22);
  /* A deeper blue/green pair (Okabe-Ito's #0072B2/#009E73 -- a categorical
     pair specifically documented as distinguishable under every common
     colorblindness type) rather than the old blue/orange, which read as too
     bright/cartoonish against the app's near-black default background. */
  --group-a:        #0072b2;   /* series slot 1: blue */
  --group-b:        #00875f;   /* series slot 2: green */
  --guessable:      #4a3aa7;   /* series slot 7: violet */
  /* A country the player has actively PICKED (Neighbor Match). Deliberately
     not --group-b: that green and the --guessable violet it replaces on click
     sit at almost the same luminance, so the two states differed by hue alone
     and measured 1.9:1 against each other here and 1.6:1 in dark mode — under
     the 3:1 WCAG asks of non-text UI, and the reason a pick read as "the same
     colour, slightly brighter". This is the same green family, lifted until it
     clears 3:1 against the violet. It stays a separate token because --group-b
     also means "the other half of the world" in Geo Shrink and "the second
     option" in Size Showdown, where nothing is selected and this contrast
     would just be loud. */
  --picked:         #00b37e;
  --eliminated-fill:#e1e0d9;
  --eliminated-stroke: #c3c2b7;
  --ocean:          #74808f;
  --graticule:      #687383;
  --good:           #0ca30c;
  --critical:       #d03b3b;
  --shadow: 0 1px 2px rgba(11,11,11,0.06), 0 8px 24px rgba(11,11,11,0.10);
  --gold: #c9902a;
  --gold-lit: #f0cf7a;

  /* --- UI scale tokens -----------------------------------------------------
     The single place that decides how big "a control", "a tile", "a label"
     is anywhere in the app — menus, screens, overlays and the in-game HUD
     all read these instead of hardcoding px. Two consequences worth the
     indirection: a phone in portrait, the same phone in landscape and a
     desktop get one consistent set of proportions rather than each screen
     re-deciding; and re-tuning for a device means editing this block and the
     two media queries at the bottom of this file, not hunting through 2000
     lines of rules. Sizes that must respond continuously (tile height) use
     clamp() against svh/vw; sizes that should step at an orientation
     boundary are overridden in those media queries. */
  --ui-gap: 10px;
  --ui-radius: 14px;
  --ctl-h: 40px;          /* minimum height of anything tappable */
  --ctl-pad-x: 14px;
  --icon-size: 22px;
  --tile-min-h: 76px;
  --tile-pad-y: 12px;
  --tile-pad-x: 14px;
  /* The menu grid's minimum column. This is a MEASURED floor, not a taste:
     the longest single word in the menu ("Multiplayer") is 99px at the title
     font, and a tile spends 85px on padding + 22px icon + gaps + the 21px (i)
     button, so anything under ~184px forces a word to break mid-word. Every
     tier below clears that. 224px would additionally put every two-word title
     on one line, which is what the large tier buys. */
  --tile-col: 190px;
  --bar-h: 9px;           /* progress/XP track thickness */
  --fs-title: 15.5px;
  --fs-body: 13px;
  --fs-label: 12px;
  --fs-micro: 10.5px;

  /* Atlas mode continent categories — slots 1-5 of the same validated set */
  --cont-1: #2a78d6;   /* Africa: blue */
  --cont-2: #eb6834;   /* Americas: orange */
  --cont-3: #1baf7a;   /* Asia: aqua */
  --cont-4: #eda100;   /* Europe: yellow */
  --cont-5: #e87ba4;   /* Oceania: magenta */
}
@media (prefers-color-scheme: dark) {
  :root:where(:not([data-theme="light"])) .viz-root {
    color-scheme: dark;
    --surface-1:      #1a1a19;
    --page-plane:     #0d0d0d;
    --text-primary:   #ffffff;
    --text-secondary: #c3c2b7;
    --text-muted:     #898781;
    --border:         rgba(255,255,255,0.10);
    --group-a:        #2f92d6;
    --group-b:        #16a373;
    --guessable:      #6e62be;
    /* Lighter than the light-mode pick: here it only has to beat the violet
       and the near-black map, not a pale --eliminated-fill. 3.5:1 vs violet. */
    --picked:         #6bf0ae;
    --eliminated-fill:#2c2c2a;
    --eliminated-stroke: #383835;
    --ocean:          #14232f;
    --graticule:      #223244;
    --good:           #0ca30c;
    --critical:       #e66767;
    --shadow: 0 1px 2px rgba(0,0,0,0.35), 0 8px 24px rgba(0,0,0,0.5);
    --cont-1: #3987e5;
    --cont-2: #d95926;
    --cont-3: #199e70;
    --cont-4: #c98500;
    --cont-5: #d55181;
  }
}
:root[data-theme="dark"] .viz-root {
  color-scheme: dark;
  --surface-1:      #1a1a19;
  --page-plane:     #0d0d0d;
  --text-primary:   #ffffff;
  --text-secondary: #c3c2b7;
  --text-muted:     #898781;
  --border:         rgba(255,255,255,0.10);
  --group-a:        #3987e5;
  --group-b:        #d95926;
  --guessable:      #6e62be;
  --picked:         #6bf0ae;
  --eliminated-fill:#2c2c2a;
  --eliminated-stroke: #383835;
  --ocean:          #14232f;
  --graticule:      #223244;
  --good:           #0ca30c;
  --critical:       #e66767;
  --shadow: 0 1px 2px rgba(0,0,0,0.35), 0 8px 24px rgba(0,0,0,0.5);
  --cont-1: #3987e5;
  --cont-2: #d95926;
  --cont-3: #199e70;
  --cont-4: #c98500;
  --cont-5: #d55181;
}

* { box-sizing: border-box; }
html, body { height: 100%; overflow: hidden; overscroll-behavior: none; }
body {
  margin: 0;
  font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
  background: var(--page-plane);
  color: var(--text-primary);
}
.viz-root { position: fixed; inset: 0; }

/* the map always fills the entire page ------------------------------- */
.board {
  position: absolute;
  inset: 0;
  background: var(--ocean);
  overflow: hidden;
}
svg#map { display: block; width: 100%; height: 100%; }
.graticule { fill: none; stroke: var(--graticule); stroke-width: 0.6; }
.sphere-outline { fill: none; stroke: var(--graticule); stroke-width: 1; }

.country { stroke: var(--surface-1); stroke-width: 0.5; cursor: default; transition: fill 260ms ease, opacity 300ms ease; }
/* Invisible larger click target standing in for countries too small to hit
   precisely at their real rendered size (see engine/map.js's updateHitAssist) --
   needs fill:transparent + explicit pointer-events (a transparent fill is
   otherwise NOT hit-testable under SVG's default visiblePainted behavior). */
.hit-assist-dot { fill: transparent; pointer-events: all; cursor: pointer; }
.country.eliminated { fill: var(--eliminated-fill); stroke: var(--eliminated-stroke); opacity: 0.6; cursor: default; }
.country.cont-1 { fill: var(--cont-1); }
.country.cont-2 { fill: var(--cont-2); }
.country.cont-3 { fill: var(--cont-3); }
.country.cont-4 { fill: var(--cont-4); }
.country.cont-5 { fill: var(--cont-5); }
.country.cont-other { fill: var(--eliminated-fill); stroke: var(--eliminated-stroke); }

/* Rivers/lakes overlay -- Water Wisdom's playing field, also shown
   read-only in World Atlas (see engine/map.js's setWaterFeatures/
   setWaterVisible/setWaterInteractive). Gray by default -- same "neutral
   = not in play" meaning countries' own --eliminated-fill already carries
   everywhere else in the app -- reserving blue exclusively for "this is
   water and you can click it," rather than three unrelated hues competing
   (blue default + green/red groups read as confusing, not three clean
   states). */
.water-layer { pointer-events: none; }
.water-layer.hidden { display: none; }

/* Mid-gesture level of detail. While the map is being dragged, pinched or
   wheel-zoomed, engine/map.js stops regenerating these two layers: the water
   overlay is by a wide margin the most expensive thing in a drag frame (~68ms
   on a 4x-throttled CPU, vs ~52ms for all 205 countries put together) and the
   labels are both costly and unreadable while everything is moving. Hiding
   them also fixes a long-standing glitch -- neither is included in the zoom
   preview transform, so during a pinch they used to visibly detach from the
   countries underneath. Instant on the way out (no transition while hidden,
   so nothing stale is ever shown sliding around), soft on the way back. */
#map .water-layer, #map .country-labels { transition: opacity 200ms ease; }
#map.gesture-lod .water-layer,
#map.gesture-lod .country-labels { opacity: 0; transition: none; pointer-events: none; }

/* The other half of mid-gesture LOD, on the rasterising side rather than the
   geometry side. Anti-aliasing every country outline is wasted on a map that
   is moving, and the themed per-country drop-shadows (glow / sci-fi / dino)
   are outright expensive to composite -- they cost real frames for an effect
   nobody can perceive mid-swipe. Both come straight back on settle. */
#map.gesture-lod .country { shape-rendering: optimizeSpeed; filter: none; }
.river { fill: none; stroke: #7d8590; stroke-width: 1; stroke-linejoin: round; stroke-linecap: round; opacity: 0.85; }
.lake { fill: #7d8590; stroke: #5f6873; stroke-width: 0.4; opacity: 0.85; }
/* Invisible wide click target standing in for a river's real 1px stroke --
   only added to the DOM while a mode has set water interactive. */
.river-hit { fill: none; stroke: transparent; stroke-width: 10; pointer-events: stroke; cursor: pointer; }

/* Water Wisdom is one-type-per-round -- the inactive layer is fully
   hidden, not just dimmed, so it's unambiguous what's actually in play. */
.water-layer.rivers-only .lake { display: none; }
.water-layer.lakes-only .river, .water-layer.lakes-only .river-hit { display: none; }

/* Geo Shrink-style binary narrowing, applied to the water layer instead of
   countries. Both groups keep the SAME blue body -- these are all rivers and
   lakes, and recoloring half of them a different blue asked the player to
   remember which blue was which while also telling two similar blues apart at
   a glance. What separates the groups is the OUTLINE: group A is edged and lit
   in bright water-blue, group B in bright green, which is a difference in hue
   AND in what the eye is being asked to compare (a rim, not a fill).
   The rim is doubled as a glow so it survives on a 1px river line and on a
   lake only a few pixels across; the body color stays constant so a group's
   identity is carried entirely by its edge.
   The two rim colors are also far apart in lightness, not only hue, so the
   pairing still separates for red-green colorblind players -- the same
   precaution the old two-blue palette was taking. */
.river.water-a, .river.water-b { stroke: #1d4ed8; stroke-width: 2.6; opacity: 1; }
.lake.water-a, .lake.water-b { fill: #1d4ed8; stroke-width: 1.4; opacity: 1; }
/* Group A -- blue rim */
.river.water-a { filter: drop-shadow(0 0 1.2px #7dd3fc) drop-shadow(0 0 3.5px #38bdf8); }
.lake.water-a { stroke: #7dd3fc; filter: drop-shadow(0 0 1.2px #7dd3fc) drop-shadow(0 0 3.5px #38bdf8); }
/* Group B -- green rim */
.river.water-b { filter: drop-shadow(0 0 1.2px #86efac) drop-shadow(0 0 3.5px #22c55e); }
.lake.water-b { stroke: #86efac; filter: drop-shadow(0 0 1.2px #86efac) drop-shadow(0 0 3.5px #22c55e); }

.country-label {
  pointer-events: none;
  font-size: 9px;
  font-weight: 700;
  text-anchor: middle;
  dominant-baseline: middle;
  user-select: none;
  fill: rgba(0,0,0,0.78);
  paint-order: stroke;
  stroke: rgba(255,255,255,0.9);
  stroke-width: 3px;
  stroke-linejoin: round;
}
@media (prefers-color-scheme: dark) {
  :root:where(:not([data-theme="light"])) .country-label { fill: rgba(255,255,255,0.88); stroke: rgba(0,0,0,0.65); }
}
:root[data-theme="dark"] .country-label { fill: rgba(255,255,255,0.88); stroke: rgba(0,0,0,0.65); }

/* --narrow-progress (0 at the start of a search, toward 1 as it narrows) is
   set by modes/narrow.js on #map, using the same progress figure already
   driving the mission-banner progress bar — colors start a little washed
   out and become fully vivid/opaque as the search closes in, echoing
   "getting warmer." Left unset (the default, 1) by every other mode, so
   this is purely additive for Geo Shrink and changes nothing elsewhere.
   Only the flat/default look (Classic) picks this up — the gradient-fill
   themes (Coral Reef, Dinosaur) and Population's own pop-based tint keep
   their own distinct looks rather than fighting two color modulations at
   once, same as before.
   group-a/group-b's fill is the diagonal shine gradient built in map.js
   (grad-group-a/b) rather than a flat color -- a gradient can't ALSO be a
   dynamic color-mix(), so the "warms up as you narrow down" feedback moved
   from saturation to opacity instead, same visual story either way. */
.country.group-a {
  fill: url(#grad-group-a);
  opacity: calc(0.72 + var(--narrow-progress, 1) * 0.28);
  filter: drop-shadow(0 0 1.5px color-mix(in srgb, var(--group-a) 60%, transparent));
  cursor: pointer;
}
.country.group-b {
  fill: url(#grad-group-b);
  opacity: calc(0.72 + var(--narrow-progress, 1) * 0.28);
  filter: drop-shadow(0 0 1.5px color-mix(in srgb, var(--group-b) 60%, transparent));
  cursor: pointer;
}
/* Neighbor Match's reference country. group-a already colors it, but a fill
   alone is invisible when the country IS a few pixels — Timor-Leste, Brunei,
   Eswatini. The outline is drawn outside the shape, so it adds width the
   country itself doesn't have, and sits after .group-a so its filter wins.
   Theme-independent on purpose: this is "the question", not decoration. */
.country.ref-country {
  stroke: var(--group-a);
  stroke-width: 2.2px;
  stroke-linejoin: round;
  paint-order: stroke fill;
  filter: drop-shadow(0 0 3px var(--group-a)) drop-shadow(0 0 7px var(--group-a));
}
.country.guessable { fill: color-mix(in srgb, var(--guessable) calc(55% + var(--narrow-progress, 1) * 45%), white); cursor: pointer; }
/* "Shown for context, not a click target this round" — e.g. Compass Quiz's
   and Size Showdown's non-highlighted crowd, or Capital Match's whole map
   (answers come from the button choices, not map clicks there). Distinct
   from .eliminated (excluded from the round entirely) via opacity, and
   never cursor:pointer — every genuinely clickable state uses .guessable,
   .group-a, or .group-b instead, so the cursor always tells the truth. */
.country.available { fill: var(--eliminated-fill); stroke: var(--eliminated-stroke); cursor: default; opacity: 0.85; }

/* Map styles — layered on top of the flat group-a/b fill above.
   .map-style-flat (Classic, free) intentionally has no rules here, so it
   stays exactly the original solid-color look. */

/* Coral Reef (free): a gradient sheen + soft glow. */
.map-style-glow .country.group-a { fill: url(#grad-group-a); filter: drop-shadow(0 0 2.5px var(--group-a)); }
.map-style-glow .country.group-b { fill: url(#grad-group-b); filter: drop-shadow(0 0 2.5px var(--group-b)); }

/* Neon Grid (free): dark backdrop, neon double-glow, slow pulse. */
.map-style-scifi .country { stroke: color-mix(in srgb, var(--graticule) 80%, white); stroke-width: 0.5; }
.map-style-scifi .country.group-a {
  fill: url(#grad-group-a);
  stroke: color-mix(in srgb, var(--group-a) 65%, white);
  stroke-width: 0.9;
  filter: drop-shadow(0 0 3px var(--group-a)) drop-shadow(0 0 8px var(--group-a));
  animation: scifiPulse 3.2s ease-in-out infinite;
}
.map-style-scifi .country.group-b {
  fill: url(#grad-group-b);
  stroke: color-mix(in srgb, var(--group-b) 65%, white);
  stroke-width: 0.9;
  filter: drop-shadow(0 0 3px var(--group-b)) drop-shadow(0 0 8px var(--group-b));
  animation: scifiPulse 3.2s ease-in-out infinite;
  animation-delay: 1.6s;
}
@keyframes scifiPulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.82; }
}
@media (prefers-reduced-motion: reduce) {
  .map-style-scifi .country.group-a, .map-style-scifi .country.group-b { animation: none; }
}

/* Dinosaur (150): a warm gradient sheen + soft amber glow, five real
   species drawn at the region each is famously associated with (map.js's
   .dino-illustrations layer — pointer-events:none, so they never affect
   click targets). The sepia ocean/graticule colors come from the theme's
   ocean/graticule override in theme.js. */
.map-style-dino .dino-illustrations { opacity: 0.8; }
.map-style-dino .country.group-a { fill: url(#grad-group-a); filter: drop-shadow(0 0 2.5px var(--group-a)); }
.map-style-dino .country.group-b { fill: url(#grad-group-b); filter: drop-shadow(0 0 2.5px var(--group-b)); }
.map-style-dino .country { stroke: var(--graticule); }
.dino-illustrations { pointer-events: none; opacity: 0; transition: opacity 400ms ease; }
/* Real paleoart silhouettes (see web/dino/CREDITS.md), not hand-drawn
   shapes — a single traced path per creature, recolored via fill and
   grounded with a drop shadow. .dino-walk applies a small pacing/bob
   animation (translate + slight rotate) so each creature reads as walking
   in place rather than a static decal. */
.dino-figure { pointer-events: none; }
.dino-part { stroke: rgba(0,0,0,0.35); stroke-width: 0.4; stroke-linejoin: round; }
.dino-shadow { fill: rgba(0,0,0,0.22); }
.dino-eye { fill: #12100c; }
.dino-toe { stroke-width: 0.3; }

/* The gait: the body bobs while the two leg groups swing in opposition.
   Transforms only, so the browser can composite them without relayout —
   five creatures animating costs effectively nothing. */
.dino-figure .dino-walk { animation: dinoBob 0.62s ease-in-out infinite; }
.dino-legs .dino-leg { transform-box: fill-box; transform-origin: center top; }
.dino-leg-a { animation: dinoStrideA 0.62s ease-in-out infinite; }
.dino-leg-b { animation: dinoStrideB 0.62s ease-in-out infinite; }
@keyframes dinoBob {
  0%, 100% { transform: translateY(0) rotate(0deg); }
  50%      { transform: translateY(-0.9px) rotate(0.8deg); }
}
/* Legs swing along the body axis (x), which after the figure's heading
   rotation is always "forward" — so a stride reads as a stride from any
   direction of travel. */
@keyframes dinoStrideA {
  0%, 100% { transform: translateX(1.7px) scaleY(0.92); }
  50%      { transform: translateX(-1.7px) scaleY(1.06); }
}
@keyframes dinoStrideB {
  0%, 100% { transform: translateX(-1.7px) scaleY(1.06); }
  50%      { transform: translateX(1.7px) scaleY(0.92); }
}
@media (prefers-reduced-motion: reduce) {
  .dino-figure .dino-walk, .dino-leg-a, .dino-leg-b { animation: none; }
}

/* Blueprint (350): drafting-table look — hairline cyan linework on navy
   paper, fills kept translucent so the grid reads through them. */
.map-style-blueprint .country { stroke: color-mix(in srgb, var(--group-a) 60%, transparent); stroke-width: 0.6; fill: rgba(120,190,235,0.10); }
.map-style-blueprint .country.group-a { fill: color-mix(in srgb, var(--group-a) 34%, transparent); stroke: var(--group-a); }
.map-style-blueprint .country.group-b { fill: color-mix(in srgb, var(--group-b) 30%, transparent); stroke: var(--group-b); }
/* Blueprint is line-art: a solid fill would break the drawing, so the pick
   reads as a brighter wash plus a hard drawn edge instead. */
.map-style-blueprint .country.picked { fill: color-mix(in srgb, var(--picked) 42%, transparent); stroke: var(--picked); stroke-width: 1; }
.map-style-blueprint .country.eliminated { fill: rgba(120,190,235,0.07); stroke: rgba(160,210,245,0.20); }
.map-style-blueprint .graticule { stroke-dasharray: 2 3; stroke-opacity: 0.85; }
.map-style-blueprint .sphere-outline { stroke: color-mix(in srgb, var(--group-a) 70%, transparent); stroke-width: 1.2; }
.map-style-blueprint .country-label { fill: #cfefff; letter-spacing: 0.06em; }

/* Basketball (450): the globe IS the ball. Pebbled orange leather on the
   ocean, eight-panel seams from map.js's gSeams layer, land in a burnt
   darker orange that keeps every contour so the games still play normally. */
.map-style-basketball .sphere-outline { fill: url(#ball-pebble); stroke: #4a2a10; stroke-width: 2.2; }
.map-style-basketball .country { stroke: #38190a; stroke-width: 0.7; fill: #7a3208; }
.map-style-basketball .country.group-a { fill: #8c3d0f; }
.map-style-basketball .country.group-b { fill: #3f2415; }
.map-style-basketball .country.eliminated { fill: #a04d15; }
.map-style-basketball .graticule { stroke-opacity: 0.25; }
.ball-seams { pointer-events: none; opacity: 0; transition: opacity 400ms ease; }
.map-style-basketball .ball-seams { opacity: 1; }
.ball-seams { clip-path: url(#sphere-clip); }
.ball-seam { fill: none; stroke: #2a1206; stroke-width: 4; stroke-linecap: round; opacity: 0.92; }

/* Vintage Atlas (500): aged parchment, sepia inks, hatched coastlines. The
   paper grain is the same turbulence filter idea as the ball's pebble, at a
   much finer frequency. */
.map-style-vintage .sphere-outline { fill: url(#atlas-paper); stroke: #a68a55; stroke-width: 1.6; }
.map-style-vintage .country { stroke: #7a5a32; stroke-width: 0.7; fill: #f0e4c6; }
.map-style-vintage .country.group-a { fill: color-mix(in srgb, var(--group-a) 62%, #f0e4c6); }
.map-style-vintage .country.group-b { fill: color-mix(in srgb, var(--group-b) 62%, #f0e4c6); }
/* Not the shared --picked: that one is tuned to be the LIGHTEST thing on a
   near-black map, and on cream paper it lands at 1.1:1 — invisible. A theme
   with a light surface needs the pick to go darker, not brighter. */
.map-style-vintage .country.picked { fill: #0a5c40; stroke: #063d2a; }
.map-style-vintage .country.eliminated { fill: #e3d3ae; stroke: #b09468; }
.map-style-vintage .graticule { stroke-dasharray: 1 4; stroke-opacity: 0.7; }
.map-style-vintage .country-label { fill: #4a3a20; font-style: italic; letter-spacing: 0.04em; }

/* Pineapple Isle (400): tropical water, pineapple-skin land. The diamond
   cross-hatch is a repeating conic pattern on the fill, not an image. */
.map-style-pineapple .country { stroke: #8a6212; stroke-width: 0.6; fill: #f2c14e; }
.map-style-pineapple .country.group-a { fill: var(--group-a); }
.map-style-pineapple .country.group-b { fill: var(--group-b); }
/* Same reason as Vintage: the pineapple skin is a light gold. */
.map-style-pineapple .country.picked { fill: #0a5c40; stroke: #063d2a; }
.map-style-pineapple .country.eliminated { fill: #d9c48a; }
.map-style-pineapple .sphere-outline { fill: var(--ocean); }
.map-style-pineapple .graticule { stroke-opacity: 0.5; stroke-dasharray: 3 5; }

/* Night Lights (900): near-black earth, and the population data as glowing
   city light rather than a country fill — see .night-lights in map.js. */
.map-style-nightlights .country { stroke: #16233d; stroke-width: 0.45; fill: #0b1220; }
.map-style-nightlights .country.group-a { fill: color-mix(in srgb, var(--group-a) 70%, #0b1220); }
.map-style-nightlights .country.group-b { fill: color-mix(in srgb, var(--group-b) 70%, #0b1220); }
.map-style-nightlights .country.eliminated { fill: #080d17; stroke: #111a2c; }
.map-style-nightlights .graticule { stroke-opacity: 0.35; }
.night-lights { pointer-events: none; opacity: 0; transition: opacity 500ms ease; }
.map-style-nightlights .night-lights { opacity: 1; }
/* The glow is ONE blur over the whole bloom layer. Per-circle drop-shadows
   (the first attempt) meant ~2000 separate filter regions to composite,
   which is exactly the kind of thing that turns a decoration into a frame-
   rate problem on a phone. */
.nl-bloom-layer { filter: blur(2.6px); }
.nl-bloom { fill: #ffae3d; }
.nl-core { fill: #fff4d6; }
/* "Your picks" in Neighbor Match. Placed after every .map-style-* block on
   purpose: those themes set `.map-style-x .country { fill: ... }` at the same
   (0,2,0) specificity as `.country.picked`, so source order is what decides,
   and a pick has to stay unmistakable in every theme rather than being
   repainted by whichever one is active.
   Contrast here is carried by three things, not just hue: a much lighter
   fill, a stroke, and a soft halo. Hue alone is what failed — it also fails
   entirely for a deuteranopic player, for whom this green and the violet it
   replaces are close to the same colour. */
.country.picked {
  fill: var(--picked);
  stroke: color-mix(in srgb, var(--picked) 55%, #000);
  stroke-width: 0.9;
  filter: drop-shadow(0 0 2px color-mix(in srgb, var(--picked) 55%, transparent));
  cursor: pointer;
}
.country.fog-known { fill: var(--group-a); opacity: 0.55; cursor: default; }
.country.fog-frontier { fill: var(--guessable); cursor: pointer; }
.country.exp-done { fill: var(--group-a); opacity: 0.55; cursor: default; }
.country.flash-good { fill: var(--good) !important; }
.country.flash-bad { fill: var(--critical) !important; }

/* Beacons over countries too small to see (map.js renderBeacons) -----------
   A microstate at world zoom is a sub-pixel speck: when it is the answer, or
   the thing you have to click, there is nothing on screen to aim at. This
   marks it with a soft flare that fades out as you zoom in and the country
   becomes legible on its own — the light is a stand-in for the shape, so it
   has no business still being there once you can see the shape.

   Deliberately quiet. It borrows the colour of whatever state the country is
   already in, so it reinforces the existing visual language instead of
   introducing a new one, and it never draws on top of a country you can
   already see. Everything here is non-interactive: the click target is the
   separate .hit-assist-dot, and a beacon that swallowed clicks would break
   that. */
.tiny-beacons { pointer-events: none; }
.tb-glow-layer { filter: blur(2.4px); }
.tb-halo, .tb-star { fill: currentColor; }
.tb-halo { opacity: 0.5; }
/* A near-white centre is what makes it read as a light source rather than a
   coloured dot — but mixed, not pure white, so it keeps contrast against the
   pale ocean of the light themes. */
.tb-core { fill: color-mix(in srgb, currentColor 55%, #ffffff); }

/* Atlas shares the violet rather than getting its own colour, for two
   reasons. It is the only hue the continent palette does not already use
   (Africa blue, Americas orange, Asia green, Europe amber, Oceania pink) —
   --group-a blue was tried and sat invisibly on top of Africa. And a player
   who has seen a violet flare mean "a country too small to draw" in five
   game modes should not have to learn a second vocabulary to explore. */
.tb-guessable, .tb-explore { color: var(--guessable); }
.tb-group-a   { color: var(--group-a); }
.tb-group-b   { color: var(--group-b); }
.tb-good      { color: var(--good); }
.tb-bad       { color: var(--critical); }

/* Slow, shallow breathing — closer to a lighthouse than a notification. The
   stagger comes from map.js (animation-delay per beacon) so a cluster of
   microstates doesn't blink in unison. */
@keyframes tb-breathe {
  0%, 100% { opacity: 0.72; transform: scale(0.94); }
  50%      { opacity: 1;    transform: scale(1.07); }
}
.tb-pulse { animation: tb-breathe 2.9s ease-in-out infinite; transform-origin: 0 0; }
/* Countries that just got revealed pulse a touch harder and faster — this is
   an event, not scenery. */
.tb-good .tb-pulse, .tb-bad .tb-pulse { animation-duration: 1.5s; }
/* The beacon still has to be there without motion — it is wayfinding, not
   decoration — so this stops the breathing and holds it at full size rather
   than hiding it. */
@media (prefers-reduced-motion: reduce) {
  .tb-pulse { animation: none; opacity: 1; transform: none; }
}
.country.group-a:hover, .country.group-b:hover, .country.guessable:hover, .country.fog-frontier:hover { filter: brightness(1.14); }

/* Geo Shrink win celebration — a small "official stamp" landing right on the
   found country (no camera zoom anymore, see modes/narrow.js finishGame()).
   Matches the passport/visa theme already established by Collections rather
   than a generic confetti burst. Lives directly in the map SVG's own
   coordinate space (appended at the country's projected centroid), so it
   tracks the map exactly with no separate positioning math needed. */
.celebration-stamp { pointer-events: none; }
.cs-pulse {
  fill: none;
  stroke: #c9902a;
  stroke-width: 2.5;
  opacity: 0.85;
  animation: csPulse 550ms 80ms ease-out both;
}
@keyframes csPulse {
  from { r: 4; opacity: 0.85; stroke-width: 3; }
  to { r: 30; opacity: 0; stroke-width: 0.5; }
}
.cs-mark {
  color: #c9902a;
  animation: csStampDown 260ms cubic-bezier(.22, 1.4, .4, 1) both, csFadeOut 300ms ease 1050ms both;
}
.cs-mark circle { fill: none; stroke: currentColor; stroke-width: 2; }
.cs-mark path { fill: none; stroke: currentColor; stroke-width: 2.6; stroke-linecap: round; stroke-linejoin: round; }
@keyframes csStampDown {
  0% { transform: scale(2.3) rotate(-16deg); opacity: 0; }
  55% { transform: scale(0.92) rotate(-2deg); opacity: 1; }
  100% { transform: scale(1) rotate(-5deg); opacity: 1; }
}
@keyframes csFadeOut {
  from { opacity: 1; }
  to { opacity: 0; }
}

.legend .known .swatch { background: var(--group-a); opacity: 0.55; }
.legend .frontier .swatch { background: var(--guessable); }

.shake { animation: shake 320ms ease; }
@keyframes shake {
  0%,100% { transform: translateX(0); }
  20% { transform: translateX(-4px); }
  40% { transform: translateX(4px); }
  60% { transform: translateX(-3px); }
  80% { transform: translateX(3px); }
}

/* HUD: overlaid chips, always opaque for guaranteed contrast ------------ */
.hud {
  z-index: 4;
  display: flex;
  align-items: center;
  gap: 8px;
  pointer-events: none; /* let clicks pass through the gaps to the map */
}
.hud > * { pointer-events: auto; }

/* hud-top + hud-mission + mp-scoreboard live in this single flow column so
   the mission banner and scoreboard always land below however tall hud-top
   actually rendered (1 row on desktop, 2-3 wrapped rows on a narrow phone)
   instead of guessing a fixed pixel offset that overlaps on smaller screens. */
.hud-stack {
  position: absolute;
  top: max(12px, env(safe-area-inset-top));
  left: max(12px, env(safe-area-inset-left));
  right: max(12px, env(safe-area-inset-right));
  z-index: 4;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  pointer-events: none;
}
.hud-stack > * { pointer-events: auto; }

.hud-top {
  width: 100%;
  justify-content: space-between;
  flex-wrap: wrap;
}
.brand-chip {
  display: flex;
  align-items: center;
  gap: 7px;
  background: var(--surface-1);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 7px 12px;
  box-shadow: var(--shadow);
  font-weight: 700;
  font-size: 15px;
  color: var(--text-primary);
}
.brand-chip svg { width: 21px; height: 21px; flex: none; }

.wallet-chip {
  display: flex;
  align-items: center;
  gap: 6px;
  background: var(--surface-1);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 7px 12px;
  box-shadow: var(--shadow);
  font: inherit;
  font-weight: 700;
  font-size: 13px;
  color: #c9902a;
  cursor: pointer;
}
.wallet-chip:hover { filter: brightness(0.97); }
@media (prefers-color-scheme: dark) { .wallet-chip:hover { filter: brightness(1.2); } }
:root[data-theme="dark"] .wallet-chip:hover { filter: brightness(1.2); }
.wallet-chip svg { flex: none; }

/* Deliberately low-key — this is an optional upgrade path, not something
   players are pushed toward. Same quiet chip treatment as the wallet, not a
   bigger/brighter "sign in now!" callout. */
.account-chip {
  display: flex;
  align-items: center;
  gap: 6px;
  background: var(--surface-1);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 7px 12px;
  box-shadow: var(--shadow);
  font: inherit;
  font-weight: 700;
  font-size: 13px;
  color: var(--text-secondary);
  cursor: pointer;
  max-width: 140px;
}
.account-chip:hover { filter: brightness(0.97); }
@media (prefers-color-scheme: dark) { .account-chip:hover { filter: brightness(1.2); } }
:root[data-theme="dark"] .account-chip:hover { filter: brightness(1.2); }
.account-chip svg { flex: none; }
.account-chip span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The settings gear button pairs with the profile chip next to it, so it
   reuses the exact same look but as a square icon-only tap target (same
   treatment as .hud-btn.icon-only), rather than a whole separate style. */
.account-chip.icon-only { padding: 8px; max-width: none; }

.stats { display: flex; gap: 7px; flex-wrap: wrap; }
.stat-chip {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 5px 11px;
  border-radius: 8px;
  background: var(--surface-1);
  border: 1px solid var(--border);
  box-shadow: var(--shadow);
  min-width: 56px;
}
.stat-chip .v { font-size: 14px; font-weight: 700; font-variant-numeric: tabular-nums; color: var(--text-primary); }
.stat-chip .l { font-size: 9.5px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.04em; }
.stat-chip.stat-points .v { color: var(--guessable); }

/* Live in-game echo of the home screen's progression banner — XP is earned
   continuously during play, so the payoff (the bar creeping toward a level
   up) should be visible while playing, not just when you go back to Home. */
.hud-level-chip {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 7px 14px;
  border-radius: 9px;
  background: var(--surface-1);
  border: 1px solid var(--border);
  box-shadow: var(--shadow);
  min-width: 72px;
}
.hud-level-top { display: flex; align-items: baseline; gap: 3px; }
.hud-level-top span:first-child { font-size: 16px; font-weight: 700; color: var(--guessable); font-variant-numeric: tabular-nums; }
.hud-level-word { font-size: 9px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.04em; }
.hud-xp-boost-badge { display: none; font-size: 9.5px; font-weight: 800; color: var(--guessable); }
.hud-xp-boost-badge.show { display: inline; }
.hud-level-track { height: 5px; border-radius: 999px; background: var(--eliminated-fill); overflow: hidden; width: 100%; }
.hud-level-xp { font-size: 9.5px; font-weight: 600; color: var(--text-muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
.hud-level-fill { height: 100%; border-radius: 999px; background: linear-gradient(90deg, var(--group-a), var(--guessable)); width: 0%; transition: width 500ms ease; }

/* Terra column | objective banner. Terra takes only the width she needs and
   margin-inline-end:auto shoves the banner to the right edge, so the whole
   middle of the screen — the part players actually rotate and zoom — is left
   clear. Non-blocking between the two, same "gaps pass clicks through" model
   as .hud/.hud-stack. */
.hud-mid {
  width: 100%;
  display: flex;
  align-items: flex-start;
  gap: 10px;
  pointer-events: none;
}
.hud-mid > * { pointer-events: auto; }
.hud-terra {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 6px;
  margin-inline-end: auto;
  min-width: 0;
}
/* :empty is not enough — the dock and the strip are always present, just
   display:none until a mode shows them, so the column would still eat the
   row's gap. Nothing to do: with no visible children it collapses to 0 wide
   and the banner simply keeps its right-hand seat. */

.hud-mission {
  /* Was min(92vw, 560px) and full-width-centered. Narrower now that it shares
     the row: wide enough for "Find: <longest country>" plus the action pill,
     no wider. Fixed rather than content-sized so the banner doesn't resize
     itself every round as the country name changes length. */
  flex: 0 1 auto;
  width: min(52vw, 400px);
  min-width: 0;
  flex-direction: column;
  align-items: stretch;
  gap: 5px;
  background: var(--surface-1);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 8px 14px;
  box-shadow: var(--shadow);
}
.mode-title-label {
  font-size: 10.5px;
  font-weight: 700;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.07em;
  text-align: center;
}
.mode-title-label:empty { display: none; }
.find { font-size: 15px; color: var(--text-secondary); text-align: center; }
.find b { color: var(--text-primary); font-weight: 700; font-size: 18px; }
.progress-track {
  height: 6px;
  border-radius: 999px;
  background: var(--eliminated-fill);
  overflow: hidden;
}
.progress-fill {
  height: 100%;
  border-radius: 999px;
  background: linear-gradient(90deg, var(--group-a), var(--group-b));
  width: 0%;
  transition: width 500ms ease;
}
/* A mode's primary in-round action, docked under the objective it acts on
   (filled by GN.hud.setMissionAction). Centered and pill-shaped so it reads
   as part of the banner, and deliberately a touch smaller/quieter than a
   .hud-btn so it stays secondary to the "Find: <country>" line above it. */
.mission-action { display: flex; justify-content: center; }
.mission-action:empty { display: none; }
button.hud-btn.mission-action-btn {
  font-size: 12.5px;
  padding: 5px 13px;
  border-radius: 999px;
}
button.hud-btn.mission-action-btn svg { color: var(--guessable); }
button.hud-btn.mission-action-btn.active svg { color: inherit; }

.hud-bottom {
  position: absolute;
  bottom: max(12px, env(safe-area-inset-bottom));
  left: max(12px, env(safe-area-inset-left));
  right: max(12px, env(safe-area-inset-right));
  flex-direction: column;
  align-items: stretch;
  gap: 8px;
}
.hud-bottom > * {
  background: var(--surface-1);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: var(--shadow);
}
.legend {
  display: flex;
  gap: 16px;
  padding: 8px 14px;
  font-size: 12px;
  color: var(--text-secondary);
  flex-wrap: wrap;
}
.legend .swatch { display: inline-block; width: 10px; height: 10px; border-radius: 3px; margin-right: 5px; vertical-align: -1px; }
.legend .a .swatch { background: var(--group-a); }
.legend .b .swatch { background: var(--group-b); }
.legend .picked .swatch { background: var(--picked); box-shadow: 0 0 0 1px color-mix(in srgb, var(--picked) 55%, #000); }
.legend .g .swatch { background: var(--guessable); }
/* Water Wisdom's two groups share one blue body and differ only in their rim
   (see .water-a / .water-b above), so their legend keys have to show a rim
   too -- a flat blue square would make both keys identical. Defined here
   rather than inline in modes/water.js so the key can't drift from the map. */
.legend .water-a-key .swatch { background: #1d4ed8; box-shadow: 0 0 0 2px #7dd3fc, 0 0 6px #38bdf8; margin-left: 2px; }
.legend .water-b-key .swatch { background: #1d4ed8; box-shadow: 0 0 0 2px #86efac, 0 0 6px #22c55e; margin-left: 2px; }
.legend .elim .swatch { background: var(--eliminated-fill); border: 1px solid var(--eliminated-stroke); }
.legend .avail .swatch { background: var(--eliminated-fill); border: 1px solid var(--eliminated-stroke); opacity: 0.85; }

.hint { padding: 6px 14px; font-size: 12.5px; color: var(--text-muted); }

.actions {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 12px;
  flex-wrap: wrap;
}
.mode-panel { display: contents; }

/* Flag Frenzy's flag, shown inline with the question in the mission banner
   rather than small in the bottom action bar — it's the whole point of the
   round, so it gets to be the visually dominant element there. */
.find-with-flag {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 16px;
  flex-wrap: wrap;
}
.find-flag-img {
  height: 68px;
  width: auto;
  border-radius: 6px;
  border: 1px solid var(--border);
  box-shadow: var(--shadow);
  display: block;
  background: var(--surface-1);
  flex: none;
}

.switch-label {
  display: flex;
  align-items: center;
  gap: 7px;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--text-secondary);
  cursor: pointer;
  user-select: none;
}
.switch-label:hover { color: var(--text-primary); }
.switch { position: relative; display: inline-flex; }
.switch input {
  position: absolute;
  opacity: 0;
  width: 30px;
  height: 17px;
  margin: 0;
  cursor: pointer;
  z-index: 1;
}
.switch-track {
  width: 30px;
  height: 17px;
  border-radius: 999px;
  background: var(--eliminated-fill);
  border: 1px solid var(--eliminated-stroke);
  display: block;
  transition: background 160ms ease;
}
.switch-thumb {
  display: block;
  width: 13px;
  height: 13px;
  margin: 1px;
  border-radius: 50%;
  background: var(--surface-1);
  box-shadow: 0 1px 2px rgba(11,11,11,0.3);
  transition: transform 160ms ease;
}
.switch input:checked ~ .switch-track { background: var(--group-a); border-color: var(--group-a); }
.switch input:checked ~ .switch-track .switch-thumb { transform: translateX(13px); }

button.hud-btn {
  font: inherit;
  font-size: var(--fs-body);
  font-weight: 600;
  min-height: var(--ctl-h);
  padding: 8px var(--ctl-pad-x);
  border-radius: 8px;
  border: 1px solid var(--border);
  background: var(--page-plane);
  color: var(--text-primary);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  white-space: nowrap;
}
button.hud-btn:hover { filter: brightness(0.97); }
@media (prefers-color-scheme: dark) { button.hud-btn:hover { filter: brightness(1.2); } }
:root[data-theme="dark"] button.hud-btn:hover { filter: brightness(1.2); }
button.hud-btn.icon-only { padding: 8px; }
button.hud-btn:disabled { cursor: default; opacity: 0.5; }
button.hud-btn#guess-btn.active {
  background: var(--guessable);
  border-color: var(--guessable);
  color: #ffffff;
}
button.hud-btn#fullscreen-btn.active { color: var(--group-a); border-color: var(--group-a); }
button.hud-btn#pole-lock-btn.active { color: var(--group-a); border-color: var(--group-a); }

/* tooltip --------------------------------------------------------------- */
.tooltip {
  position: absolute;
  pointer-events: none;
  background: var(--text-primary);
  color: var(--surface-1);
  font-size: 12.5px;
  font-weight: 600;
  padding: 5px 9px;
  border-radius: 6px;
  transform: translate(-50%, calc(-100% - 10px));
  white-space: nowrap;
  opacity: 0;
  transition: opacity 120ms ease;
  z-index: 5;
  box-shadow: var(--shadow);
}
.tooltip.show { opacity: 1; }
.tooltip::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 100%;
  transform: translateX(-50%);
  border: 5px solid transparent;
  border-top-color: var(--text-primary);
}

.toast {
  position: fixed;
  left: 50%;
  bottom: max(20px, env(safe-area-inset-bottom));
  transform: translateX(-50%) translateY(12px);
  background: var(--text-primary);
  color: var(--surface-1);
  font-size: 13px;
  font-weight: 600;
  padding: 9px 16px;
  border-radius: 999px;
  box-shadow: var(--shadow);
  opacity: 0;
  transition: opacity 220ms ease, transform 220ms ease;
  pointer-events: none;
  z-index: 20;
}
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }

/* mascot briefing bubble (engine/coach.js) --------------------------------
   Fixed rather than absolute: it is positioned from the live viewport rect of
   #mascot-dock, which lives inside the .hud-stack flex flow and therefore
   moves depending on what chrome the current mode put above it. Above the HUD
   bars (z-index 4) because it is the one thing on screen the player should
   read first, and it disappears on the first tap anywhere on it. */
.coach-bubble {
  position: fixed;
  z-index: 21;
  max-width: 340px;
  background: var(--surface-1);
  border: 1px solid var(--border);
  border-radius: 14px;
  box-shadow: var(--shadow);
  padding: 11px 35px 11px 13px; /* right side reserves room for .coach-bubble-close */
  font-size: 13px;
  line-height: 1.45;
  color: var(--text-primary);
  cursor: pointer;
  opacity: 0;
  transform: translateX(-6px) scale(0.96);
  transform-origin: left top;
  transition: opacity 200ms ease, transform 240ms cubic-bezier(.2,.9,.3,1.2);
  pointer-events: none;
}
.coach-bubble.show { opacity: 1; transform: translateX(0) scale(1); pointer-events: auto; }
.coach-bubble.coach-centered { left: 50%; top: 84px; transform: translateX(-50%) scale(0.96); }
.coach-bubble.coach-centered.show { transform: translateX(-50%) scale(1); }
.coach-bubble p { margin: 0 0 6px; }
.coach-bubble p:last-child { margin-bottom: 0; }
.coach-bubble b { color: var(--group-a); }
.coach-title {
  font-weight: 700;
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-muted);
  margin-bottom: 5px;
}
/* The speech tail, pointing back at the mascot — left-facing by default,
   flipped to point upward when the bubble has to sit underneath instead. */
.coach-bubble::before {
  content: '';
  position: absolute;
  left: -7px;
  top: 18px;
  width: 12px;
  height: 12px;
  background: var(--surface-1);
  border-left: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
  transform: rotate(45deg);
  border-radius: 0 0 0 3px;
}
.coach-bubble.coach-below::before { left: 26px; top: -7px; transform: rotate(135deg); }
/* Now that a line sits for ~15s, this is the control that matters — it is
   how you get rid of one you don't want, so it has to be findable and
   hittable rather than merely present. It carries a faint permanent
   background instead of appearing on hover: there is no hover on a phone,
   and an affordance only visible to mouse users is not an affordance. */
.coach-bubble-close {
  position: absolute;
  top: 6px;
  right: 6px;
  width: 24px;
  height: 24px;
  display: grid;
  place-items: center;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: color-mix(in srgb, var(--text-primary) 7%, transparent);
  color: var(--text-muted);
  cursor: pointer;
  transition: background 140ms ease, color 140ms ease;
}
.coach-bubble-close:hover {
  color: var(--text-primary);
  background: color-mix(in srgb, var(--text-primary) 16%, transparent);
}

/* Tutorial steps (engine/coach.js) ----------------------------------------
   The same bubble, but it waits for the player instead of timing out, so it
   needs real controls and a wider tap target than an ambient line. */
.coach-bubble.coach-tutorial { cursor: default; padding-bottom: 9px; }
.coach-step-count {
  float: right;
  font-weight: 600;
  letter-spacing: 0.04em;
  opacity: 0.75;
}
.coach-actions {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  gap: 8px;
  margin-top: 9px;
}
.coach-btn {
  border: 1px solid var(--border);
  background: var(--group-a);
  color: #fff;
  border-radius: 9px;
  padding: 5px 12px;
  font: inherit;
  font-size: 12px;
  font-weight: 650;
  line-height: 1.2;
  cursor: pointer;
  white-space: nowrap;
}
.coach-btn:hover { filter: brightness(1.08); }
/* "Skip tutorial" is deliberately the quieter of the two — always reachable,
   never the thing the eye lands on first. */
.coach-btn-quiet {
  background: transparent;
  color: var(--text-muted);
  border-color: transparent;
  font-weight: 550;
  padding: 5px 6px;
}
.coach-btn-quiet:hover { color: var(--text-primary); background: color-mix(in srgb, var(--text-primary) 9%, transparent); filter: none; }

/* Terra's personality control (engine/terra.js) -----------------------------
   Three segments in one pill, sized to sit level with the icon-only buttons
   it shares the .actions row with. */
/* The one-time pointer at .terra-bar (engine/coach.js's maybePointAtToggle).
   Positioned from the bar's live rect: it sits in the bottom actions row,
   which reflows between portrait, landscape and desktop, so a fixed corner
   would point at empty space on two of the three. */
.coach-mark {
  position: fixed;
  z-index: 22;
  transform: translate(-50%, calc(-100% - 12px)) translateY(4px);
  opacity: 0;
  pointer-events: none;
  transition: opacity 200ms ease, transform 200ms ease;
  cursor: pointer;
}
.coach-mark.show { opacity: 1; pointer-events: auto; transform: translate(-50%, calc(-100% - 12px)); }
.coach-mark-body {
  max-width: min(78vw, 290px);
  background: var(--surface-1);
  border: 1px solid color-mix(in srgb, var(--group-a) 55%, var(--border));
  border-radius: 12px;
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--group-a) 22%, transparent), var(--shadow);
  padding: 10px 12px;
  font-size: 12.5px;
  line-height: 1.45;
  color: var(--text-primary);
}
/* The arrow itself — a rotated square tucked under the card so it reads as one
   shape with it, with only the two outer edges keeping the border. */
.coach-mark-arrow {
  position: absolute;
  left: 50%;
  bottom: -6px;
  width: 11px;
  height: 11px;
  transform: translateX(-50%) rotate(45deg);
  background: var(--surface-1);
  border-right: 1px solid color-mix(in srgb, var(--group-a) 55%, var(--border));
  border-bottom: 1px solid color-mix(in srgb, var(--group-a) 55%, var(--border));
}
@media (prefers-reduced-motion: reduce) {
  .coach-mark, .coach-mark.show { transition: none; }
}

.terra-bar {
  display: inline-flex;
  align-items: stretch;
  gap: 2px;
  padding: 3px;
  min-height: var(--ctl-h);   /* level with the .hud-btn row it sits in */
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--page-plane);
}
.terra-bar-label {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 0 7px 0 5px;
  margin-right: 2px;
  border-right: 1px solid var(--border);
  color: var(--text-secondary);
  font-size: var(--fs-micro);
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  white-space: nowrap;
  flex: none;
}
.terra-bar-label svg { flex: none; }
.terra-toggle { display: inline-flex; align-items: stretch; gap: 2px; }
/* 26px segments, narrower than the 38px icon buttons beside them but still
   the pill's full height, and a mis-tap only lands on a neighbouring
   personality — the cheapest possible wrong outcome.

   On a phone this pill pushes .actions onto a second line (~54px of map).
   That was measured rather than assumed, and it is not fixable by shrinking:
   at 390px the row has about 99px of slack and four segments need ~110px
   even squeezed, so fitting would mean ~18px targets. .actions is
   flex-wrap by design, so it wraps cleanly instead. The alternative — one
   button that cycles Quiet -> Facts -> Jokes — fits in the slack, but hides
   two of the three options behind a tap, which is the worse trade for a
   control whose whole job is to advertise that Terra has personalities. */
.terra-opt,
.terra-help {
  display: grid;
  place-items: center;
  width: 26px;
  padding: 0;
  border: 0;
  border-radius: 7px;
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
  transition: background 140ms ease, color 140ms ease;
}
.terra-opt:hover,
.terra-help:hover { color: var(--text-primary); background: color-mix(in srgb, var(--text-primary) 10%, transparent); }
.terra-opt.active { background: var(--group-a); color: #fff; }
/* Hairline divider: the replay button is an action, not a fourth personality,
   so it reads as a separate thing sharing the pill rather than an option. */
.terra-help {
  display: none;
  border-left: 1px solid var(--border);
  border-radius: 0 7px 7px 0;
}
/* Only meaningful inside a mode that has a tutorial — coach.js adds .show. */
.terra-help.show { display: grid; }

/* Milestone reward ladder (engine/home.js renderRewardPath) ----------------
   A horizontally scrollable row of level nodes under the XP bar. Scrolls
   rather than wraps so the ladder always reads as one continuous path, and so
   a phone shows "there is more ahead" by the row running off the edge. */
.reward-path {
  display: flex;
  gap: 8px;
  margin-top: 12px;
  /* Top padding has to clear what .rp-node.next sticks out by: a 2px lift plus
     its 1px ring plus the glow. At the old 2px the gold frame of the very
     milestone this row exists to advertise was sliced off by overflow-y —
     which has to stay hidden, or a horizontal scroller grows a second bar. */
  padding: 12px 2px 8px;
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: thin;
  scroll-padding-left: 8px;
}
.rp-node {
  flex: none;
  min-width: 74px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: 5px 6px 5px;
  border-radius: var(--ui-radius);
  background: var(--surface-1);
  border: 1px solid var(--border);
  /* Unearned milestones are dimmed rather than hidden — the whole motivational
     value is in seeing what is still ahead. */
  opacity: 0.55;
  transition: opacity 200ms ease, border-color 200ms ease, transform 200ms ease;
}
/* The reward-effect edge. --rp-accent is set per node by engine/home.js from
   REWARD_ACCENT, so each category (title / theme / companion / background /
   border / globe) carries its own colour while the treatment stays identical —
   one recognisable "this is a prize" object, six families of it. A locked node
   keeps the accent at low strength so the category is still readable through
   the dimming; earning it turns the rim up rather than changing its colour. */
.rp-node {
  border-color: color-mix(in srgb, var(--rp-accent, var(--gold)) 30%, var(--border));
  box-shadow: inset 0 1px 0 color-mix(in srgb, var(--rp-accent, var(--gold)) 18%, transparent);
}
.rp-node.done {
  opacity: 1;
  border-color: color-mix(in srgb, var(--rp-accent, var(--good)) 62%, var(--border));
  box-shadow:
    inset 0 1px 0 color-mix(in srgb, #fff 22%, transparent),
    0 0 0 1px color-mix(in srgb, var(--rp-accent, var(--good)) 26%, transparent),
    0 0 10px -2px color-mix(in srgb, var(--rp-accent, var(--good)) 45%, transparent);
}
.rp-node.next {
  opacity: 1;
  border-color: var(--gold);
  transform: translateY(-2px);
  box-shadow:
    inset 0 1px 0 color-mix(in srgb, #fff 30%, transparent),
    0 0 0 1px color-mix(in srgb, var(--gold) 55%, transparent),
    0 0 14px -1px color-mix(in srgb, var(--gold) 55%, transparent),
    var(--shadow);
}
/* Level 100. The one node that should look like the end of the road. */
.rp-node.capstone {
  border-color: color-mix(in srgb, var(--gold) 70%, var(--border));
  background: linear-gradient(160deg, color-mix(in srgb, var(--gold) 12%, var(--surface-1)), var(--surface-1));
}
/* The prize itself, in its own inset well so the art reads as an object sitting
   in the box rather than as another icon in a row. */
.rp-art {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  border-radius: 10px;
  background: radial-gradient(circle at 50% 32%,
    color-mix(in srgb, var(--rp-accent, var(--gold)) 26%, transparent), transparent 70%),
    color-mix(in srgb, var(--page-plane) 82%, transparent);
  box-shadow:
    inset 0 0 0 1px color-mix(in srgb, var(--rp-accent, var(--gold)) 34%, transparent),
    inset 0 1px 0 color-mix(in srgb, #fff 16%, transparent);
}
.rp-node.done .rp-art, .rp-node.next .rp-art {
  box-shadow:
    inset 0 0 0 1px color-mix(in srgb, var(--rp-accent, var(--gold)) 62%, transparent),
    inset 0 1px 0 color-mix(in srgb, #fff 26%, transparent),
    0 0 9px -3px var(--rp-accent, var(--gold));
}
.rp-art-fig { display: flex; align-items: center; justify-content: center; width: 23px; height: 23px; }
.rp-art-fig svg { width: 100%; height: 100%; filter: drop-shadow(0 1px 2px rgba(0,0,0,0.5)); }
/* Two dots of the theme's own group-a/group-b, the same shorthand the shop and
   Collections already use to stand for a map theme. */
.rp-art-theme { gap: 3px; }
.rp-art-theme i {
  width: 11px; height: 11px; border-radius: 50%; display: block;
  box-shadow: inset 0 0 0 1px rgba(0,0,0,0.35), 0 0 5px -1px currentColor;
}
.rp-art-bg { width: 26px; height: 26px; border-radius: 7px; box-shadow: inset 0 0 0 1px rgba(255,255,255,0.14); }
/* The companion preview draws at its own scale; clamp it into the well. */
.rp-art-mascot .mascot-preview { width: 23px; height: 23px; display: block; }
.rp-art-mascot .mascot-preview svg { width: 100%; height: 100%; }
.rp-level {
  font-size: var(--fs-micro);
  font-weight: 700;
  letter-spacing: 0.06em;
  color: var(--text-muted);
}
.rp-node.next .rp-level { color: var(--gold-lit); }
.rp-node.done .rp-level { color: var(--good); }
/* The consumable bundle is now the supporting act — the cosmetic in the well
   above is the headline — so it drops to a size that reads as "and also
   these" without making every node taller. */
.rp-items { display: flex; gap: 2px; align-items: center; }
.rp-node .rp-item .item-art { width: 17px; height: 17px; }
.rp-node .rp-amount { font-size: 8px; right: -2px; bottom: -2px; }
.rp-item { position: relative; display: block; }
/* The shiny edge: a crisp rim highlight plus a soft drop, which is what makes
   a flat SVG read as a collectible object rather than a UI glyph. */
.rp-item .item-art {
  width: 26px;
  height: 26px;
  border-radius: 50%;
  filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.45));
}
.rp-amount {
  position: absolute;
  right: -3px;
  bottom: -3px;
  font-size: 9px;
  font-weight: 700;
  font-style: normal;
  line-height: 1;
  padding: 1px 3px;
  border-radius: 999px;
  background: var(--page-plane);
  border: 1px solid var(--border);
  color: var(--text-primary);
}
.rp-cosmetic {
  font-size: 9.5px;
  color: var(--text-muted);
  text-align: center;
  line-height: 1.15;
}

/* Earth health strip (engine/lifeline.js) ---------------------------------
   Sits directly under the mascot dock in the .hud-stack flow, so it reads as
   belonging to the companion above it rather than as another HUD gauge. */
.lifeline-strip { display: none; align-self: flex-start; }
.lifeline-strip.show { display: flex; flex-direction: column; gap: 5px; }
.ll-condition {
  display: flex;
  align-items: center;
  gap: 3px;
  background: var(--surface-1);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 5px 9px;
  box-shadow: var(--shadow);
}
.ll-pip { width: 6px; height: 6px; border-radius: 50%; background: var(--good); transition: background 260ms ease; }
.ll-pip.hurt { background: color-mix(in srgb, var(--critical) 55%, transparent); }
.ll-heal { font-size: var(--fs-micro); color: var(--text-muted); margin-left: 5px; white-space: nowrap; }
.ll-pass {
  font-size: var(--fs-micro); font-weight: 700; letter-spacing: 0.06em;
  color: var(--gold-lit); margin-left: 5px;
}
.lifeline-strip.is-broken .ll-condition { border-color: var(--critical); }

.ll-items { display: flex; gap: 5px; }
.ll-item {
  display: flex; align-items: center; gap: 3px;
  background: var(--surface-1);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 3px 8px 3px 4px;
  box-shadow: var(--shadow);
  color: var(--text-primary);
  cursor: pointer;
}
.ll-item:disabled { opacity: 0.45; cursor: default; }
.ll-item .item-art { width: 22px; height: 22px; }
.ll-count { font-size: var(--fs-micro); font-weight: 700; }

/* Reward/item art. The shiny edge is a single sweeping highlight rather than
   an animated glint — it reads as "this is a prize" at rest, which is what a
   reward on a progress path needs, without pulling the eye while playing. */
.item-art { display: block; overflow: visible; }
.item-art-shine {
  border-radius: 50%;
  box-shadow: 0 0 0 1px rgba(255,255,255,0.35), 0 2px 10px rgba(0,0,0,0.35),
              inset 0 1px 6px rgba(255,255,255,0.4);
}

/* The rest panel — reuses the shop overlay chrome so it feels like part of
   the same system rather than a bespoke interstitial. */
.lifeline-rest-card { max-width: 380px; text-align: center; }
.ll-rest-art { display: grid; place-items: center; margin-bottom: 6px; }
.ll-rest-art .item-art { width: 68px; height: 68px; filter: drop-shadow(0 4px 14px rgba(0,0,0,0.45)); }
.ll-rest-options { display: flex; flex-direction: column; gap: 8px; margin: 12px 0; }
.ll-rest-primary { background: var(--group-a); color: #fff; border-color: var(--group-a); }
.ll-rest-primary:disabled { opacity: 0.5; }
.ll-rest-sub { border-color: var(--gold); color: var(--gold-lit); }
.ll-rest-wait { font-size: var(--fs-label); color: var(--text-muted); }
.ll-rest-close { align-self: center; }

/* flying coin / XP rewards (engine/rewards.js) ----------------------------
   A single fixed, click-through layer above everything, holding only tokens
   that are mid-flight. Tokens position themselves from viewport coordinates
   and are removed on arrival, so this stays empty the rest of the time. */
.reward-layer {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 40;
  overflow: hidden;
}
.reward-token {
  position: absolute;
  width: 26px;
  height: 26px;
  margin: 0;
  display: block;
  will-change: transform, opacity;
}
.reward-token svg { width: 100%; height: 100%; display: block; }
.reward-coin svg { fill: #f0b429; stroke: #b8860b; stroke-width: 1.1; }
.reward-coin { filter: drop-shadow(0 2px 4px rgba(0,0,0,0.35)); }
.reward-xp svg { fill: var(--guessable); stroke: none; }
.reward-xp { filter: drop-shadow(0 0 5px color-mix(in srgb, var(--guessable) 70%, transparent)); }
/* Terra Patches keep engine/lifeline.js's own colours (the art is the item),
   so this only lifts them off whatever they fly over. */
.reward-patch { filter: drop-shadow(0 2px 5px rgba(0,0,0,0.4)); }

/* The chip's acknowledgement as tokens land. */
@keyframes reward-hit-pop {
  0% { transform: scale(1); }
  35% { transform: scale(1.16); }
  100% { transform: scale(1); }
}
.reward-hit { animation: reward-hit-pop 520ms cubic-bezier(.3,1.4,.5,1); }

@media (prefers-reduced-motion: reduce) {
  .coach-bubble { transition: opacity 120ms ease; transform: none !important; }
  .reward-hit { animation: none; }
}

/* win overlay ------------------------------------------------------------ */
.overlay {
  position: absolute;
  inset: 0;
  /* Below .hud's z-index (4) on purpose: the top/bottom HUD bars (Home,
     New Game, Fullscreen, mode panel) must stay clickable while a win
     screen is showing, not get trapped behind it. */
  z-index: 3;
  display: none;
  align-items: center;
  justify-content: center;
  padding: 20px;
  /* Non-blocking outside its own card (same "gaps pass clicks through"
     model as .hud/.hud-stack) -- players asked to still be able to drag
     /rotate/zoom the map and look at the result while this is showing,
     not get stuck behind a full-screen modal. */
  pointer-events: none;
}
.overlay.show { display: flex; }
.overlay-card {
  pointer-events: auto;
  position: relative;
  /* Less opaque + a lighter blur than before -- the map (now centered on
     the winning country right underneath, see modes/narrow.js's
     finishGame) should read through the card, not get fully hidden by it. */
  background: color-mix(in srgb, var(--surface-1) 78%, transparent);
  backdrop-filter: blur(5px);
  -webkit-backdrop-filter: blur(5px);
  border: 1px solid var(--border);
  border-radius: 14px;
  box-shadow: var(--shadow);
  padding: 16px 20px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 7px;
  text-align: center;
  max-width: min(88vw, 360px);
  animation: overlayIn 320ms ease;
}
@keyframes overlayIn {
  from { opacity: 0; transform: scale(0.96); }
  to { opacity: 1; transform: scale(1); }
}
/* Corner dismiss. Sits in the card's own padding rather than the content
   column, so it never pushes the medal off center. 34px box around a 14px
   glyph keeps it past the 44px-ish touch target once the card padding is
   counted, without reading as a second action next to "Play again". */
.overlay-close {
  position: absolute;
  top: 6px;
  right: 6px;
  width: 34px;
  height: 34px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 0;
  border-radius: 9px;
  background: transparent;
  color: var(--text-secondary);
  cursor: pointer;
  opacity: 0.65;
  transition: opacity 120ms ease, background 120ms ease;
}
.overlay-close:hover, .overlay-close:focus-visible { opacity: 1; background: color-mix(in srgb, var(--text-primary) 10%, transparent); }

/* Collapsed round result (engine/hud.js's X handler). The win card can simply
   be dismissed — New game and Home sit above it in the HUD, so closing it is
   never a dead end. A ROUND result is different: the mode handed hud.js an
   onNext that nothing else can reach, so throwing the card away would strand
   the run. Collapsing keeps that one button alive as a small pill at the
   bottom while giving the whole map — answer still flashed on it — back to
   the player. Above .hud-bottom so the pill never lands on the button row. */
#round-result-overlay.minimised {
  align-items: flex-end;
  padding-bottom: clamp(78px, 14vh, 132px);
  animation: none;
}
#round-result-overlay.minimised .overlay-card {
  padding: 7px 9px;
  gap: 0;
  min-width: 0;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  background: color-mix(in srgb, var(--surface-1) 94%, transparent);
}
#round-result-overlay.minimised .big,
#round-result-overlay.minimised .sub,
#round-result-overlay.minimised .overlay-close { display: none; }
.overlay .medal { width: 36px; height: 36px; color: var(--good); }
.overlay .medal .medal-x { display: none; }
.overlay.failed .medal { color: var(--critical); }
.overlay.failed .medal .medal-check { display: none; }
.overlay.failed .medal .medal-x { display: inline; }
.overlay .big { font-size: 24px; font-weight: 700; color: var(--text-primary); }
.overlay .sub { font-size: 13.5px; color: var(--text-secondary); }
.win-bonus { font-size: 13px; font-weight: 700; color: #c9902a; }
/* A real passport-collection milestone (see engine/hud.js's showWin `visa`
   param) gets its own highlighted badge instead of reading like a footnote
   in the sub text -- toggled via JS between display:flex/none, not CSS, so
   no `display` set here. */
.win-visa {
  align-items: center; gap: 8px;
  padding: 8px 14px;
  border-radius: 10px;
  font-size: 13px; font-weight: 700;
  color: var(--text-primary);
  background: color-mix(in srgb, #c9902a 14%, var(--surface-1));
  border: 1px solid color-mix(in srgb, #c9902a 45%, var(--border));
  animation: winVisaPop 420ms ease;
}
.win-visa-icon { width: 22px; height: 22px; flex: none; color: #c9902a; }
.win-visa.tier-2 { background: color-mix(in srgb, #8a90a0 16%, var(--surface-1)); border-color: color-mix(in srgb, #8a90a0 45%, var(--border)); }
.win-visa.tier-2 .win-visa-icon { color: #8a90a0; }
.win-visa.tier-3 {
  background: linear-gradient(120deg, color-mix(in srgb, #c9902a 22%, var(--surface-1)), var(--surface-1));
  border-color: #c9902a;
  box-shadow: 0 0 0 1px color-mix(in srgb, #c9902a 35%, transparent) inset, 0 2px 12px color-mix(in srgb, #c9902a 30%, transparent);
}
.win-visa.tier-3 .win-visa-icon { color: #c9902a; }
@keyframes winVisaPop {
  0% { transform: scale(0.85); opacity: 0; }
  60% { transform: scale(1.05); opacity: 1; }
  100% { transform: scale(1); opacity: 1; }
}

/* Consumables handed over by a run (engine/hud.js's showWin `items`), as art
   + count rather than a clause in the sub line. Same badge treatment as
   .win-visa above, and like it toggled between flex/none from JS, so no
   `display` here. engine/rewards.js reads .win-item's position to launch the
   tokens from the badge the player is looking at. */
.win-items { align-items: center; justify-content: center; gap: 10px; flex-wrap: wrap; }
.win-item {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 6px 13px;
  border-radius: 10px;
  background: color-mix(in srgb, var(--good) 14%, var(--surface-1));
  border: 1px solid color-mix(in srgb, var(--good) 45%, var(--border));
  animation: winVisaPop 420ms ease;
}
.win-item .item-art { width: 28px; height: 28px; flex: none; }
.win-item-count { font-size: 15px; font-weight: 800; color: var(--text-primary); }
.overlay button.hud-btn { margin-top: 6px; background: var(--surface-1); }
#round-result-overlay.correct .big { color: var(--good); }
#round-result-overlay.wrong .big { color: var(--critical); }

/* shop overlay ------------------------------------------------------------ */
.shop-overlay {
  position: absolute;
  inset: 0;
  z-index: 6;
  background: color-mix(in srgb, var(--surface-1) 82%, transparent);
  display: none;
  align-items: center;
  justify-content: center;
  padding: 20px;
}
.shop-overlay.show { display: flex; animation: overlayIn 320ms ease; }
.shop-card {
  width: min(92vw, 480px);
  max-height: min(86vh, 640px);
  overflow-y: auto;
  background: var(--page-plane);
  border: 1px solid var(--border);
  border-radius: 16px;
  box-shadow: var(--shadow);
  padding: 18px 20px 22px;
  display: flex;
  flex-direction: column;
  gap: 16px;
}
.shop-header { display: flex; align-items: center; gap: 10px; }
.shop-header h2 { flex: 1; margin: 0; font-size: 18px; font-weight: 800; color: var(--text-primary); }
.shop-balance { display: flex; align-items: center; gap: 6px; font-weight: 700; font-size: 14px; color: #c9902a; }
.shop-section h3 { margin: 0 0 4px; font-size: 13.5px; font-weight: 700; color: var(--text-primary); }
.shop-hint { margin: 0 0 10px; font-size: var(--fs-label); color: var(--text-muted); }

/* Category tabs. Same component as the Collections screen's tab row, just
   in the shop's accent — one shop is four small catalogs, not one long
   scroll, and the tab row is what makes that structure visible. */
.shop-tab-row { display: flex; gap: 6px; flex-wrap: wrap; }
.shop-tab {
  font: inherit; font-size: var(--fs-label); font-weight: 700;
  padding: 7px 12px; border-radius: 999px;
  border: 1px solid var(--border); background: var(--surface-1);
  color: var(--text-secondary); cursor: pointer;
}
.shop-tab.active { background: #c9902a; border-color: #c9902a; color: #ffffff; }
.shop-body { display: flex; flex-direction: column; }

.shop-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(136px, 1fr));
  gap: 10px;
}

/* The slot sub-tabs (Hats / Glasses / Beards / Auras) sit between the hint and
   the card grid, which is a flex column with no gap — so the pills' borders
   ran straight into the first card row's borders. Centered over the grid and
   pulled up a touch, with real breathing room underneath. Scoped to the shop:
   the Collections wardrobe lays its tabs out inside a grid, left-aligned. */
.shop-body .wardrobe-tabs {
  justify-content: center;
  margin: -4px 0 12px;
}

/* One card shape for every purchasable thing — accessory, theme, potion.
   Media on top, name + (i), then the price/action foot pinned to the
   bottom so the buttons line up across a row of uneven cards. */
.shop-item {
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  padding: 11px 10px 10px;
  border-radius: 12px;
  border: 1px solid var(--border);
  background: var(--surface-1);
  text-align: center;
}
.shop-item.equipped { border-color: var(--group-a); }
.shop-item-media { display: flex; align-items: center; justify-content: center; height: 62px; }
.shop-item-head { display: flex; align-items: center; justify-content: center; gap: 5px; }
.shop-item-name { font-size: var(--fs-label); font-weight: 700; color: var(--text-primary); line-height: 1.2; }
.shop-item-meta { font-size: var(--fs-micro); color: var(--text-muted); }
.shop-info-btn {
  flex: none; width: 17px; height: 17px; border-radius: 50%;
  border: 1px solid var(--border); background: var(--page-plane);
  color: var(--text-muted); font: inherit; font-size: 10px; font-weight: 800; font-style: italic;
  line-height: 1; cursor: pointer; padding: 0;
  display: flex; align-items: center; justify-content: center;
}
.shop-info-btn:hover { color: var(--text-primary); border-color: var(--text-muted); }
/* Collapsed until the (i) is tapped — same interaction as the Home tiles. */
.shop-item-desc { display: none; font-size: var(--fs-micro); color: var(--text-secondary); line-height: 1.45; }
.shop-item.expanded .shop-item-desc { display: block; }
.shop-item.expanded .shop-info-btn { color: #ffffff; background: var(--group-a); border-color: var(--group-a); }
.shop-item-foot { margin-top: auto; padding-top: 4px; display: flex; align-items: center; justify-content: center; gap: 6px; flex-wrap: wrap; }
.shop-price { display: inline-flex; align-items: center; gap: 3px; font-size: var(--fs-micro); font-weight: 800; color: #c9902a; }
.shop-price.short { color: var(--text-muted); } /* can't afford it yet — stated, not shouted */
.shop-locked { font-size: var(--fs-micro); font-weight: 700; color: var(--text-muted); }

/* Companion preview: the actual face + accessory, drawn from the same paths
   engine/mascot.js puts on the live globe. */
/* Taller than wide: the viewBox now reserves room above the head for hats
   (see engine/mascot.js PREVIEW_VIEWBOX), so a square box would squash them. */
/* Sized to the PREVIEW_VIEWBOX aspect (100 x 128) so the head fills the box —
   the box grew when the viewBox did, to make room for a wizard beard hanging
   below the chin and a traffic cone standing above the crown. */
.mascot-preview { display: block; width: 68px; height: 90px; }
.mascot-preview svg { width: 100%; height: 100%; display: block; overflow: visible; }
/* Wardrobe: a "wearing now" composite, then one tab per slot. The composite
   is the proof that slots stack — hat AND beard AND glasses AND aura in a
   single picture — which a grid of single-item cards can never show. */
.wardrobe-worn {
  grid-column: 1 / -1;
  display: flex; align-items: center; gap: 14px;
  background: var(--surface-1); border: 1px solid var(--border);
  border-radius: 12px; padding: 10px 14px;
}
.wardrobe-worn .mascot-preview { width: 74px; height: 98px; flex: none; }
.wardrobe-worn-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.wardrobe-worn-text b { font-size: 12px; text-transform: uppercase; letter-spacing: 0.07em; color: var(--text-muted); }
.wardrobe-worn-text span { font-size: 13.5px; font-weight: 700; color: var(--text-primary); }

.wardrobe-tabs { grid-column: 1 / -1; display: flex; gap: 8px; flex-wrap: wrap; }
.wardrobe-tab {
  display: flex; align-items: center; gap: 6px;
  font: inherit; font-size: 12.5px; font-weight: 700;
  padding: 7px 13px; border-radius: 999px; cursor: pointer;
  background: var(--surface-1); border: 1px solid var(--border); color: var(--text-secondary);
}
.wardrobe-tab.active { background: var(--group-a); border-color: var(--group-a); color: #fff; }
.wardrobe-tab-count {
  font-size: 11px; font-weight: 800; opacity: 0.75;
  background: rgba(0, 0, 0, 0.22); border-radius: 999px; padding: 1px 6px;
}
.wardrobe-grid {
  grid-column: 1 / -1;
  display: grid; gap: 10px;
  grid-template-columns: repeat(auto-fill, minmax(132px, 1fr));
}

.mp-globe { fill: #16283f; stroke: rgba(255,255,255,0.18); stroke-width: 1.2; }
.mp-eye { fill: #10161f; }
.mp-mouth { fill: #10161f; }

/* The shop's item-only preview: the goods, with no companion under them. Wider
   than tall because most of these are hats and glasses, and framed to the
   item's own bounding box by engine/mascot.js — so a hat here is roughly twice
   the size it is on the 68px worn preview, which is the point. */
.mascot-item-preview { display: block; width: 78px; height: 60px; }
.mascot-item-preview svg { width: 100%; height: 100%; display: block; overflow: visible; }
/* A hairline light outline on every piece, only here. Off the companion there
   is no lit globe behind the item, so the near-black ones — the eye patch, the
   pixel shades, the tricorn, the top hat — were a black shape on a black card.
   non-scaling-stroke keeps it 1px whatever zoom the item's own bounding box
   ends up framed at, which varies wildly between a monocle and a viking helm. */
.mascot-item-preview .mascot-accessory path {
  stroke: rgba(255, 255, 255, 0.32);
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
  stroke-linejoin: round;
}
/* An aura has nothing to hold up on its own, so its card keeps a bare disc for
   the halo to surround — square, since the halo is. */
.mascot-item-preview.aura-item { width: 62px; height: 60px; }
/* The empty-slot card. Deliberately drawn, not a blank space: "nothing" has to
   look like a choice you can make, not like a card that failed to load. */
.mascot-item-preview.none-item { width: 56px; height: 56px; }
.none-ring { fill: none; stroke: var(--text-muted); stroke-width: 2.4; stroke-dasharray: 5 4.5; opacity: 0.65; }
.none-slash { stroke: var(--text-muted); stroke-width: 2.4; stroke-linecap: round; opacity: 0.65; }
.wardrobe-card.wardrobe-none-on { border-color: var(--group-a); }

.shop-util-icon {
  display: flex; align-items: center; justify-content: center;
  width: 50px; height: 50px; border-radius: 13px;
}
.shop-util-icon svg { width: 26px; height: 26px; }
.util-shield { color: var(--group-a); background: color-mix(in srgb, var(--group-a) 14%, transparent); }
.util-repair { color: var(--good); background: color-mix(in srgb, var(--good) 14%, transparent); }
.util-potion { color: var(--guessable); background: color-mix(in srgb, var(--guessable) 16%, transparent); }

.wardrobe-card .mascot-preview { width: 56px; height: 74px; margin: 0 auto 2px; }
.wardrobe-card .mascot-item-preview { margin: 0 auto 2px; }
.theme-swatch { display: flex; gap: 3px; }
.theme-swatch span { width: 16px; height: 16px; border-radius: 50%; display: block; box-shadow: 0 0 0 1px var(--border) inset; }
/* color is set inline per swatch (each theme's own hex, via shop.js) so the
   glow always matches that swatch's actual fill, not whichever theme is
   currently equipped app-wide. */
.theme-swatch-glow span, .theme-swatch-dino span { box-shadow: 0 0 0 1px var(--border) inset, 0 0 5px 1px color-mix(in srgb, currentColor 60%, transparent); }
.theme-swatch-scifi span { box-shadow: 0 0 0 1px var(--border) inset, 0 0 6px 2px currentColor; animation: scifiPulse 3.2s ease-in-out infinite; }
.theme-swatch-scifi span:last-child { animation-delay: 1.6s; }
.theme-swatch-nightlights span { box-shadow: 0 0 0 1px var(--border) inset, 0 0 7px 2px #ffb545; }
.theme-swatch-blueprint span { border-radius: 3px; box-shadow: 0 0 0 1px #4fd2ff inset; }
.theme-swatch-basketball span { box-shadow: 0 0 0 1.5px #2b1508 inset; }
.theme-swatch-vintage span { box-shadow: 0 0 0 1px #a68a55 inset; }
.theme-swatch-pineapple span { box-shadow: 0 0 0 1px var(--border) inset, 0 0 0 2px rgba(255,255,255,0.15); }
.bg-theme-swatch {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
  width: 100%;
  height: 34px;
  border-radius: 8px;
  box-shadow: 0 0 0 1px var(--border) inset;
}
.bg-theme-swatch span { width: 14px; height: 14px; border-radius: 50%; box-shadow: 0 0 0 1px rgba(255,255,255,0.18) inset; }
/* Light and Dark carry no palette of their own (they're luminance modes, not
   repaints), so their swatches are drawn from fixed values here rather than
   from a palette object that doesn't exist. */
.bg-theme-swatch-light { background: #e9e8e3; }
.bg-theme-swatch-light span:first-child { background: #ffffff; }
.bg-theme-swatch-light span:last-child { background: #b9c3cf; }
.bg-theme-swatch-dark { background: #0d0d0d; }
.bg-theme-swatch-dark span:first-child { background: #1a1a19; }
.bg-theme-swatch-dark span:last-child { background: #14232f; }
.theme-label { font-size: 12.5px; font-weight: 700; color: var(--text-primary); }
.shop-equipped { font-size: 11px; font-weight: 700; color: var(--good); }
.shop-btn { font-size: 12px; padding: 6px 12px; }
.shop-boost-active { color: var(--guessable); font-weight: 700; }

/* account overlay (reuses .shop-overlay/.shop-card/.shop-header/.shop-hint) */
.account-provider-btn {
  width: 100%;
  justify-content: center;
  background: #ffffff;
  color: #1f1f1f;
  border-color: #d5d5d5;
}
.account-provider-btn:hover { filter: brightness(0.97); }
.account-email-toggle-btn { width: 100%; justify-content: center; background: transparent; }
.account-danger-btn { color: var(--critical); border-color: var(--critical); }
.account-danger-btn:hover { background: color-mix(in srgb, var(--critical) 10%, var(--surface-1)); }

/* home screen ------------------------------------------------------------- */
.board {
  transition: opacity 260ms ease;
}
.board.hidden { opacity: 0; pointer-events: none; }

.home-screen {
  position: absolute;
  inset: 0;
  background: var(--page-plane);
  display: flex;
  flex-direction: column;
  overflow: hidden;
  z-index: 2;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity 260ms ease;
}
.home-screen.show { opacity: 1; visibility: visible; pointer-events: auto; }

/* compact top bar: a slim branded strip, not a scroll-costing hero section.
   The small circular "mini-globe" frame keeps a dark backdrop just around
   the spinning globe (so it still reads clearly at small size) without
   spending the whole screen's height on it. */
.home-topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  /* Extra headroom, not symmetric padding: the brand globe is the companion
     with its loadout on, and the tall hats (wizard, chef's toque, traffic
     cone, viking horns) rise ~11px above the 46px frame at this size — see
     the anchor math in engine/mascot.js. .home-screen clips at overflow
     hidden, so without the head start the point of the hat gets shaved off. */
  padding: 17px clamp(16px, 3vw, 48px) 12px;
  background: var(--surface-1);
  border-bottom: 1px solid var(--border);
}
.home-brand { display: flex; align-items: center; gap: 12px; min-width: 0; }
.home-topbar-right { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.mini-globe-frame {
  position: relative;
  width: 46px;
  height: 46px;
  flex: none;
  border-radius: 50%;
  overflow: hidden;
  background: radial-gradient(circle at 35% 30%, var(--hero-bg-1, #1c3a5e) 0%, var(--hero-bg-2, #060f1c) 85%);
  box-shadow: 0 1px 3px rgba(11, 11, 11, 0.25);
}
.mini-globe-frame svg { width: 100%; height: 100%; display: block; }
.hero-sphere-outline { fill: var(--hero-sphere, #16283f); stroke: rgba(255, 255, 255, 0.18); stroke-width: 1; }
.hero-graticule { fill: none; stroke: rgba(255, 255, 255, 0.13); stroke-width: 0.5; }
.hero-land { fill: var(--hero-land, #4f8fc7); stroke: rgba(255, 255, 255, 0.16); stroke-width: 0.4; }
/* Level-earned globe skins (GN.progression.GLOBE_SKINS) -- applied as a
   class on .mini-globe-frame from engine/home.js's renderLevelBadge(); no
   'classic' rule needed, the var() fallbacks above already are that skin. */
.mini-globe-frame.skin-bronze-globe { --hero-bg-1: #4a3016; --hero-bg-2: #1a0f04; --hero-sphere: #6b4a1f; --hero-land: #c98a3d; }
.mini-globe-frame.skin-silver-globe { --hero-bg-1: #3a3f47; --hero-bg-2: #14171b; --hero-sphere: #5a626d; --hero-land: #c7ced6; }
.mini-globe-frame.skin-gold-globe { --hero-bg-1: #4a3c0a; --hero-bg-2: #1a1502; --hero-sphere: #6b5814; --hero-land: #ffd75e; }

.home-brand-text { display: flex; flex-direction: column; min-width: 0; }
.home-brand-text h1 { font-size: 18px; font-weight: 800; margin: 0; letter-spacing: -0.01em; color: var(--text-primary); }
.home-tagline { font-size: 11.5px; color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* Home is a scrolling page of two labelled groups — "Games" (the 8 modes)
   and "Explore & collect" (the 5 destinations) — over a docked XP footer.
   It used to be a flex column that stretched both tile grids to exactly
   fill the viewport, which made every tile's height a function of how many
   tiles existed and how tall the window happened to be: unpredictable, and
   worst on a phone. Tiles now have their own natural height and the page
   scrolls, which is what a phone reader expects anyway. */
.home-inner {
  flex: 1;
  min-height: 0; /* required for a flex child to actually shrink/scroll instead of overflowing its parent */
  display: flex;
  flex-direction: column;
  gap: 20px;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
  max-width: 1100px;
  width: 100%;
  margin: 0 auto;
  padding: 14px clamp(14px, 3vw, 40px) 18px;
}
/* flex: 1 0 auto — grow to share whatever height is left over, but NEVER
   shrink. That combination is what makes Home fill a phone screen without
   reintroducing the overlap bug the old fill-the-viewport layout had: when
   the content is shorter than the screen the sections stretch to absorb the
   slack, and when it is taller they keep their natural size and .home-inner
   scrolls instead of squashing rows into each other. */
.home-section { display: flex; flex-direction: column; gap: 10px; flex: 1 0 auto; }
/* Share the leftover height in proportion to how many tiles each group has,
   so a row in the 8-tile group ends up about as tall as a row in the 5-tile
   one instead of the smaller group getting conspicuously fatter tiles.
   Browsers without :has() simply split it evenly — a cosmetic difference. */
.home-section:has(.mode-grid) { flex-grow: 8; }
.home-section:has(.callout-grid) { flex-grow: 5; }
/* Groups need to read as separate places, not one long list of tiles. Three
   things do that together: a rule and a generous gap above the second group,
   a colored tick keyed to that group (blue for games, gold for the rest),
   and a heading strong enough to actually own the tiles beneath it. */
.home-section + .home-section { border-top: 1px solid var(--border); padding-top: 22px; }
.home-section-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; padding: 0 2px; }
.home-section-titles { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.home-section-head h2 {
  margin: 0; font-size: 15px; font-weight: 800; letter-spacing: 0.07em;
  text-transform: uppercase; color: var(--text-primary);
  display: flex; align-items: center; gap: 8px;
}
.home-section-head h2::before {
  content: '';
  width: 4px; height: 17px; border-radius: 2px;
  background: var(--group-a);
  flex: none;
}
.home-section:has(.callout-grid) .home-section-head h2::before { background: var(--gold); }
/* Indented to the heading's text, not the tick, so the two lines share a
   left edge and read as one title block. */
.home-section-sub {
  margin: 0; padding-left: 12px;
  font-size: 11.5px; line-height: 1.35; color: var(--text-muted);
}
.home-section-note { font-size: 12px; color: var(--text-muted); flex: none; padding-top: 3px; }

/* Equal-width columns, centered: the three tiers are one choice of three
   equivalent options, and a flex row sized them by label length instead
   ("Easy" narrow, "Medium" wide), which read as a ranking. grid-auto-flow
   keeps that true for any number of tiers. */
.tier-picker {
  flex: none;
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  gap: 8px;
  width: 100%;
  max-width: 560px;
  margin: 0 auto;
}

/* Unified "app launcher" menu: the 5 destinations and the 8 game modes both
   use this same tile shape (icon + title, description collapsed behind an
   (i) button) so the whole Home screen reads as one clear, static menu
   rather than a mix of big description cards. Rows are content-sized — the
   grid no longer stretches to fill the viewport (see .home-inner). */
.menu-grid {
  display: grid;
  /* auto-fill spends spare width on MORE columns, never wider ones, so a
     hardcoded 150px floor made a 1440px browser show 6 columns of 162px —
     narrower tiles than a 390px phone gets, and the reason single words broke
     mid-word on the widest screens. The floor is a token now, stepped per
     size class below. */
  grid-template-columns: repeat(auto-fill, minmax(var(--tile-col), 1fr));
  /* minmax(min-content, 1fr): spare height in the section is shared out
     between the rows, and a row can never be shorter than what is inside it
     — so a title that wraps to two lines pushes its row taller instead of
     being clipped. The comfortable-tap floor lives on .menu-tile's
     min-height, which min-content then picks up. */
  grid-auto-rows: minmax(min-content, 1fr);
  gap: var(--ui-gap);
  flex: 1 0 auto;
}
@keyframes cardIn {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: translateY(0); }
}
.menu-tile {
  display: flex;
  flex-direction: column;
  justify-content: center;
  min-height: var(--tile-min-h);
  gap: 4px;
  text-align: left;
  font: inherit;
  background: var(--surface-1);
  border: 1px solid var(--border);
  animation: cardIn 380ms ease both;
  border-radius: var(--ui-radius);
  padding: var(--tile-pad-y) var(--tile-pad-x);
  box-shadow: var(--shadow);
  cursor: pointer;
  color: var(--text-primary);
  transition: transform 140ms ease, box-shadow 140ms ease;
}
.menu-tile:hover { transform: translateY(-2px); box-shadow: 0 4px 14px rgba(11,11,11,0.12); }
/* One row: icon, title, (i) button on the right. The room for a long
   single-word title like "Multiplayer" comes from the pieces around it
   being small — 22px icon, 7px gaps, 20px button — not from moving the
   button out of the line. */
.tile-main { display: flex; align-items: center; gap: 7px; }
.tile-icon, .mode-icon { width: var(--icon-size); height: var(--icon-size); flex: none; color: var(--group-a); }
.tile-title {
  flex: 1; min-width: 0;
  font-size: var(--fs-title); font-weight: 700; color: var(--text-primary); line-height: 1.2;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
  /* Last-resort only: multi-word titles wrap at their spaces. This just
     guarantees an over-long single word breaks onto the second line instead
     of being sliced mid-glyph by the clamp's overflow. */
  overflow-wrap: break-word;
}
.tile-status { font-size: var(--fs-label); color: var(--text-secondary); }
.tile-status:empty { display: none; }
/* Collapsed by default -- toggled via engine/home.js's delegated
   .tile-info-btn listener, which adds/removes .expanded on the tile. */
.tile-desc { display: none; font-size: var(--fs-body); color: var(--text-secondary); line-height: 1.4; margin-top: 2px; }
.menu-tile.expanded .tile-desc { display: block; }
.tile-info-btn {
  flex: none;
  width: 21px; height: 21px; border-radius: 50%;
  border: 1px solid var(--border); background: var(--page-plane);
  color: var(--text-muted); font: inherit; font-size: 12px; font-weight: 800; font-style: italic;
  line-height: 1; cursor: pointer; padding: 0;
  display: flex; align-items: center; justify-content: center;
}
.tile-info-btn:hover { color: var(--text-primary); border-color: var(--text-muted); }
.menu-tile.expanded .tile-info-btn { color: #ffffff; background: var(--group-a); border-color: var(--group-a); }
/* Accent colors -- each callout keeps its previous distinct identity,
   now just applied to the icon/info-button instead of a big gradient
   background, matching the mode cards' already-compact look. */
.accent-a .tile-icon { color: var(--group-a); }
.accent-guessable .tile-icon { color: var(--guessable); }
.accent-good .tile-icon { color: var(--good); }
.accent-gold .tile-icon { color: #c9902a; }
.accent-violet .tile-icon { color: #7c5cff; }
.accent-coin .tile-icon { color: #d9861f; }
.tier-chip {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  gap: 1px;
  min-width: 0;
  min-height: var(--ctl-h);
  font: inherit;
  padding: 8px 10px;
  border-radius: 14px;
  border: 1px solid var(--border);
  background: var(--surface-1);
  color: var(--text-secondary);
  cursor: pointer;
  text-align: center;
}
.tier-chip-label { font-size: var(--fs-title); font-weight: 700; line-height: 1.15; }
/* The XP bonus is shown on every tier, locked or not — it is the reason to
   climb, so hiding it until you have already climbed is backwards. */
.tier-chip-caption { font-size: var(--fs-micro); font-weight: 800; color: var(--group-a); letter-spacing: 0.01em; }
.tier-chip-lock { font-size: var(--fs-micro); font-weight: 700; color: var(--text-muted); }
.tier-chip-lock::before { content: '🔒 '; font-size: 0.9em; }
.tier-chip.active { background: var(--group-a); border-color: var(--group-a); color: #ffffff; }
.tier-chip.active .tier-chip-caption,
.tier-chip.active .tier-chip-lock { color: rgba(255,255,255,0.88); }
/* Locked stays clickable (see engine/home.js) so tapping it explains why via
   a toast, rather than a native-disabled button that just does nothing. */
.tier-chip.locked { opacity: 0.66; }
.tier-chip.locked .tier-chip-caption { color: var(--text-muted); }

/* Passport counter — the collection's headline number, on Home rather than
   only behind the Collections screen. Deliberately one slim strip: gold
   accent so it's unmistakably the collectible, but a fraction of a tile's
   height so it motivates without competing with the games above it. */
.passport-strip {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  font: inherit;
  text-align: left;
  cursor: pointer;
  color: var(--text-primary);
  background: linear-gradient(100deg, color-mix(in srgb, #c9902a 13%, var(--surface-1)), var(--surface-1) 62%);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 9px 13px;
  transition: transform 140ms ease;
}
.passport-strip:hover { transform: translateY(-1px); }
.passport-strip.pending { opacity: 0.55; } /* map data still loading — the total isn't known yet */
.passport-badge {
  width: 34px; height: 34px; flex: none;
  display: flex; align-items: center; justify-content: center;
  border-radius: 9px;
  color: #c9902a;
  background: color-mix(in srgb, #c9902a 14%, transparent);
}
.passport-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 5px; }
.passport-count { font-size: 12.5px; color: var(--text-secondary); }
.passport-count b { font-size: 16.5px; font-weight: 800; color: var(--text-primary); }
.passport-track { display: block; height: 5px; border-radius: 999px; background: var(--eliminated-fill); overflow: hidden; }
.passport-fill {
  display: block; height: 100%; width: 0%; border-radius: 999px;
  background: linear-gradient(90deg, #c9902a, #e6b954);
  transition: width 600ms ease;
}
.passport-note { flex: none; font-size: 12.5px; font-weight: 800; color: #c9902a; }
.passport-strip.empty .passport-note { color: var(--text-muted); font-weight: 700; }

/* Level/XP footer — the long-horizon motivator, so it carries a bit more
   weight than the rest of the chrome without becoming a banner again.
   The track spans a whole 5-level BLOCK (L6 -> L10): the tick marks are the
   levels inside it and the gold node at the end is the reward milestone, so
   one bar answers both "how far to my next level" and "how far to my next
   reward". Gold is used only on the milestone node and the track's edge as
   it fills — an accent that says "prize", not a gold bar. */
@property --p { syntax: '<number>'; inherits: false; initial-value: 0; }
.home-footer {
  flex: none;
  display: flex;
  align-items: center;
  gap: 13px;
  padding: 11px clamp(14px, 3vw, 40px);
  padding-bottom: max(11px, env(safe-area-inset-bottom));
  background: linear-gradient(180deg, color-mix(in srgb, var(--gold) 5%, var(--surface-1)), var(--surface-1));
  border-top: 1px solid var(--border);
}
.xp-ring {
  --p: 0;
  position: relative;
  width: 46px; height: 46px; flex: none;
  border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: conic-gradient(var(--guessable) calc(var(--p) * 1%), var(--eliminated-fill) 0);
  transition: --p 700ms cubic-bezier(0.22, 0.9, 0.28, 1);
}
.xp-ring::before {
  content: '';
  position: absolute; inset: 3.5px;
  border-radius: 50%;
  background: var(--surface-1);
}
.xp-ring-num { position: relative; font-size: 17px; font-weight: 800; color: var(--text-primary); line-height: 1; }
.xp-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 6px; }
.xp-row { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
.xp-label-main { font-size: var(--fs-label); font-weight: 700; color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* What the next milestone hands over — the reason the gold node matters. */
.xp-reward { flex: none; display: flex; align-items: baseline; gap: 5px; font-size: var(--fs-micro); color: var(--text-muted); white-space: nowrap; }
.xp-reward b { font-weight: 800; color: var(--gold); }
.xp-reward-away { color: var(--text-muted); }

.xp-journey { display: flex; align-items: center; gap: 8px; }
.xp-track-lg {
  position: relative; flex: 1; min-width: 0;
  height: var(--bar-h); border-radius: 999px;
  background: var(--eliminated-fill);
  overflow: hidden;
}
.xp-fill-lg {
  position: relative; height: 100%; width: 0%;
  border-radius: 999px;
  background: linear-gradient(90deg, var(--group-a), var(--guessable));
  transition: width 700ms cubic-bezier(0.22, 0.9, 0.28, 1);
}
/* Lit leading edge — "you are here", and the one warm highlight on the bar. */
.xp-fill-head {
  position: absolute; top: 0; right: 0; bottom: 0;
  width: 10px; border-radius: 999px;
  background: linear-gradient(90deg, transparent, color-mix(in srgb, var(--gold-lit) 75%, transparent));
}
/* Level boundaries inside the block. Hairlines, not dividers: they should be
   readable as steps without chopping the bar into segments. */
.xp-ticks { position: absolute; inset: 0; pointer-events: none; }
.xp-tick {
  position: absolute; top: 0; bottom: 0; width: 2px;
  transform: translateX(-1px);
  background: color-mix(in srgb, var(--page-plane) 70%, transparent);
}
.xp-tick.passed { background: color-mix(in srgb, #ffffff 34%, transparent); }

/* The milestone node. Gold, but small and outside the track, so it reads as
   a destination marker rather than making the whole footer gold. */
.xp-goal {
  flex: none;
  display: flex; align-items: center; gap: 3px;
  padding: 3px 8px 3px 6px;
  border-radius: 999px;
  font-size: var(--fs-micro); font-weight: 800;
  color: var(--text-muted);
  border: 1px solid var(--border);
  background: var(--page-plane);
}
.xp-goal svg { width: 11px; height: 11px; }
.xp-goal.reached {
  color: #2a1c05;
  border-color: color-mix(in srgb, var(--gold) 70%, transparent);
  background: linear-gradient(145deg, var(--gold-lit), var(--gold));
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--gold-lit) 45%, transparent), 0 1px 6px color-mix(in srgb, var(--gold) 35%, transparent);
}
@keyframes xpSweep { from { transform: translateX(-120%); } to { transform: translateX(420%); } }
@keyframes xpPop { 0% { transform: scale(1); } 40% { transform: scale(1.13); } 100% { transform: scale(1); } }
.home-footer.gained .xp-track-lg::after {
  content: '';
  position: absolute; inset: 0; width: 28%;
  background: linear-gradient(90deg, transparent, color-mix(in srgb, var(--gold-lit) 60%, transparent), transparent);
  animation: xpSweep 900ms ease-out;
}
.home-footer.leveled .xp-ring { animation: xpPop 700ms ease; }
@media (prefers-reduced-motion: reduce) {
  .xp-ring, .xp-fill-lg, .passport-fill { transition: none; }
  .home-footer.gained .xp-track-lg::after, .home-footer.leveled .xp-ring { animation: none; }
}

.bg-picker { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.bg-chip-row { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.bg-chip {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1px;
  font: inherit;
  font-size: 12px;
  font-weight: 700;
  padding: 6px 12px;
  border-radius: 12px;
  border: 1px solid var(--border);
  background: var(--surface-1);
  color: var(--text-secondary);
  cursor: pointer;
}
.bg-chip.active { background: var(--group-a); border-color: var(--group-a); color: #ffffff; }
.bg-chip.locked { opacity: 0.65; }
.bg-chip.locked .tier-chip-caption { color: var(--critical); }

/* multiplayer -------------------------------------------------------------- */
.multiplayer-screen {
  position: absolute;
  inset: 0;
  background: var(--page-plane);
  overflow-y: auto;
  z-index: 2;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity 260ms ease;
}
.multiplayer-screen.show { opacity: 1; visibility: visible; pointer-events: auto; }
.mp-topbar {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px clamp(16px, 3vw, 48px);
  background: var(--surface-1);
  border-bottom: 1px solid var(--border);
}
.mp-topbar h1 { font-size: 16px; font-weight: 800; margin: 0; color: var(--text-primary); }
.mp-inner { max-width: 480px; margin: 0 auto; padding: 32px 20px; }

.mp-menu { display: flex; flex-direction: column; gap: 12px; }
.mp-big-btn { padding: 16px; font-size: 15px; justify-content: center; }

.mp-form { display: flex; flex-direction: column; gap: 14px; }
.mp-form label { display: flex; flex-direction: column; gap: 6px; font-size: 12.5px; font-weight: 700; color: var(--text-secondary); }
.mp-form input, .mp-form select {
  font: inherit;
  font-size: 14px;
  padding: 10px 12px;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: var(--surface-1);
  color: var(--text-primary);
}
.mp-form-actions { display: flex; justify-content: space-between; gap: 10px; }

.mp-room-code { font-size: 20px; font-weight: 800; color: var(--text-primary); text-align: center; margin-bottom: 4px; }
.mp-lobby { display: flex; flex-direction: column; gap: 10px; }
.mp-lobby-players { display: flex; flex-direction: column; gap: 6px; margin: 6px 0; }
.mp-lobby-player {
  padding: 9px 12px;
  border-radius: 8px;
  background: var(--surface-1);
  border: 1px solid var(--border);
  font-weight: 700;
  font-size: 13.5px;
  color: var(--text-primary);
}

.mp-results { display: flex; flex-direction: column; gap: 10px; }
.mp-results h2 { margin: 0 0 4px; font-size: 18px; color: var(--text-primary); }
.mp-result-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  border-radius: 8px;
  background: var(--surface-1);
  border: 1px solid var(--border);
}
.mp-result-row.winner { border-color: var(--good); background: color-mix(in srgb, var(--good) 10%, var(--surface-1)); }
.mp-result-rank { font-weight: 800; color: var(--text-muted); width: 28px; font-size: 15px; }
.mp-result-name { flex: 1; font-weight: 700; color: var(--text-primary); }
.mp-result-score { font-weight: 800; color: var(--guessable); font-variant-numeric: tabular-nums; }

/* Nameplate flair (GN.progression's LEVEL_TITLES/BORDERS, level-earned) --
   applied via modes/multiplayer.js's nameplateHtml()/borderClass(). An
   inset ring works consistently whether the row already has its own full
   border (.mp-lobby-player/.mp-result-row) or just a border-top
   (.mp-player-row, a compact list style, not a bordered card). */
.mp-title { font-size: 10.5px; font-weight: 700; color: #c9902a; margin-left: 4px; }
.border-bronze { box-shadow: inset 0 0 0 2px #c98a3d; }
.border-silver { box-shadow: inset 0 0 0 2px #b9c2cc; }
.border-gold { box-shadow: inset 0 0 0 2px #e0b83a; }

/* between-rounds intermission panel --------------------------------------
   Deliberately NOT a full-board .overlay: players asked to keep observing
   (and zooming/panning) the map between rounds, so this docks to the right
   inside .hud-stack exactly like .mp-scoreboard — same "gaps pass clicks
   through to the map" pointer-events model, just a taller panel. */
.mp-round-summary-panel {
  display: none;
  width: 250px;
  max-width: 100%;
  max-height: min(60vh, 460px);
  overflow-y: auto;
  align-self: flex-end;
  /* Translucent + blurred rather than a solid opaque card — the map (and
     the highlighted country on it) should stay the visually dominant thing
     during the intermission, this is a supporting readout beside it. */
  background: color-mix(in srgb, var(--page-plane) 82%, transparent);
  backdrop-filter: blur(7px);
  -webkit-backdrop-filter: blur(7px);
  border: 1px solid var(--border);
  border-radius: 12px;
  box-shadow: 0 1px 2px rgba(11,11,11,0.05), 0 4px 12px rgba(11,11,11,0.08);
  padding: 11px 13px;
  flex-direction: column;
  gap: 8px;
  text-align: left;
  font-size: 12.5px;
}
.mp-round-summary-panel.show { display: flex; animation: overlayIn 260ms ease; }
/* Anchors #mp-round-summary-close, and reserves room for it beside the title
   so the X never sits on top of the heading text. */
.mp-round-summary-panel { position: relative; }
.mp-round-summary-panel h2 { padding-right: 26px; }
.mp-round-summary-panel .overlay-close { width: 26px; height: 26px; top: 4px; right: 4px; }
.mp-round-summary-panel h2 { margin: 0; font-size: 13.5px; color: var(--text-primary); text-align: center; }
.mp-round-summary-target { font-size: 11.5px; color: var(--text-secondary); text-align: center; }
.mp-round-summary-target b { color: var(--good); font-weight: 800; }
.mp-round-summary-list { display: flex; flex-direction: column; gap: 8px; }
.mp-summary-row {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 9px 12px;
  border-radius: 8px;
  background: var(--surface-1);
  border: 1px solid var(--border);
}
.mp-summary-row.me { border-color: var(--group-a); }
.mp-summary-row-top { display: flex; align-items: center; gap: 8px; }
.mp-summary-medal { width: 18px; text-align: center; flex: none; font-size: 15px; }
.mp-summary-name { flex: 1; font-weight: 700; color: var(--text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mp-summary-ready { width: 18px; text-align: center; color: var(--text-muted); flex: none; }
.mp-summary-ready.ready { color: var(--good); }
.mp-summary-row-bottom { display: flex; align-items: center; justify-content: space-between; gap: 8px; font-size: 12px; color: var(--text-secondary); }
.mp-summary-total { font-weight: 800; color: var(--guessable); font-variant-numeric: tabular-nums; white-space: nowrap; }
.mp-summary-tally { display: flex; gap: 6px; font-size: 11.5px; color: var(--text-muted); }
.mp-medal-count { white-space: nowrap; }
.mp-round-summary-footer { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.mp-round-summary-timer { font-size: 12px; color: var(--text-muted); }
.mp-round-summary-timer b { color: var(--text-primary); font-variant-numeric: tabular-nums; }

/* final match-results medal table ---------------------------------------- */
.mp-result-medals { display: flex; gap: 6px; font-size: 12px; color: var(--text-muted); }

/* live-match scoreboard — a dedicated floating panel, not the single-player
   #stats-container (built for one player's stat row, not a multi-player
   roster with per-player submitted status). */
.mp-scoreboard {
  align-self: flex-end;
  width: 210px;
  max-width: 100%;
  z-index: 4;
  background: var(--surface-1);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: var(--shadow);
  overflow: hidden;
  font-size: 12.5px;
}
.mp-scoreboard:empty { display: none; }
.mp-scoreboard-head {
  display: flex;
  justify-content: space-between;
  padding: 8px 10px;
  font-weight: 800;
  background: color-mix(in srgb, var(--group-a) 12%, var(--surface-1));
  color: var(--text-primary);
  border-bottom: 1px solid var(--border);
}
.mp-player-row { display: flex; align-items: center; gap: 8px; padding: 7px 10px; border-top: 1px solid var(--border); }
.mp-player-row:first-of-type { border-top: none; }
.mp-player-row.me { background: color-mix(in srgb, var(--group-a) 8%, transparent); }
.mp-player-name { flex: 1; font-weight: 700; color: var(--text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mp-player-row.me .mp-player-name { color: var(--group-a); }
.mp-player-score { font-weight: 800; color: var(--guessable); font-variant-numeric: tabular-nums; }
.mp-player-status { width: 16px; text-align: center; color: var(--text-muted); }
.mp-player-status.submitted { color: var(--good); }

/* repair-tool globe widget (Geo Shrink only) ------------------------------
   Docks left in .hud-stack (mirrors how .mp-scoreboard docks right for
   multiplayer — same "lives in the HUD flow, gaps pass clicks through to
   the map" pointer-events model), a small rotating globe that cracks with
   each mistake and mends crack-by-crack as Repair Tools are spent. Colors
   deliberately match .hero-* (the Home hero globe) exactly — one shared
   "spinning globe" visual identity across the app, not tied to the
   light/dark page theme. */
.globe-repair-widget {
  display: none;
  align-self: flex-start;
  align-items: center;
  gap: 8px;
  background: var(--surface-1);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 5px 10px 5px 5px;
  box-shadow: var(--shadow);
}
.globe-repair-widget.show { display: flex; }
.grw-globe-wrap { position: relative; width: 46px; height: 46px; flex: none; border-radius: 50%; overflow: visible; }
#grw-globe { width: 100%; height: 100%; display: block; border-radius: 50%; }
.grw-sphere { fill: #16283f; stroke: rgba(255,255,255,0.18); stroke-width: 1; }
.grw-graticule { fill: none; stroke: rgba(255,255,255,0.13); stroke-width: 0.4; }
.grw-land { fill: #4f8fc7; stroke: rgba(255,255,255,0.16); stroke-width: 0.3; }
.grw-crack {
  fill: none; stroke: #ffffff; stroke-width: 2.1;
  stroke-linecap: round; stroke-linejoin: round;
  filter: drop-shadow(0 0 1.2px rgba(0,0,0,0.75));
}
.grw-shattered .grw-globe-wrap::after {
  content: '';
  position: absolute; inset: -1px;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(255,255,255,0.4), rgba(255,255,255,0.05) 70%);
  pointer-events: none;
}
.grw-cracks-badge {
  position: absolute; top: -3px; right: -3px;
  min-width: 16px; height: 16px; padding: 0 3px;
  border-radius: 999px;
  background: var(--critical);
  color: #ffffff;
  font-size: 10px; font-weight: 800;
  line-height: 16px; text-align: center;
  display: none;
}
.grw-cracks-badge.show { display: block; }
.grw-repair-btn { display: flex; align-items: center; gap: 5px; padding: 6px 10px; font-size: 12px; }
.grw-repair-btn svg { flex: none; }
@keyframes grwImpact {
  0% { transform: translateX(0) rotate(0); }
  20% { transform: translateX(-3px) rotate(-3deg); }
  40% { transform: translateX(3px) rotate(3deg); }
  60% { transform: translateX(-2px) rotate(-1deg); }
  80% { transform: translateX(2px) rotate(1deg); }
  100% { transform: translateX(0) rotate(0); }
}
.globe-repair-widget.grw-impact { animation: grwImpact 320ms ease; }

/* Collections / Passports -------------------------------------------------- */
.collections-inner { max-width: 920px; margin: 0 auto; padding: 24px 20px 40px; }
/* Wraps: five sections don't fit one line on a phone, and a row that
   overflows hides whichever tab is last. */
.collections-tab-row { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 14px; }
.collections-tab {
  font: inherit; font-size: 13.5px; font-weight: 700;
  padding: 9px 18px; border-radius: 999px;
  border: 1px solid var(--border); background: var(--surface-1);
  color: var(--text-secondary); cursor: pointer;
}
.collections-tab.active { background: #c9902a; border-color: #c9902a; color: #ffffff; }
.collections-progress { font-size: 13px; font-weight: 700; color: var(--text-secondary); margin-bottom: 16px; }
.diplomat-rank-panel {
  display: flex; flex-direction: column; gap: 4px;
  background: var(--surface-1); border: 1px solid var(--border); border-radius: 12px;
  padding: 14px 16px 22px; margin-bottom: 16px;
}
.diplomat-rank-head { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.diplomat-rank-current { font-size: 14px; color: var(--text-primary); }
.diplomat-rank-current b { color: #c9902a; }
/* Second row: the point total and the distance to the next rank. Separate
   from .diplomat-rank-head so the Equip button keeps the first row to itself
   instead of wrapping below an over-full line at narrow widths. */
.diplomat-rank-sub { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.diplomat-rank-points { font-size: 12.5px; font-weight: 700; color: #c9902a; }
.diplomat-rank-next { font-size: 12.5px; color: var(--text-secondary); }
.diplomat-equip-btn { margin-left: auto; padding: 6px 14px; font-size: 12.5px; }
.diplomat-equip-btn.active { background: var(--good); border-color: var(--good); color: #ffffff; }
.diplomat-rank-panel.capstone {
  border-color: #c9902a;
  background: linear-gradient(120deg, color-mix(in srgb, #c9902a 14%, var(--surface-1)), var(--surface-1));
  box-shadow: 0 0 0 1px color-mix(in srgb, #c9902a 35%, transparent) inset;
}
.diplomat-rank-panel.capstone .diplomat-rank-current b { color: #e0b83a; }

/* The rank progress bar: a continuous fill (Diplomat Points out of
   countries x 3) with a discrete medal node per rank positioned at its
   threshold — same "season pass" shape as the Home progression banner's
   dot tracker, styled per the bronze/silver/gold + shiny materials
   engine/collections.js's materialForRankIndex() assigns. */
.diplomat-track-wrap { margin: 14px 6px 0; }
.diplomat-track { position: relative; height: 7px; border-radius: 999px; background: var(--eliminated-fill); }
.diplomat-track-fill {
  position: absolute; left: 0; top: 0; height: 100%; border-radius: 999px;
  background: linear-gradient(90deg, #8a5326, #c9902a, #ffd75e);
  transition: width 500ms ease;
}
.diplomat-track-node {
  position: absolute; top: 50%; transform: translate(-50%, -50%);
  width: 22px; height: 22px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  border: 2px solid var(--surface-1); box-shadow: var(--shadow);
  background: var(--eliminated-fill); color: var(--text-muted);
  opacity: 0.5; filter: grayscale(0.7);
  transition: transform 200ms ease, opacity 200ms ease;
  cursor: default;
}
.diplomat-track-node.reached { opacity: 1; filter: none; color: rgba(255,255,255,0.92); }
.diplomat-track-node.mat-bronze.reached { background: #a5672f; border-color: #7c4c22; }
.diplomat-track-node.mat-bronze-shiny.reached { background: linear-gradient(135deg, #e0a868, #8a5326); background-size: 200% 200%; }
.diplomat-track-node.mat-silver.reached { background: #9aa1ab; border-color: #6c727a; }
.diplomat-track-node.mat-silver-shiny.reached { background: linear-gradient(135deg, #eef1f5, #9aa1ab); background-size: 200% 200%; }
.diplomat-track-node.mat-gold.reached { background: #c9902a; border-color: #96691c; }
.diplomat-track-node.mat-gold-shiny.reached { background: linear-gradient(135deg, #fff0bd, #e0b83a); background-size: 200% 200%; }
.diplomat-track-node[class*="-shiny"].reached { animation: rankShineSweep 2.6s ease-in-out infinite; }
.diplomat-track-node.current { transform: translate(-50%, -50%) scale(1.3); z-index: 2; box-shadow: 0 0 0 3px color-mix(in srgb, #c9902a 45%, transparent), var(--shadow); }
.diplomat-track-node.capstone.reached { box-shadow: 0 0 14px 2px rgba(255, 215, 90, 0.65); }
@keyframes rankShineSweep {
  0%, 100% { background-position: 0% 50%; }
  50% { background-position: 100% 50%; }
}
@media (prefers-reduced-motion: reduce) {
  .diplomat-track-node[class*="-shiny"].reached { animation: none; }
}
.collections-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
  gap: 12px;
}
.coll-card {
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  background: var(--surface-1); border: 1px solid var(--border); border-radius: 12px;
  padding: 14px 10px; text-align: center; position: relative;
}
.coll-flag { width: 48px; height: 34px; object-fit: cover; border-radius: 4px; border: 1px solid var(--border); }
.coll-flag-none { width: 48px; height: 34px; border-radius: 4px; background: var(--eliminated-fill); display: block; }
.coll-name { font-size: 12px; font-weight: 700; color: var(--text-primary); line-height: 1.25; }
.coll-tier-badge { width: 20px; height: 20px; }
.coll-tier-icon { width: 20px; height: 20px; }
.coll-tier-label { font-size: 10.5px; color: var(--text-muted); }

.coll-card.tier-0 .coll-flag { opacity: 0.32; filter: grayscale(1); }
.coll-card.tier-1 { border-color: color-mix(in srgb, #c9902a 35%, var(--border)); }
.coll-card.tier-1 .coll-tier-icon, .coll-card.tier-1 .coll-tier-label { color: color-mix(in srgb, #c9902a 55%, var(--text-secondary)); font-weight: 700; }
.coll-card.tier-2 { border-color: #8a90a0; box-shadow: 0 0 0 1px color-mix(in srgb, #8a90a0 30%, transparent) inset; }
.coll-card.tier-2 .coll-tier-icon, .coll-card.tier-2 .coll-tier-label { color: #75798a; font-weight: 700; }
.coll-card.tier-3 {
  border-color: #c9902a;
  background: color-mix(in srgb, #c9902a 10%, var(--surface-1));
  box-shadow: 0 2px 10px color-mix(in srgb, #c9902a 25%, transparent);
}
.coll-card.tier-3 .coll-tier-icon, .coll-card.tier-3 .coll-tier-label { color: #c9902a; font-weight: 800; }

/* Milestones tab (100-level reward track) -- shares .collections-grid's
   layout with the Passports cards above, just a different card shape. */
.milestone-card {
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  background: var(--surface-1); border: 1px solid var(--border); border-radius: 12px;
  padding: 14px 10px; text-align: center;
}
.milestone-card.locked { opacity: 0.55; }
.milestone-level { font-size: 11px; font-weight: 800; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.03em; }
.milestone-type { font-size: 10.5px; color: var(--text-muted); }
.milestone-label { font-size: 13px; font-weight: 700; color: var(--text-primary); line-height: 1.25; }
.milestone-extra { font-size: 11px; font-weight: 700; color: #c9902a; }
.milestone-status { font-size: 11px; font-weight: 700; color: var(--text-muted); margin-top: 4px; }
.milestone-equip-btn { margin-top: 4px; padding: 5px 12px; font-size: 12px; }
.milestone-equip-btn.active { background: var(--good); border-color: var(--good); color: #ffffff; }
.milestone-card.capstone {
  border-color: #c9902a;
  background: linear-gradient(160deg, color-mix(in srgb, #c9902a 16%, var(--surface-1)), var(--surface-1));
  box-shadow: 0 0 0 1px color-mix(in srgb, #c9902a 40%, transparent) inset, 0 2px 14px color-mix(in srgb, #c9902a 30%, transparent);
}
.milestone-card.capstone .milestone-label { color: #c9902a; }
/* An equipped single-slot cosmetic (a map theme, the empty title) is shown
   as a disabled button — there is nothing to switch it to from itself. It
   must still read as the confirmed green state, not as a dimmed control. */
.milestone-equip-btn.active:disabled { opacity: 1; }
.milestone-card.wardrobe-none-on { border-color: var(--group-a); }

/* Map Themes / Titles tabs -- one equipped slot each, both spanning the
   whole .collections-grid so a section header sits on its own line. */
.coll-section-head {
  grid-column: 1 / -1;
  display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
  margin: 6px 0 -2px;
  font-size: 12px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--text-secondary);
}
.coll-section-head:first-child { margin-top: 0; }
.coll-section-head span { font-size: 11px; font-weight: 600; letter-spacing: 0; text-transform: none; color: var(--text-muted); }
/* Bigger than the shop's 16px dots: this card has room, and the swatch is
   the only thing on it that says what the theme actually looks like. */
.theme-card .theme-swatch { padding: 6px 0 2px; }
.theme-card .theme-swatch span { width: 24px; height: 24px; }
.title-card-mark { color: #c9902a; display: flex; padding: 4px 0 2px; }
.title-card-mark svg { width: 20px; height: 20px; }
.title-card-mark.none-mark { color: var(--text-muted); opacity: 0.5; }
.milestone-card.capstone .title-card-mark { color: #e0b83a; }
.diplomat-titles-link {
  font: inherit; font-size: 12px; font-weight: 700; padding: 0;
  border: 0; background: none; color: var(--group-a); cursor: pointer;
}
.diplomat-titles-link:hover { text-decoration: underline; }

/* Progress screen ---------------------------------------------------------- */
.progress-inner { max-width: 560px; margin: 0 auto; padding: 16px 16px 48px; }
.progress-hero { display: flex; align-items: center; gap: 14px; margin: 2px 0 18px; }
.progress-mascot-wrap { flex: none; }
.progress-mascot-wrap .mascot-widget { width: 74px; height: 74px; }
.progress-headline { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.progress-headline strong { font-size: 17px; font-weight: 800; color: var(--text-primary); }
.progress-headline span { font-size: 12.5px; color: var(--text-secondary); }

/* Headline figures. Four of them, because the answer to "how am I doing?"
   is a small set of numbers, not a chart — a chart of a single value is
   always worse than the value. */
.pg-stat-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(78px, 1fr)); gap: 8px; margin-bottom: 14px; }
.pg-stat {
  display: flex; flex-direction: column; gap: 1px;
  background: var(--surface-1); border: 1px solid var(--border);
  border-radius: 12px; padding: 9px 11px;
}
.pg-stat-value { font-size: 19px; font-weight: 800; color: var(--text-primary); line-height: 1.1; font-variant-numeric: tabular-nums; }
.pg-stat-label { font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-muted); }
.pg-stat-sub { font-size: 10.5px; color: var(--text-muted); }

.pg-highlights { display: flex; flex-direction: column; gap: 6px; margin-bottom: 16px; }
.pg-highlight {
  display: flex; align-items: center; gap: 9px;
  background: var(--surface-1); border: 1px solid var(--border);
  border-radius: 10px; padding: 7px 11px;
}
/* Icon + word ("strongest", "needs practice") carry the meaning; the color
   is a second, redundant channel, never the only one. */
.pg-highlight-icon { font-size: 13px; line-height: 1; color: var(--text-muted); }
.pg-highlight.good .pg-highlight-icon { color: var(--good); }
.pg-highlight.improved .pg-highlight-icon { color: var(--group-a); }
.pg-highlight.weak .pg-highlight-icon { color: #c9902a; }
.pg-highlight-text { display: flex; flex-direction: column; min-width: 0; }
.pg-highlight-text b { font-size: 13px; font-weight: 700; color: var(--text-primary); }
.pg-highlight-text span { font-size: 11px; color: var(--text-muted); }

.pg-section { margin-bottom: 18px; }
.pg-section-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; margin-bottom: 8px; }
.pg-section-head h2 { margin: 0; font-size: 11.5px; font-weight: 800; letter-spacing: 0.09em; text-transform: uppercase; color: var(--text-muted); }
.pg-section-note { font-size: 10.5px; color: var(--text-muted); }
.pg-hint { font-size: 11.5px; color: var(--text-muted); margin: 4px 0 0; }
.pg-footnote { font-size: 10.5px; color: var(--text-muted); line-height: 1.45; margin: 18px 0 0; }

/* Activity: answers per day. Bars, because the gaps between play days are
   part of what's being read — a line would draw straight through them. */
.pg-activity {
  display: flex; align-items: flex-end; gap: 2px; height: 74px;
  background: var(--surface-1); border: 1px solid var(--border);
  border-radius: 12px; padding: 9px 9px 4px;
}
.pg-bar-col { flex: 1; min-width: 0; display: flex; flex-direction: column; align-items: center; gap: 3px; height: 100%; }
.pg-bar-slot { flex: 1; width: 100%; display: flex; align-items: flex-end; justify-content: center; }
.pg-bar { width: 100%; border-radius: 4px 4px 0 0; background: var(--group-a); min-height: 3px; }
.pg-bar-empty { width: 100%; height: 3px; border-radius: 2px; background: var(--eliminated-fill); }
.pg-bar-col.today .pg-bar { background: var(--guessable); }
.pg-bar-x { font-size: 8.5px; color: var(--text-muted); line-height: 1; }
.pg-bar-col.today .pg-bar-x { color: var(--text-secondary); font-weight: 800; }
.pg-axis-note { display: flex; justify-content: space-between; font-size: 10px; color: var(--text-muted); padding: 4px 2px 0; }

/* Accuracy trend. A line, because the question is direction. Unplayed days
   break the line rather than plotting as 0% — a day you didn't play is not
   a day you scored nothing. */
.pg-linechart {
  position: relative;
  background: var(--surface-1); border: 1px solid var(--border);
  border-radius: 12px; padding: 10px 12px 22px;
}
.pg-linechart svg { display: block; width: 100%; height: 64px; overflow: visible; }
.pg-grid { stroke: var(--border); stroke-width: 1; vector-effect: non-scaling-stroke; }
.pg-line { fill: none; stroke: var(--group-a); stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; vector-effect: non-scaling-stroke; }
.pg-line-dot { fill: var(--group-a); }
.pg-line-head { fill: var(--group-a); stroke: var(--surface-1); stroke-width: 1.5; vector-effect: non-scaling-stroke; }
.pg-line-scale { position: absolute; right: 10px; font-size: 9px; color: var(--text-muted); }
.pg-line-scale-top { top: 6px; }
.pg-line-scale-mid { top: 40px; }
.pg-line-last { position: absolute; left: 12px; bottom: 6px; font-size: 10.5px; font-weight: 700; color: var(--text-secondary); }

/* Topic / continent mastery: horizontal magnitude bars. Topics all share
   one hue on purpose — coloring them by rank would repaint a topic the
   moment it stopped being the strongest. Continents keep the color they
   already have in World Atlas, because there the color IS the entity. */
.pg-mastery { display: flex; flex-direction: column; gap: 10px; }
.pg-mastery-row {
  background: var(--surface-1); border: 1px solid var(--border);
  border-radius: 10px; padding: 8px 11px 7px;
}
.pg-mastery-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; margin-bottom: 6px; }
.pg-mastery-label { font-size: 12.5px; font-weight: 700; color: var(--text-primary); }
.pg-mastery-value { font-size: 12.5px; font-weight: 800; color: var(--text-primary); font-variant-numeric: tabular-nums; }
.pg-mastery-track { height: 6px; border-radius: 999px; background: var(--eliminated-fill); overflow: hidden; }
.pg-mastery-fill { height: 100%; border-radius: 999px; background: var(--group-a); transition: width 500ms ease; }
.pg-mastery-foot { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: 5px; font-size: 10.5px; color: var(--text-muted); }
.pg-delta { font-weight: 800; font-variant-numeric: tabular-nums; }
.pg-delta.up { color: var(--good); }
.pg-delta.down { color: var(--critical); }
.pg-delta.flat, .pg-delta.none { color: var(--text-muted); }

.pg-empty {
  display: flex; flex-direction: column; align-items: center; text-align: center; gap: 6px;
  background: var(--surface-1); border: 1px solid var(--border);
  border-radius: 14px; padding: 26px 20px; color: var(--text-muted);
}
.pg-empty h2 { margin: 4px 0 0; font-size: 16px; color: var(--text-primary); }
.pg-empty p { margin: 0; font-size: 12.5px; color: var(--text-secondary); line-height: 1.5; max-width: 34em; }

/* Mirrors .mini-globe-frame's exact circular-frame treatment, just bigger —
   see engine/mascot.js for why the globe rendering is a separate module
   from engine/heroGlobe.js rather than a shared one. */
.mascot-widget {
  position: relative;
  width: 140px;
  height: 140px;
  border-radius: 50%;
  background: radial-gradient(circle at 35% 30%, #1c3a5e 0%, #060f1c 85%);
  box-shadow: 0 2px 8px rgba(11, 11, 11, 0.3);
  animation: mascotBob 3.2s ease-in-out infinite;
}
.mascot-widget svg { width: 100%; height: 100%; display: block; overflow: visible; }
/* Body colors are the same --hero-* variables the Home brand globe uses, with
   the mascot's own palette as the fallback. That's what lets one widget be
   both the in-game companion (plain defaults) and the Home brand globe
   (picking up whichever GLOBE_SKINS skin is equipped) without a second globe. */
.mascot-sphere-outline { fill: var(--hero-sphere, #16283f); stroke: rgba(255, 255, 255, 0.18); stroke-width: 1; }
.mascot-graticule { fill: none; stroke: rgba(255, 255, 255, 0.13); stroke-width: 0.5; }
.mascot-land { fill: var(--hero-land, #4f8fc7); stroke: rgba(255, 255, 255, 0.16); stroke-width: 0.4; }
/* Dark red ink, softened by a light halo rather than sheer brightness.
   Measured contrast is why the halo exists: dark red alone is only ~1.5:1
   against the mascot's ocean (#16283f), so the face would disappear every
   time it drifted over water, while a pure white face was 14:1 there and
   read as glaring. The halo is a drop-shadow, not a stroke, because the open
   mouth curves would show their implicit closing chord if stroked. */
:root {
  --mascot-eye-ink: #0c1016;   /* black */
  --mascot-mouth-ink: #0c1016; /* black, matching the eyes */
  --mascot-halo: rgba(226, 236, 248, 0.7);
}
.mascot-face { pointer-events: none; filter: drop-shadow(0 0 0.9px var(--mascot-halo)); }
.mascot-eye { fill: var(--mascot-eye-ink); transform-box: fill-box; transform-origin: center; transition: transform 90ms ease; }
.mascot-eye.mascot-blink { transform: scaleY(0.12); }
/* Catchlight — the single cheapest thing that makes a dark circle read as a
   cartoon eye rather than a hole. Not affected by --mascot-eye-ink: it has to
   stay light to be a highlight. */
.mascot-eye-shine {
  fill: rgba(255, 255, 255, 0.92);
  transform-box: fill-box; transform-origin: center;
  transition: transform 90ms ease;
}
.mascot-eye-shine.mascot-blink { transform: scaleY(0.12); }
/* Filled rather than stroked — an unclosed curve (neutral/happy/concerned)
   implicitly closes for fill purposes, rendering as a clean crescent smile/
   frown; the closed shapes (cheering/surprised) fill as a full open mouth. */
/* Stays fill-only, no stroke: the open curves would show their implicit
   closing chord if stroked (see the note above). */
.mascot-mouth { fill: var(--mascot-mouth-ink); stroke: none; }
/* Level-earned skins (GN.progression.MASCOT_SKINS) -- see engine/mascot.js's
   paintLoadout(). An empty slot has no `d` at all, so it needs no color. */
.mascot-accessory { stroke: none; }
/* Slot-level seating. Authored in the face's own units (the whole face group
   is scaled to the widget), so these stay correct at every size — the
   companion, the shop card and the wardrobe tile all read the same numbers.
   Hats were drawn with their brims crossing INSIDE the silhouette, which
   reads as a hat sunk into the head rather than sitting on it; beards sat
   high enough to crowd the mouth. */
.mascot-acc-hat { transform: translateY(-6px); }
.mascot-acc-beard { transform: translateY(3px); }

/* Colour is keyed off the ACCESSORY PATH's own `item-<id>` class, not off a
   class on the widget. That is what allows three slots to be worn at once —
   a hat and eyewear each carry their own item class — and it means the shop
   and wardrobe previews get the identical colour for free by tagging their
   preview path the same way. */
.mascot-accessory.item-sunhat { fill: #e0b96a; }
.mascot-accessory.item-explorer-cap { fill: #8a9a5b; }
.mascot-accessory.item-aviator-shades { fill: #1a1a1a; }
.mascot-accessory.item-beanie { fill: #c94f4f; }
.mascot-accessory.item-top-hat { fill: #23262e; }
.mascot-accessory.item-party-hat { fill: #e0518f; }
.mascot-accessory.item-crown { fill: #e8b52a; }
.mascot-accessory.item-round-specs { fill: #2a2f38; }
.mascot-accessory.item-headphones { fill: #3a7bd5; }
/* Beards. Stubble is deliberately translucent — at full opacity a shape that
   size reads as a full beard rather than as unshaven. */
.mascot-accessory.item-stubble { fill: rgba(38, 44, 54, 0.55); }
.mascot-accessory.item-moustache { fill: #6b4a2f; }
.mascot-accessory.item-goatee { fill: #6b4a2f; }
.mascot-accessory.item-full-beard { fill: #7a5334; }

/* Multi-piece items (engine/mascot.js: an ARRAY entry in ACCESSORIES). The
   group still carries the item's own fill, which every unclassed piece
   inherits — so only the pieces that differ need a rule here. */
.mascot-accessory.item-cowboy-hat { fill: #8a5a33; }
.mascot-accessory.item-cowboy-hat .acc-band { fill: #3a2a1c; }
.mascot-accessory.item-cowboy-hat .acc-crease { fill: rgba(0, 0, 0, 0.2); }
.mascot-accessory.item-viking-helm { fill: #aeb4bd; }
.mascot-accessory.item-viking-helm .acc-rim { fill: #7e8590; }
.mascot-accessory.item-viking-helm .acc-horn { fill: #efe6d2; }
.mascot-accessory.item-wizard-hat { fill: #4b3d9c; }
.mascot-accessory.item-wizard-hat .acc-star { fill: #ffd75e; }
.mascot-accessory.item-chef-toque { fill: #f4f6f8; }
.mascot-accessory.item-chef-toque .acc-band { fill: #e2e6ec; }
.mascot-accessory.item-chef-toque .acc-pleat { fill: #cfd6e0; }
.mascot-accessory.item-traffic-cone { fill: #f2701f; }
.mascot-accessory.item-traffic-cone .acc-stripe { fill: #f7f7f5; }
.mascot-accessory.item-propeller-cap { fill: #3f7fd4; }
.mascot-accessory.item-propeller-cap .acc-panel { fill: #e8483f; }
.mascot-accessory.item-propeller-cap .acc-metal { fill: #cfd6de; }
.mascot-accessory.item-propeller-cap .acc-prop { fill: #f0c33c; }
.mascot-accessory.item-pirate-tricorn { fill: #1f2229; }
.mascot-accessory.item-pirate-tricorn .acc-trim { fill: #d9b03c; }
/* Lenses stay translucent so the eye reads through the glass instead of being
   blanked out by it — the same reason stubble is translucent. */
.mascot-accessory.item-heart-glasses { fill: #d63f6e; }
.mascot-accessory.item-heart-glasses .acc-lens { fill: #ff77a8; opacity: 0.7; }
.mascot-accessory.item-star-shades { fill: #2a2f38; }
.mascot-accessory.item-star-shades .acc-lens { fill: #ff4f9a; opacity: 0.85; }
.mascot-accessory.item-star-shades .acc-glint { fill: rgba(255, 255, 255, 0.5); }
.mascot-accessory.item-cat-eye { fill: #2b2f38; }
.mascot-accessory.item-cat-eye .acc-lens { fill: #bcdcf2; opacity: 0.45; }
.mascot-accessory.item-cat-eye .acc-gem { fill: #ffd75e; }
.mascot-accessory.item-monocle { fill: #d9b03c; }
.mascot-accessory.item-monocle .acc-glass { fill: #cfe6f5; opacity: 0.3; }
.mascot-accessory.item-pixel-shades { fill: #14161b; }
.mascot-accessory.item-pixel-shades .acc-glint { fill: rgba(255, 255, 255, 0.75); }
.mascot-accessory.item-3d-glasses { fill: #f2f4f7; }
.mascot-accessory.item-3d-glasses .acc-red { fill: #e0393c; opacity: 0.82; }
.mascot-accessory.item-3d-glasses .acc-cyan { fill: #2fc4d6; opacity: 0.82; }
.mascot-accessory.item-explorer-goggles { fill: #b8873f; }
.mascot-accessory.item-explorer-goggles .acc-strap { fill: #4a3826; }
.mascot-accessory.item-explorer-goggles .acc-glass { fill: #ffd9a0; opacity: 0.34; }
.mascot-accessory.item-eye-patch { fill: #14161b; }
.mascot-accessory.item-eye-patch .acc-strap { fill: #23262e; }
.mascot-accessory.item-handlebar { fill: #4a3423; }
.mascot-accessory.item-mutton-chops { fill: #6b4a2f; }
.mascot-accessory.item-soul-patch { fill: #6b4a2f; } /* near-black vanished against the planet */
.mascot-accessory.item-viking-braid { fill: #b8843f; }
.mascot-accessory.item-viking-braid .acc-band { fill: #d9b03c; }
.mascot-accessory.item-wizard-beard { fill: #cfd4dc; }
.mascot-accessory.item-wizard-beard .acc-wisp { fill: rgba(255, 255, 255, 0.5); }

/* The propeller actually turns. Flipping scaleX through zero is the flat-plane
   spin illusion — cheaper and more convincing at this size than a rotate,
   which would just look like a slowly tumbling stick. transform-box: fill-box
   pins the origin to the blades' own bounding-box centre, i.e. the hub. */
.mascot-accessory.item-propeller-cap .acc-prop {
  transform-box: fill-box; transform-origin: center;
  animation: propSpin 0.9s linear infinite;
}
@keyframes propSpin {
  0% { transform: scaleX(1); }
  50% { transform: scaleX(-1); }
  100% { transform: scaleX(1); }
}

/* --- auras --------------------------------------------------------------
   Two kinds. The original pair is pure glow — no geometry, so it layers under
   any hat and eyewear without ever colliding with them. The drawn ones
   (engine/mascot.js's AURA_ART) add their own <svg> behind the planet: a
   blurred bloom plus a ring of hearts / rays / sparks that reaches past the
   rim, which a drop-shadow fundamentally cannot do. */
.mascot-widget.mascot-aura-glow-cyan { filter: drop-shadow(0 0 5px rgba(56, 189, 248, 0.9)) drop-shadow(0 0 13px rgba(56, 189, 248, 0.45)); }
.mascot-widget.mascot-aura-glow-gold { filter: drop-shadow(0 0 5px rgba(232, 181, 42, 0.9)) drop-shadow(0 0 13px rgba(232, 181, 42, 0.5)); }
.mascot-widget.mascot-aura-glow-hearts { filter: drop-shadow(0 0 6px rgba(255, 95, 141, 0.85)) drop-shadow(0 0 16px rgba(255, 95, 141, 0.4)); }
.mascot-widget.mascot-aura-glow-sun { filter: drop-shadow(0 0 6px rgba(255, 197, 49, 0.9)) drop-shadow(0 0 18px rgba(255, 150, 30, 0.45)); }
.mascot-widget.mascot-aura-glow-stars { filter: drop-shadow(0 0 5px rgba(188, 216, 255, 0.85)) drop-shadow(0 0 15px rgba(120, 170, 255, 0.4)); }
.mascot-widget.mascot-aura-glow-moon { filter: drop-shadow(0 0 5px rgba(200, 220, 255, 0.75)) drop-shadow(0 0 14px rgba(140, 170, 220, 0.4)); }
/* The same two glows on the flat previews, which have no widget. */
.mascot-preview.item-glow-cyan .mp-globe,
.mascot-item-preview.item-glow-cyan .mp-globe { filter: drop-shadow(0 0 4px rgba(56, 189, 248, 0.95)); }
.mascot-preview.item-glow-gold .mp-globe,
.mascot-item-preview.item-glow-gold .mp-globe { filter: drop-shadow(0 0 4px rgba(232, 181, 42, 0.95)); }

/* The drawn auras' own SVG. On the live companion it is an absolutely
   positioned sibling BEHIND #mascot-globe (z-index, since both are
   positioned): sized to 146% so the authored radius-40 planet in its viewBox
   lands exactly on the real globe's rim, which is what keeps a ray's foot
   touching the edge at every widget size. Inside a flat preview it is a
   nested <svg> instead, sized by its own x/y/width/height. */
.mascot-widget > svg.aura-svg {
  position: absolute; left: 50%; top: 50%;
  width: 138%; height: 138%;
  transform: translate(-50%, -50%);
  z-index: 0;
}
.mascot-widget > svg#mascot-globe { position: relative; z-index: 1; }
.aura-svg { overflow: visible; pointer-events: none; }
/* One blurred disc doing the work a radial gradient would, without needing a
   <defs> id that would collide across the dozen copies on a shop screen. */
.aura-bloom { fill: currentColor; opacity: 0.3; filter: blur(9px); }
.aura-glow-hearts { color: #ff5f8d; }
.aura-glow-sun { color: #ffb01f; }
.aura-glow-stars { color: #8fb6ff; }

.aura-heart {
  fill: #ff5f8d;
  transform-box: fill-box; transform-origin: center;
  animation: auraBeat 2.8s ease-in-out infinite;
}
@keyframes auraBeat {
  0%, 100% { transform: scale(0.8) translateY(1px); opacity: 0.55; }
  45% { transform: scale(1.15) translateY(-1.5px); opacity: 1; }
}
.aura-spark {
  fill: #dce9ff;
  transform-box: fill-box; transform-origin: center;
  animation: auraTwinkle 3.6s ease-in-out infinite;
}
@keyframes auraTwinkle {
  0%, 100% { transform: scale(0.35) rotate(0deg); opacity: 0.25; }
  50% { transform: scale(1.15) rotate(45deg); opacity: 1; }
}
/* The solar corona: a soft ring sitting on the planet's own rim, so the planet
   reads as the thing that is glowing. */
.aura-corona {
  fill: none; stroke: #ffc531; stroke-width: 4;
  opacity: 0.55; filter: blur(2.5px);
  animation: coronaBreathe 4.6s ease-in-out infinite;
}
@keyframes coronaBreathe {
  0%, 100% { opacity: 0.42; }
  50% { opacity: 0.68; }
}
/* Lunar Orbit (level 35). The one aura with a second body in it, so the one
   aura that turns: .aura-spin rotates about 50% 50% of the aura's own
   symmetric viewBox, which with transform-box: view-box resolves to exactly
   the centre of the planet. */
.aura-glow-moon { color: #9fb4cc; }
.aura-orbit-ring { fill: none; stroke: rgba(205, 226, 255, 0.38); stroke-width: 1.4; stroke-dasharray: 3 4.5; }
.aura-moon { fill: #eef1f6; filter: drop-shadow(0 0 3px rgba(190, 214, 255, 0.8)); }
.aura-crater { fill: #b4bcca; }
.aura-spin {
  transform-box: view-box; transform-origin: 50% 50%;
  animation: auraTurn 16s linear infinite;
}
@keyframes auraTurn {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}

/* Each flare pushes a little way straight out of the corona and settles back.
   The rotate() that puts it in the fan lives on its parent wrapper, so this
   translate stays purely radial — see the note in engine/mascot.js. */
.aura-ray {
  fill: #ffd34d;
  animation: auraFlare 2.4s ease-in-out infinite;
}
.aura-ray.short { fill: #ffa826; }
@keyframes auraFlare {
  0%, 100% { transform: translateY(0); opacity: 0.72; }
  50% { transform: translateY(-3.5px); opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .aura-heart, .aura-spark, .aura-ray, .aura-corona, .aura-spin,
  .mascot-accessory.item-propeller-cap .acc-prop { animation: none; }
}

/* The spotlight country on the mascot's body. Reuses the map's own
   flash-good/flash-bad colours so a correct answer reads as the same event
   in both places. */
/* Damage cracks on the companion (engine/mascot.js setDamage). Two stacked
   strokes: a dark fissure with a bright hairline offset inside it, which is
   what makes a flat line read as depth rather than a scribble. */
.mascot-cracks { pointer-events: none; }
/* A crack is a FILLED tapered opening plus a lit lip along one edge, not two
   stacked strokes — engine/mascot.js builds both shapes. Constant-width
   strokes were what made the old ones read as drawn-on lines: real fracture is
   wide where it began and closes to nothing.

   The gap is nearly black rather than a flat dark grey so it reads as depth —
   you are looking into the shell, not at a line painted on it. */
.mascot-crack-gap {
  fill: rgba(0, 0, 0, 0.86);
  stroke: none;
}
/* The lit edge. Warm and thin, and deliberately only on ONE side of the gap:
   a highlight on both sides reads as a glowing seam, a highlight on one side
   reads as light falling across a broken edge. */
.mascot-crack-lip {
  fill: none;
  stroke: #ffd9a8;
  stroke-width: 0.4;
  stroke-opacity: 0.72;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* One soft glow for the whole crack group rather than a filter per path —
   ten cracks with branches is up to ~30 shapes, and a per-element filter is
   what would make that expensive. Same two-layer trick the map's beacons use. */
.mascot-crack { filter: drop-shadow(0 0 1px rgba(255, 185, 115, 0.32)); }
/* Fully shattered: the whole companion desaturates and slumps, so "you are
   out" is legible from the character alone, before any panel explains it. */
.mascot-widget.mascot-broken .mascot-land { filter: grayscale(0.85) brightness(0.62); }
.mascot-widget.mascot-broken .mascot-sphere-outline { filter: grayscale(0.85) brightness(0.62); }
.mascot-widget.mascot-broken { animation: none; opacity: 0.92; }
/* Shattered: the openings go blacker and wider-looking, and the lips burn a
   little hotter — the same ten cracks, but read as a shell that has given up
   rather than one that is holding. */
.mascot-widget.mascot-broken .mascot-crack-gap { fill: rgba(0, 0, 0, 0.95); }
.mascot-widget.mascot-broken .mascot-crack-lip { stroke: #ffc47a; stroke-width: 0.55; stroke-opacity: 0.95; }

.mascot-highlight { fill: none; stroke: none; pointer-events: none; transition: fill 180ms ease; }
.mascot-highlight.good { fill: var(--good); stroke: rgba(255, 255, 255, 0.5); stroke-width: 0.5; }
.mascot-highlight.bad { fill: var(--critical); stroke: rgba(255, 255, 255, 0.5); stroke-width: 0.5; }

/* In-game dock (index.html #mascot-dock). Lives in the .hud-stack flex
   column exactly like .globe-repair-widget, so it lands below the mission
   banner instead of being absolutely positioned into a guessed corner.

   The companion out-ranks everything else in the stack: a tall hat overhangs
   the dock's 96px box by ~25px and used to be sliced off at the mission
   banner's lower border, because the banner is an ordinary in-flow sibling
   above it in tree order. Positioned with a z-index above .hud's 4 so the
   whole character — hat, horns, cone and all — paints over the banner rather
   than under it. The coach bubble (z-index 21) still wins, as it should. */
.mascot-dock { display: none; align-self: flex-start; width: 96px; height: 96px; position: relative; z-index: 5; }
.mascot-dock.show { display: block; }
.mascot-dock .mascot-widget { width: 100%; height: 100%; }

/* Home brand globe. The frame already draws the circular background and owns
   the equipped globe skin, so the widget drops its own background, shadow and
   idle bob here and contributes only the globe + face — otherwise the two
   backgrounds would stack and the bob would jitter the topbar. Overflow goes
   visible so a level-up gesture isn't clipped by the frame's circle. */
.mini-globe-frame.has-mascot { overflow: visible; }
.mini-globe-frame.has-mascot > #hero-globe { display: none; }
.mini-globe-frame .mascot-widget {
  width: 100%; height: 100%;
  background: none;
  box-shadow: none;
  animation: none;
}
/* On the light-land globe skins the dark red already carries 6–7:1 on its
   own, so the halo would just muddy it. */
.mini-globe-frame.skin-silver-globe,
.mini-globe-frame.skin-gold-globe { --mascot-halo: transparent; }

/* Idle breathing. Every one-shot gesture (bounce, hop, wobble, cheer,
   stretch) is a Web Animations API animation in engine/mascot.js's GESTURES
   instead of a keyframe here — a WAAPI animation outranks this one while it
   plays and hands the bob back when it ends, so the two never need to know
   about each other, and no duration has to be kept in sync across two files
   the way the old mascotBounce/BOUNCE_MS pair did. */
@keyframes mascotBob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-4px); } }

/* The mascot is the most persistently-moving thing in the app, so it needs
   the same opt-out .dino-walk and the rank shine already have. The globe
   spin, gestures and gaze are all stopped in JS instead (engine/mascot.js's
   motionReduced) — they're rAF/WAAPI-driven, not CSS animations.
   Expressions still change; only the perpetual motion stops. */
@media (prefers-reduced-motion: reduce) {
  .mascot-widget { animation: none; }
  .mascot-eye, .mascot-eye-shine { transition: none; }
}

/* Large screen -------------------------------------------------------------
   The tier this file did not have. Everything wider than 640px and taller than
   560px — i.e. every desktop browser — used to fall through to the base tokens,
   which are sized for a mid-range phone, and then auto-fill made it worse by
   spending the extra width on more columns instead of wider ones. 230px puts
   the grid at 4 comfortable columns inside .home-inner's 1100px cap and clears
   the 224px every-title-on-one-line threshold with room to spare. */
@media (min-width: 1100px) {
  .viz-root {
    --tile-col: 230px;
    --tile-min-h: 84px;
    --tile-pad-y: 14px;
    --tile-pad-x: 16px;
    --ui-gap: 12px;
    --fs-title: 16px;
  }
}

/* Portrait gate ------------------------------------------------------------
   `pointer: coarse` is doing real work in this query: without it a desktop
   browser window dragged shorter than 560px would be told to rotate a phone
   it does not have. Height-keyed rather than width-keyed for the same reason
   the landscape block below is — a sideways phone is often wider than 640px. */
.rotate-gate { display: none; }
@media (orientation: landscape) and (max-height: 560px) and (pointer: coarse) {
  .rotate-gate {
    position: fixed;
    inset: 0;
    z-index: 999;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 16px;
    background: var(--page-plane);
  }
  html.rotate-gate-off .rotate-gate { display: none; }
  .rotate-gate-card {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    text-align: center;
    max-width: 420px;
  }
  .rotate-gate-icon { width: 40px; height: 40px; color: var(--group-a); }
  .rotate-gate-title { font-size: 18px; font-weight: 700; color: var(--text-primary); }
  .rotate-gate-sub { font-size: var(--fs-body); color: var(--text-secondary); margin: 0; }
}

/* responsive ------------------------------------------------------------- */
@media (max-width: 640px) {
  /* Portrait phone: retune the scale, not the individual rules. The tile
     floor is viewport-relative so a tall phone gets genuinely taller tiles
     instead of a fixed size with slack left at the bottom — svh (not vh) so
     a collapsing address bar doesn't resize them mid-scroll. */
  .viz-root {
    --tile-min-h: clamp(78px, 10.5svh, 116px);
    --tile-pad-y: 11px;
    --tile-pad-x: 12px;
    --ctl-h: 44px;          /* full touch target on the smallest screens */
    --ui-gap: 9px;
    /* 14.5px, not 15: two columns on a 360-390px phone leaves ~96px of text
       room, and "Multiplayer" needs 95.8px at 15px — a margin thin enough
       that it broke in practice. At 14.5px it needs 92.6px and clears. */
    --fs-title: 14.5px;
    /* 138px so a 360px-wide phone still gets exactly 2 columns. Below the
       184px no-break floor by necessity — two columns is the right call on a
       phone — which is what the smaller title size above pays for. */
    --tile-col: 138px;
  }
  .home-inner { padding: 12px 12px 16px; gap: 18px; }
  .home-section + .home-section { padding-top: 16px; }
  .home-section-head h2 { font-size: 13.5px; }
  .home-section-head h2::before { height: 15px; }
  .home-topbar { padding: 15px 12px 10px; } /* same hat headroom as the base rule */
  .home-tagline { display: none; } /* keep the top bar to one compact row on small screens */
  .home-footer { padding: 10px 12px; padding-bottom: max(10px, env(safe-area-inset-bottom)); gap: 11px; }
  .progress-inner { padding: 14px 12px 40px; }

  .hud-stack { top: max(8px, env(safe-area-inset-top)); left: max(8px, env(safe-area-inset-left)); right: max(8px, env(safe-area-inset-right)); gap: 7px; }
  .brand-chip { padding: 6px 8px; }
  .brand-chip-text { display: none; } /* icon-only wordmark — the row is tight enough during gameplay without it */
  /* Same trade as the wordmark above: a fourth segment joined this pill, and
     the word is the cheapest thing in it to lose. Terra's face stays. */
  .terra-bar-label-text { display: none; }
  .terra-bar-label { padding: 0 5px 0 3px; }
  .brand-chip svg { width: 19px; height: 19px; }
  .stat-chip { min-width: 40px; padding: 4px 7px; }
  .stat-chip .v { font-size: 12.5px; }
  .stat-chip .l { font-size: 8.5px; }
  .hud-level-chip { min-width: 46px; padding: 5px 8px; }
  .hud-level-top span:first-child { font-size: 12.5px; }
  .hud-level-xp { font-size: 7.5px; }
  .wallet-chip { padding: 6px 9px; font-size: 12px; }
  /* Narrow phone: Terra shrinks so the row still fits beside the banner.
     Side by side still wins here — it saves the companion's whole height,
     which is the scarcest thing on a phone. */
  .hud-mission { width: min(60vw, 340px); padding: 7px 12px; }
  .hud-mid { gap: 7px; }
  .mascot-dock { width: 74px; height: 74px; }
  .find { font-size: 13px; }
  .find b { font-size: 15px; }
  .find-flag-img { height: 48px; }
  .mp-scoreboard { width: 100%; align-self: stretch; } /* right-docked sidebar doesn't fit next to a near-full-width mission banner on phones — stack instead */
  .mp-round-summary-panel { width: 100%; align-self: stretch; max-height: 34vh; }
  .grw-globe-wrap { width: 32px; height: 32px; }
  .grw-repair-btn { padding: 6px 8px; font-size: 11px; }
  .collections-inner { padding: 16px 14px 32px; }
  .collections-grid { grid-template-columns: repeat(auto-fill, minmax(104px, 1fr)); gap: 9px; }
  .progress-mascot-wrap .mascot-widget { width: 62px; height: 62px; }

  /* icon-only HUD buttons keep a touch-friendly hit area even as content shrinks */
  button.hud-btn.icon-only { padding: 10px; }

  .hud-bottom { bottom: max(8px, env(safe-area-inset-bottom)); left: max(8px, env(safe-area-inset-left)); right: max(8px, env(safe-area-inset-right)); }
  .hint { display: none; } /* save space; discoverable via the legend + buttons */
  .actions { justify-content: space-between; }
  .overlay .big { font-size: 22px; }
}

/* Keyed to HEIGHT rather than width -- a landscape phone (e.g. 700x340) is
   often *wider* than the 640px breakpoint above, so none of that width-based
   compaction applies, yet the desktop-sized paddings/gaps/card heights above
   still eat most of a short viewport, pushing content below the fold. This
   catches that case regardless of width, and stacks on top of the width
   rules when both are short AND narrow. */
@media (max-height: 480px) {
  /* 34px frame here, so the tallest hats overhang ~8px rather than ~11. */
  .home-topbar { padding: 11px 14px 6px; }
  .mini-globe-frame { width: 34px; height: 34px; }
  .home-inner { padding: 8px 14px 10px; gap: 12px; }
  /* Home's tiles are NOT shrunk here. A short viewport is almost always a
     phone turned sideways, where the constraint is height but the surplus is
     width — so the landscape block below spends that width on wider, more
     readable tiles rather than making them smaller in both directions. Only
     the chrome around them gets tighter. */
  .tile-status { display: none; } /* title + icon alone still identifies each tile */
  /* Same trade as .tile-status: the heading still names the group, so the
     explanatory line is the first thing to go when height is scarce. */
  .home-section-sub { display: none; }
  .home-section + .home-section { padding-top: 13px; }
  .home-footer { padding: 6px 14px; padding-bottom: max(6px, env(safe-area-inset-bottom)); }
  .xp-ring { width: 32px; height: 32px; }
  .xp-ring-num { font-size: 12.5px; }
  .passport-strip { padding: 7px 11px; }
  .passport-badge { width: 28px; height: 28px; }

  .hud-stack { gap: 5px; }
  .hud-mission { width: min(58vw, 330px); padding: 5px 12px; }
  button.hud-btn.mission-action-btn { font-size: 11.5px; padding: 4px 11px; }
  .hud-bottom { gap: 5px; }
  .hint { display: none; }
  .legend { padding: 5px 12px; }
}

/* Landscape phone/tablet ---------------------------------------------------
   The map is the point of this app, and in landscape the thing it is short
   of is HEIGHT. So the bottom bar stops being a bar: legend and actions
   become a narrow rail down the right edge, and the top chrome collapses to
   the slimmest single-line form it has. engine/map.js measures where the
   chrome actually ended up (a full-width piece pads vertically, a narrow one
   pads horizontally — see padFromRect), so the map's usable box follows this
   layout automatically rather than being told about it. */
@media (orientation: landscape) and (max-height: 560px) {
  /* Home: wide and short. Sideways there is width to spare and no height to
     spare, so tiles get WIDER (a 190px floor gives ~4 columns on a 844px
     phone, enough that "Multiplayer" and "Neighbor Match" sit on one line at
     full size) and shorter. Text size is deliberately not reduced here —
     text not fitting was the problem, and more columns' worth of width is
     what pays for keeping it readable. */
  .viz-root {
    --tile-min-h: 56px;     /* height is the scarce axis here, width is not */
    --tile-pad-y: 8px;
    --tile-pad-x: 12px;
    --ctl-h: 36px;
    --ui-gap: 8px;
    --bar-h: 8px;
    /* --fs-title deliberately NOT reduced: text not fitting was the problem,
       and the extra column width below is what pays for keeping it full size. */
  }
  .home-inner { padding: 8px 14px 12px; gap: 12px; }
  /* Was a direct grid-template override; now the token, so this tier composes
     with the others instead of racing them on specificity. 190px clears the
     184px no-break floor at full title size. */
  .viz-root { --tile-col: 190px; }
  .home-section { gap: 7px; }
  .home-section + .home-section { padding-top: 10px; }
  .home-section-note { display: none; } /* the group heading alone carries it when height is scarce */
  .tier-chip { padding: 5px 10px; }
  .passport-strip { padding: 6px 12px; }
  .passport-badge { width: 28px; height: 28px; }
  .home-footer { padding: 7px clamp(14px, 3vw, 40px); padding-bottom: max(7px, env(safe-area-inset-bottom)); gap: 10px; }
  .xp-ring { width: 38px; height: 38px; }
  .xp-ring-num { font-size: 14px; }
  .xp-body { gap: 4px; }
  .progress-inner { max-width: 720px; }

  .hud-stack { gap: 4px; top: max(6px, env(safe-area-inset-top)); }
  .hud-top { gap: 6px; }
  /* Sideways, .hud-bottom becomes a vertically-centred rail down the RIGHT
     edge (below), which is exactly where the objective banner now sits.
     Reserve the rail's width so the two can't overlap — keyed to the
     legend's own max-width plus its padding/border and a gap, so the two
     numbers stay visibly related. */
  .hud-mid { padding-right: 172px; }
  .hud-mission { width: min(40vw, 340px); padding: 4px 12px; gap: 3px; }
  .mode-title-label { font-size: 9px; letter-spacing: 0.06em; }
  .find { font-size: 12.5px; }
  .find b { font-size: 14px; }
  .find-flag-img { height: 38px; }
  .progress-track { height: 4px; }
  button.hud-btn.mission-action-btn { font-size: 11px; padding: 3px 10px; }
  .mascot-dock { display: none; } /* companion is decoration; height is not spare here */
  .grw-globe-wrap { width: 26px; height: 26px; }
  .globe-repair-widget { gap: 6px; }

  /* The rail. left:auto is what makes it narrow — and therefore what makes
     engine/map.js treat it as horizontal chrome rather than a bottom bar. */
  .hud-bottom {
    left: auto;
    right: max(6px, env(safe-area-inset-right));
    top: 50%;
    bottom: auto;
    transform: translateY(-50%);
    width: auto;
    max-height: 88vh;
    align-items: flex-end;
    gap: 6px;
    overflow-y: auto;
  }
  .hud-bottom .legend { max-width: 148px; padding: 5px 10px; font-size: 10.5px; }
  .hint { display: none; }
  .actions { flex-direction: column; align-items: stretch; gap: 6px; }
  .actions .mode-panel { display: contents; }
  /* The rail stretches its controls to one shared width. The pill already
     matches that width from its own content, but the segments inside it are
     fixed-size, so they need centering for the day a longer sibling label
     makes the rail wider than the pill's content. */
  .actions .terra-bar { justify-content: center; }
  button.hud-btn { font-size: 12px; padding: 7px 11px; }
  button.hud-btn.icon-only { padding: 8px; }

  /* Full-height panels can't be centered against a rail — dock them left. */
  .mp-scoreboard { align-self: flex-start; width: auto; max-width: 40vw; }
  .mp-round-summary-panel { max-height: 76vh; }
}

/* --- worn flair: nameplate + in-game title chip -------------------------
   Titles, Diplomat ranks and nameplate borders are all earned cosmetics
   that previously had nowhere to appear. Both surfaces share the gold
   treatment and the .border-* rings multiplayer already uses, so a title
   looks the same wherever it is worn. */
.nameplate .np-text { display: flex; flex-direction: column; align-items: flex-start; line-height: 1.15; min-width: 0; }
.nameplate .np-flair { display: flex; align-items: center; gap: 4px; }
.nameplate .np-flair:empty { display: none; }
.np-title, .np-rank {
  font-size: var(--fs-micro); font-weight: 800; letter-spacing: 0.03em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 13ch;
  /* A quiet metallic sheen rather than flat gold — it reads as a prize
     without shouting over the name above it. */
  background: linear-gradient(92deg, var(--gold-lit), var(--gold) 55%, var(--gold-lit));
  -webkit-background-clip: text; background-clip: text; color: transparent;
}
.np-rank { max-width: 11ch; }
.np-sep { font-size: var(--fs-micro); color: var(--text-muted); }
/* The border ring only makes sense once something is in the plate. */
.nameplate.has-flair { border-color: color-mix(in srgb, var(--gold) 45%, var(--border)); }

.mission-head { display: flex; align-items: center; justify-content: center; gap: 8px; flex-wrap: wrap; }
.title-chip {
  display: flex; align-items: center; gap: 4px;
  padding: 2px 9px; border-radius: 999px;
  border: 1px solid color-mix(in srgb, var(--gold) 40%, var(--border));
  background: linear-gradient(180deg, color-mix(in srgb, var(--gold) 12%, var(--surface-1)), var(--surface-1));
  max-width: 40vw;
}
.title-chip:empty { display: none; } /* costs no HUD width until something is equipped */
.title-chip-mark { width: 10px; height: 10px; flex: none; fill: var(--gold); }
.title-chip-text, .title-chip-rank {
  font-size: var(--fs-micro); font-weight: 800; letter-spacing: 0.03em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  background: linear-gradient(92deg, var(--gold-lit), var(--gold) 60%, var(--gold-lit));
  -webkit-background-clip: text; background-clip: text; color: transparent;
}
.title-chip-rank { opacity: 0.85; }
.title-chip-rank::before { content: '·'; color: var(--text-muted); margin-right: 4px; -webkit-text-fill-color: var(--text-muted); }
@media (orientation: landscape) and (max-height: 560px) {
  .title-chip { padding: 3px 8px; max-width: 26vw; }
}
