:root {
    --ink-deep: #0a0d18;
    --ink-mid: #141a2a;
    --ink-soft: #1f2740;
    --parchment: #f0e2c4;
    --parchment-warm: #e8d4a8;
    --parchment-dim: #c9b890;
    --ink-text: #1a1612;
    --brass: #b08756;
    --brass-bright: #d4a972;
    --copper: #c97a3f;
    --crimson: #8b2e2a;
    --crimson-bright: #b8453f;
    --forest: #4a6447;
    --forest-bright: #6a8a64;
    --ocean: #1d2942;
    --ocean-deep: #0f1828;
    --grain: rgba(26, 22, 18, 0.04);
    /* Background color of the option card buttons on hover.
       Debug-tunable so the player can swatch-test alternatives
       without code edits. Default matches the prior hardcoded
       var(--ink-text) value. */
    --option-hover-bg: #f2e2c0;
    /* Text color of the option card buttons on hover. Paired
       with --option-hover-bg so contrast between fill and label
       can be tuned together. Default is parchment, matching the
       previous hardcoded color. */
    --option-hover-fg: #000000;
    /* Difficulty-card (Multiple Choice / Text Entry) image area:
       solid fill behind the transparent PNG, and a hue-rotate offset
       applied to the PNG. Both are live-tunable from the debug
       "Game Mode" tab. */
    --mode-card-img-bg: #2e62d1;
    --mode-card-hue: 302deg;
    --mode-card-sat: 3;
    --mode-card-bright: 2;
  }

  * { box-sizing: border-box; margin: 0; padding: 0; }

  html, body {
    width: 100%; height: 100%;
    overflow: hidden;
    background: var(--ink-deep);
    font-family: 'IBM Plex Mono', monospace;
    color: var(--parchment);
    -webkit-font-smoothing: antialiased;
  }

  #globe {
    position: fixed;
    inset: 0;
    width: 100vw; height: 100vh;
    display: block;
  }

  /* Vignette overlay for depth */
  body::after {
    content: "";
    position: fixed;
    inset: 0;
    pointer-events: none;
    background: radial-gradient(ellipse at center, transparent 30%, rgba(10, 13, 24, 0.55) 100%);
    z-index: 1;
  }

  /* ============ COUNTRY LABELS (overlay during showcase) ============ */
  #labels {
    position: fixed;
    inset: 0;
    pointer-events: none;
    z-index: 5;
    overflow: hidden;
  }
  /* Debug overlay: prints the current target's bboxArea (in deg²)
     near the country on screen. Solid dark background + bright
     yellow text + light-gray border means it stays legible against
     any palette — country fills, ocean blue, halo glow, parchment
     UI. Positioned via fixed coords + centered with transform so
     it sits exactly at the projected centroid (offset slightly
     downward so it doesn't sit under the pin). */
  .debug-bbox-overlay {
    position: fixed;
    pointer-events: none;
    background: rgba(18, 12, 24, 0.94);
    color: #ffd84a;
    border: 1px solid rgba(255, 255, 255, 0.35);
    padding: 0.25rem 0.55rem;
    border-radius: 0.3rem;
    font-family: 'IBM Plex Mono', monospace;
    font-size: 0.7rem;
    font-weight: 600;
    white-space: nowrap;
    z-index: 1000;
    transform: translate(-50%, 0);
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
  }
  .debug-bbox-overlay.hidden { display: none; }

  .country-label {
    position: absolute;
    transform: translate(-50%, -50%);
    white-space: nowrap;
    font-family: 'Fraunces', serif;
    letter-spacing: 0.04em;
    pointer-events: none;
    user-select: none;
    opacity: 0;
    transition: opacity 0.45s ease;
    will-change: opacity, left, top;
    display: inline-flex;
    align-items: center;
    gap: 0.45em;
  }
  .country-label .lbl-flag {
    height: 0.8em;
    width: auto;
    max-height: 1.4rem;
    border: 1px solid rgba(20, 16, 12, 0.6);
    box-shadow: 0 0 6px rgba(255, 215, 140, 0.55),
                0 1px 2px rgba(0, 0, 0, 0.7);
    flex-shrink: 0;
    display: block;
  }
  .country-label.show { opacity: 1; }
  .country-label.target {
    font-weight: 700;
    font-size: 1.5rem;
    color: var(--parchment);
    text-shadow:
      0 0 14px rgba(255, 215, 140, 0.85),
      0 0 4px rgba(20, 16, 12, 0.95),
      0 1px 2px rgba(0, 0, 0, 1);
    letter-spacing: 0.06em;
  }
  .country-label.adjacent {
    font-weight: 500;
    font-size: 0.85rem;
    font-style: normal;
    color: rgba(255, 255, 255, 0.88);
    text-shadow:
      0 0 6px rgba(20, 16, 12, 0.9),
      0 1px 2px rgba(0, 0, 0, 0.95);
  }

  /* Time Attack start countdown (3-2-1). Full-viewport centered
     overlay with one large outlined red numeral. pointer-events:none
     — answering is gated in JS during the countdown. vmin sizing so
     the numeral stays huge on phones and desktop alike. */
  .ta-countdown {
    position: fixed;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    pointer-events: none;
    z-index: 90;
  }
  .ta-countdown.hidden { display: none; }
  .ta-countdown-num {
    font-family: 'Fraunces', serif;
    font-weight: 800;
    font-size: 42vmin;
    line-height: 1;
    color: #e2342b;                       /* vivid red fill */
    -webkit-text-stroke: 0.055em #360606; /* dark outline */
    /* Paint the stroke BEHIND the fill. With the default order the
       stroke draws over the fill and, on glyphs with tight terminals
       (notably Fraunces' "3"), the two stroke sides meet across the
       thin parts and the numeral looks broken. stroke→fill keeps the
       red fill crisp and the outline purely outside it. */
    paint-order: stroke fill;
    text-shadow:
      0 0 0.05em rgba(0, 0, 0, 0.45),
      0.025em 0.03em 0 #5a0d0d;
    user-select: none;
    -webkit-user-select: none;
  }
  /* "Get Ready" phrase step — smaller and kept on one line. */
  .ta-countdown-num.is-phrase {
    font-size: 13vmin;
    white-space: nowrap;
  }
  .ta-countdown-pop { animation: ta-count-pop 0.45s ease-out both; }
  /* Pop in and HOLD (ends at full opacity / scale 1) so each step
     stays visible for its whole duration; the next step re-triggers
     the animation to pop the new text in. */
  @keyframes ta-count-pop {
    0%   { opacity: 0; transform: scale(0.4); }
    55%  { opacity: 1; transform: scale(1.12); }
    80%  { transform: scale(0.98); }
    100% { opacity: 1; transform: scale(1.0); }
  }
  @media (max-width: 720px) {
    .country-label.target { font-size: 1.15rem; }
    .country-label.adjacent { font-size: 0.7rem; }
  }

  /* ============ LOADER ============ */
  #loader {
    position: fixed;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 1rem;
    z-index: 100;
    transition: opacity 0.5s ease;
    pointer-events: none;
  }
  /* Only the card itself catches clicks — the rest of the overlay
     should pass through to the globe canvas so it can be rotated /
     zoomed even while the title menu is showing. */
  #loader > .menu-card { pointer-events: auto; }
  #loader.hidden { opacity: 0; pointer-events: none; }
  #loader.hidden > .menu-card { pointer-events: none; }
  /* Temporary fade during title-screen focused state — when the user
     clicks a country, the menu fades out to give the globe + label
     room. Distinct from .hidden (which is set when leaving intro
     for gameplay) so the two states don't accidentally interact.
     Returning to the wide-idle view removes this class. */
  #loader.intro-faded { opacity: 0; pointer-events: none; }
  #loader.intro-faded > .menu-card { pointer-events: none; }

  /* Desktop title-screen layout: menu pinned to the left, globe shows
     on the right side of the viewport. Mobile keeps the centered
     overlay (the globe is fully visible behind, around, and at the
     edges of the card).

     Flex-direction is column, so align-items controls the horizontal
     axis and justify-content controls the vertical. To pin left and
     center vertically: align-items: flex-start; justify-content: center. */
  @media (min-width: 820px) {
    #loader {
      align-items: flex-start;
      justify-content: center;
      /* --menu-left-px is set by JS (_updateIntroMenuOffset) so the
         menu's right edge stays within 225px of the globe's left
         edge on wide viewports. Falls back to the 3.5rem baseline
         on narrower desktop widths where the natural gap is
         already ≤ 225px. */
      padding-left: var(--menu-left-px, 3.5rem);
    }
  }

  /* Globe canvas covers the full viewport on both title and gameplay
     screens. The title-screen "globe on the right" look is achieved
     by shifting the globeGroup in 3D scene space (see
     _computeIntroOffsetX in the Game class), not by translating the
     canvas. Translating the canvas left a slab of body background
     visible on the left of the viewport — same flat dark color but
     without stars or halo, which read as a "black bar" alongside
     the canvas's textured space. With the internal shift, the
     stars/halo/vignette all extend behind the menu card. */
  #globe { /* no transform — see comment above */ }

  /* On the title screen, hide the game-only UI completely (display:
     none, not just opacity:0). The opacity-fade approach is for
     showing/hiding mid-game where we want a soft transition; on the
     title screen these panels are conceptually absent, so we don't
     want them participating in layout or rendering their parchment
     backgrounds at all. The masthead is hidden too — its "Country
     Quiz" title would duplicate the one already in the menu card. */
  body.intro-active #question-panel,
  body.intro-active #score-panel,
  body.intro-active #top-controls,
  body.intro-active #masthead {
    display: none !important;
  }

  .loader-status {
    font-size: 0.75rem;
    letter-spacing: 0.2em;
    text-transform: uppercase;
    color: var(--parchment-dim);
    margin-top: 0.4rem;
    text-align: center;
  }
  .loader-spinner {
    display: inline-block;
    width: 8px; height: 8px;
    border-radius: 50%;
    background: var(--brass-bright);
    margin: 0 4px;
    animation: pulse-dot 1.4s ease-in-out infinite;
  }
  .loader-spinner:nth-child(2) { animation-delay: 0.2s; }
  .loader-spinner:nth-child(3) { animation-delay: 0.4s; }
  @keyframes pulse-dot {
    0%, 80%, 100% { opacity: 0.2; transform: scale(0.8); }
    40% { opacity: 1; transform: scale(1.2); }
  }
  .loader-error {
    color: var(--crimson-bright);
    font-size: 0.8rem;
    margin-top: 1rem;
    max-width: 400px;
    line-height: 1.5;
    display: none;
    text-align: center;
  }

  /* ---- Mode select (lives inside the loader's menu-card; revealed
         once the data has finished loading) ---- */
  #mode-select {
    display: none;
    flex-direction: column;
    gap: 1.1rem;
    margin-top: 0.5rem;
    width: 100%;
  }
  #mode-select.show { display: flex; }
  /* Unified mode button — used on the intro screen's 3-page wizard.
     Rounded pill on a parchment fill, sitting on the dark menu-card
     background. Earlier iterations used a rectangular ink+brass drop
     shadow to give buttons a "stamped" 3D pop; switched to a pure
     rounded shape per user direction, so the visual weight comes
     entirely from the fill + border + ring on hover rather than from
     an offset shadow. The pill shape is fine for single-line button
     content; descriptions now live OUTSIDE the button so we don't
     need to fit a paragraph inside a 999px radius. */
  .mode-btn {
    appearance: none;
    background: var(--parchment);
    border: 1px solid var(--brass);
    color: var(--ink-text);
    padding: 0.75rem 1.4rem;
    font-family: 'Fraunces', serif;
    text-align: left;
    cursor: pointer;
    /* Flex row so single-line buttons keep their content on the left
       and any optional right-side annotation (e.g. the date stamp
       on 20 Daily) sits on the far right of the pill. space-between
       handles the layout for both cases. */
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.6rem;
    width: 100%;
    border-radius: 999px;
    /* Subtle inner highlight so the parchment fill has a hint of
       depth without the old offset shadow. Reads as a faint vignette
       around the rim, consistent with the antique-paper aesthetic. */
    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.18);
    transition: transform 0.12s ease, background 0.15s ease,
                border-color 0.15s ease;
  }
  .mode-btn-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.6rem;
  }
  .mode-btn .mode-name {
    font-size: 1.05rem;
    font-weight: 700;
    letter-spacing: 0.02em;
    display: block;
  }
  .mode-btn .mode-sub {
    font-family: 'IBM Plex Mono', monospace;
    font-size: 0.7rem;
    color: var(--ink-mid);
    letter-spacing: 0.05em;
    display: block;
    margin-top: 0.2rem;
  }
  .mode-status {
    font-family: 'IBM Plex Mono', monospace;
    font-size: 0.6rem;
    font-weight: 600;
    letter-spacing: 0.12em;
    /* "in progress" pill now sits on the dark menu-card backdrop
       (it moved out of the parchment button onto its own row), so
       the old ink-on-translucent-brass treatment vanished into the
       background. Brass-bright text on a deeper brass tint with a
       brass border gives it the same "stamp" feel against the dark
       card. Completed-state override below stays untouched — forest
       on parchment is already plenty visible. */
    color: var(--brass-bright);
    text-transform: uppercase;
    flex-shrink: 0;
    padding: 0.2rem 0.6rem;
    border: 1px solid var(--brass);
    border-radius: 999px;
    background: rgba(176, 135, 86, 0.18);
    line-height: 1;
  }
  .mode-status.completed {
    color: var(--parchment);
    background: var(--forest);
    border-color: var(--forest);
  }
  /* mode-status pill when it sits INSIDE a .mode-btn (parchment
     background) instead of on the dark menu card. Daily 20's
     completed pill now lives in the button between the title and
     the date stamp — Nunito rounded font matches the orange bonus
     pill's visual softness and the smaller font/padding keeps the
     pill from blowing up the button's row height. */
  .mode-btn .mode-status {
    font-family: 'Nunito', sans-serif;
    font-size: 0.55rem;
    font-weight: 800;
    letter-spacing: 0.14em;
    padding: 0.18rem 0.55rem;
  }
  .mode-progress {
    display: grid;
    grid-template-columns: repeat(9, 1fr);
    gap: 2.5px;
    margin-top: 0.6rem;
  }
  .mode-progress .pe {
    width: 100%;
    aspect-ratio: 1 / 1;
    border-radius: 1.5px;
    box-sizing: border-box;
  }
  .mode-progress .pe-correct { background: #41c632; }
  .mode-progress .pe-wrong   { background: #dc221a; }
  .mode-progress .pe-pending {
    /* Pending cells need a visible rim against the dark card —
       --ink-mid was the right color for the old in-button parchment
       backdrop but is essentially the card background itself, so the
       cells just disappeared. Brass at 0.6 opacity reads as a faint
       outline without screaming for attention. */
    background: transparent;
    border: 1.5px solid var(--brass);
    opacity: 0.6;
  }
  @media (hover: hover) {
    .mode-btn:hover {
      /* Parchment-warm fill + a brighter brass rim. No translate or
         shadow offset — the pill stays put and the visual cue is
         entirely chromatic, which sits better on the dark
         menu-card backdrop than the old "raised paper" look. */
      background: var(--parchment-warm);
      border-color: var(--brass-bright);
    }
    .mode-card:hover {
      transform: translateY(-2px);
    }
  }
  .mode-btn:active {
    transform: scale(0.98);
    background: var(--parchment-dim);
  }
  .mode-btn.current {
    background: #e4d2b0;
    border-color: var(--brass-bright);
  }
  @media (hover: hover) {
    .mode-btn.current:hover {
      background: #dec8a5;
    }
  }
  /* Completed-today tint for the Daily 20 mode button on the title
     screen. Soft mint-green gradient so the player gets immediate
     visual confirmation that today's run is done without needing to
     read the progress strip below. buildPage2HTML adds the class
     when loadProgress shows 20/20 cells filled. */
  .mode-btn.daily-complete {
    background: linear-gradient(180deg,
      rgb(146 255 161) 10% 5%,
      rgb(200 255 208) 70%,
      rgb(196 240 225) 100%);
  }
  @media (hover: hover) {
    .mode-btn.daily-complete:hover {
      background: linear-gradient(180deg,
        rgb(170 255 184) 10% 5%,
        rgb(214 255 222) 70%,
        rgb(212 248 234) 100%);
    }
  }

  /* ---- Multi-page intro menu slider --------------------------------
     The title-screen #mode-select hosts a 3-page wizard:
       page 1 → Standard / Expert (difficulty)
       page 2 → 20 Daily / Time Attack / Continents / Endless (category)
       page 3 → six continents (only reached if Continents was picked)
     Pages live side-by-side in a horizontal flex track inside an
     overflow:hidden clip; navigation animates the track left/right
     by 33.333% per page. All three pages stay rendered so the slide
     can interpolate between two real DOM trees. */
  .menu-pages {
    width: 100%;
    overflow: hidden;
  }
  .menu-pages-track {
    display: flex;
    width: 300%;
    /* cubic-bezier feels less mechanical than 'ease' for this kind of
       multi-step wizard — slight overshoot at the start, soft glide
       at the end. The 0.38s duration is the sweet spot where the
       animation reads as "this happened on purpose" without being
       slow enough that the player can hammer the back button mid-
       slide and end up with two transitions queued. */
    transition: transform 0.38s cubic-bezier(0.32, 0.72, 0.0, 1.0);
  }
  .menu-pages-track[data-page="1"] { transform: translateX(0); }
  .menu-pages-track[data-page="2"] { transform: translateX(-33.5%); }
  .menu-pages-track[data-page="3"] { transform: translateX(-67%); }
  .menu-page {
    /* 33.5% (not 33.3333%) intentionally — each page is slightly
       wider than 1/3 of the track so its right edge overhangs the
       next page's left edge by ~1.85px on a typical menu-card. The
       menu-pages container's overflow:hidden then clips that overhang
       cleanly, which has the effect of hiding any sub-pixel sliver
       of the NEXT page that would otherwise leak through at the
       seam (the visible bug on mobile before this change). The
       overhang is absorbed by the padding-right below — without it,
       buttons inside (which use width:100%) would have their own
       right edge clipped by the menu-pages overflow:hidden. Percentage
       padding on a flex item is relative to the flex container (the
       track at 300% of menu-pages), so 0.5% of track ≈ 1.5% of
       menu-pages ≈ 4-6px on typical viewports — comfortably above
       the ~2px overhang plus any sub-pixel rounding. translateX
       values on the track are matched (-33.5% / -67%) so the active
       page still aligns exactly with menu-pages' left edge. */
    flex: 0 0 33.5%;
    width: 33.5%;
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    gap: 0.7rem;
    padding: 0 0.5% 0 0;
  }
  .menu-page[aria-hidden="true"] {
    /* Inactive pages should not steal Tab focus from the active one
       — keyboard users would otherwise tab through every button on
       every page even though only one slide is on-screen. */
    pointer-events: none;
  }

  /* Breadcrumb row on pages 2 + 3. Back arrow on the left, trail
     text on the right ("Standard", or "Standard » Continents"). Both
     elements need to read against the dark menu-card background. */
  .menu-breadcrumb {
    display: flex;
    align-items: center;
    gap: 0.7rem;
    margin: 0 0 0.4rem;
  }
  .menu-back-btn {
    appearance: none;
    /* Soft brass-tinted fill + brass border + parchment glyph. The
       earlier ink-on-ink treatment vanished into the dark card; this
       gives the back arrow a clear "tap target" silhouette. */
    background: rgba(176, 135, 86, 0.18);
    border: 1px solid var(--brass);
    color: var(--parchment);
    font-family: 'Fraunces', serif;
    font-size: 1.2rem;
    line-height: 1;
    cursor: pointer;
    padding: 0.18rem 0.65rem 0.28rem;
    border-radius: 999px;
    transition: background 0.15s ease, color 0.15s ease,
                border-color 0.15s ease;
  }
  @media (hover: hover) {
    .menu-back-btn:hover {
      background: rgba(176, 135, 86, 0.32);
      color: var(--brass-bright);
      border-color: var(--brass-bright);
    }
  }
  .bc-trail {
    font-family: 'Fraunces', serif;
    font-size: 0.95rem;
    letter-spacing: 0.04em;
    /* Bronze trail color — the breadcrumb runs the full width of the
       row above the buttons, so it needs to be readable on the dark
       card without competing with the white parchment titles inside
       the buttons. Brass-bright gives it the right "antique signage"
       feel. The current/last segment gets full parchment for
       emphasis (it's where the user is in the wizard). */
    color: var(--brass-bright);
  }
  .bc-trail .bc-current {
    color: var(--parchment);
    font-weight: 600;
  }
  .bc-trail .bc-arrow {
    margin: 0 0.45em;
    color: var(--brass);
    font-weight: 600;
  }

  /* Mode-card description line. Lives OUTSIDE the button now (as a
     sibling element), sitting directly on the dark menu-card. Parchment
     fill at full bold so the descriptions read clearly against the
     dark backdrop without competing for attention with the parchment
     buttons themselves — the buttons stay the dominant element, the
     descriptions are bold but smaller. Negative top margin pulls
     each desc tight to its associated button so (button, desc) reads
     as a grouped pair with clear breathing room before the next pair. */
  .mode-desc {
    font-family: 'IBM Plex Mono', monospace;
    font-size: 0.7rem;
    color: #F0E3C4;
    font-weight: 700;
    letter-spacing: 0.04em;
    line-height: 1.5;
    margin: -0.4rem 0.9rem 0;
  }
  /* Difficulty cards (page 1): one clickable button shaped as a card
     — a configurable-fill image area on top (the PNG is transparent,
     so --mode-card-img-bg shows behind it, with --mode-card-hue
     rotating the PNG's hue), and a parchment body below with a black
     title + description. Both vars are live-tunable from the debug
     "Game Mode" tab. */
  .mode-card {
    appearance: none;
    display: block;
    width: 100%;
    padding: 0;
    border: none;
    border-radius: 16px;
    overflow: hidden;
    background: var(--parchment);
    cursor: pointer;
    text-align: center;
    transition: transform 0.12s ease;
  }
  .mode-card-img {
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 1.1rem 1rem 1rem;
    background: var(--mode-card-img-bg);
  }
  .mode-card-img img {
    width: 50%;
    height: auto;
    filter: hue-rotate(var(--mode-card-hue))
            saturate(var(--mode-card-sat))
            brightness(var(--mode-card-bright));
  }
  /* Per-card image defaults — each difficulty card carries its own
     --mode-card-* values. The debug "Game Mode" tab overrides these
     inline on the selected card for live tuning; a page reload
     restores these baked defaults. */
  .mode-card[data-difficulty="standard"] {
    --mode-card-img-bg: #2e62d1;
    --mode-card-hue: 302deg;
    --mode-card-sat: 1.5;
    --mode-card-bright: 2.9;
  }
  .mode-card[data-difficulty="expert"] {
    --mode-card-img-bg: #9b1212;
    --mode-card-hue: 184deg;
    --mode-card-sat: 2;
    --mode-card-bright: 4;
  }
  .mode-card-body {
    display: block;
    padding: 0.7rem 1rem 0.85rem;
    background: var(--parchment);
  }
  .mode-card-title {
    display: block;
    font-family: 'Fraunces', serif;
    font-weight: 800;
    font-size: 1.05rem;
    letter-spacing: 0.02em;
    color: var(--ink-text);
  }
  .mode-card-desc {
    display: block;
    margin-top: 0.3rem;
    font-family: 'IBM Plex Mono', monospace;
    font-size: 0.7rem;
    font-weight: 700;
    letter-spacing: 0.04em;
    line-height: 1.5;
    color: var(--ink-text);
  }
  .mode-card:active { transform: translateY(0); }
  .mode-btn-stamp {
    /* Date stamp on the right side of the 20-Daily button. Lives
       INSIDE the parchment pill so its text color is dark (ink) to
       contrast with the light fill, opposite to .mode-desc which
       lives on the dark card and is parchment-colored. Small caps
       in plain monospace so it reads as a passive metadata tag and
       doesn't crowd the "20 Daily" name on the left. */
    font-family: 'IBM Plex Mono', monospace;
    font-size: 0.62rem;
    color: var(--ink-text);
    font-weight: 900;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    flex-shrink: 0;
  }

  /* Daily-progress strip on page 2. Used to live INSIDE the 20-Daily
     mode button; pulled out here so the button itself stays a clean
     name+description card and the progress info reads as a separate
     annotation underneath. Status pill sits on the right of the strip
     row when the run is in progress. Negative top margin keeps the
     row visually attached to the daily desc above it. */
  .daily-progress-row {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    margin: -0.3rem 0.9rem 0.2rem;
    padding: 0;
  }
  .daily-progress-row .mode-progress {
    flex: 1;
    margin-top: 0;
  }
  .daily-progress-row .mode-status {
    flex-shrink: 0;
  }

  /* Star Rush overview (Daily section → page 3). Follows the
     continents toggle convention: a Star Rush toggle reveals the
     rundown (2×9 squares + crimson star pill) and Play when selected. */
  .sr-date {
    margin-left: auto;          /* pin to the right of the breadcrumb */
    font-family: 'IBM Plex Mono', monospace;
    font-size: 0.6rem;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--brass-bright);
    flex-shrink: 0;
  }
  /* Title + description toggle — same reversed scheme as the continent
     toggles (parchment text on black, brass outline, warm hover,
     high-contrast brass fill when selected). */
  .sr-toggle {
    appearance: none;
    -webkit-appearance: none;
    display: block;
    width: 100%;
    text-align: left;
    background: #000;
    color: var(--parchment);
    border: 1.5px solid var(--brass);
    border-radius: 16px;
    padding: 0.9rem 1.1rem;
    cursor: pointer;
    transition: background 0.14s ease, color 0.14s ease,
                border-color 0.14s ease, transform 0.08s ease;
  }
  .sr-toggle-title {
    display: block;
    font-family: 'Fraunces', serif;
    font-weight: 800;
    font-size: 1.2rem;
    letter-spacing: 0.01em;
  }
  .sr-toggle-desc {
    display: block;
    margin-top: 0.3rem;
    font-family: 'IBM Plex Mono', monospace;
    font-size: 0.7rem;
    font-weight: 700;
    letter-spacing: 0.04em;
    opacity: 0.85;
  }
  @media (hover: hover) {
    .sr-toggle:hover {
      background: #1c1710;
      border-color: var(--brass-bright);
    }
  }
  .sr-toggle.selected {
    background: var(--brass-bright);
    color: var(--ink-text);
    border-color: var(--brass-bright);
  }
  .sr-toggle:active { transform: scale(0.99); }
  /* Rundown + Play, revealed once the toggle is selected. */
  .sr-reveal { margin-top: 0.9rem; }
  .sr-reveal.hidden { display: none; }
  .sr-progress {
    display: flex;
    align-items: center;
    gap: 0;
    margin: 0 0.3rem 0.9rem;
  }
  .sr-squares.mode-progress {
    grid-template-columns: repeat(9, 16px);
    gap: 3px;
    margin-top: 0;
    flex: 0 0 auto;
  }
  /* Star count lives in the standard crimson bonus pill, centered in
     the space to the right of the squares. */
  .sr-progress .speed-bonus-rect {
    margin: 0 auto;
    padding: 0.4rem 0.95rem;
  }
  /* Play button is content-width and centered — a deliberate "go". */
  .sr-play-row { display: flex; justify-content: center; }
  .sr-play.mode-btn {
    width: auto;
    justify-content: center;
    padding: 0.7rem 2.6rem;
  }

  /* Continents page: a single column of toggles. Selecting one fades
     the rest out, slides the chosen one to the top, and reveals Play
     — see wirePage3 for the orchestration. */
  .menu-continents {
    display: flex;
    flex-direction: column;
    align-items: center;       /* toggles size to their content and are
                                  centered; JS locks them all to the
                                  widest ("South America") so the column
                                  is only as wide as it needs to be. */
    gap: 0.7rem;
  }
  .menu-continents .mode-btn { padding: 0.7rem 0.9rem; }

  /* Continent toggles — single-select. Reversed from the parchment
     mode buttons: parchment text on black with a brass outline. Hover
     is a subtle warm lift; the selected state inverts to a bright
     brass fill for clear, high-contrast feedback. */
  .menu-drill-toggle {
    appearance: none;
    -webkit-appearance: none;
    display: flex;
    align-items: center;
    gap: 0.7rem;
    background: #000;
    color: var(--parchment);
    border: 1.5px solid var(--brass);
    border-radius: 999px;
    padding: 0.55rem 1.1rem;
    font-family: 'Fraunces', serif;
    font-weight: 700;
    font-size: 1rem;
    letter-spacing: 0.02em;
    text-align: left;
    white-space: nowrap;
    cursor: pointer;
    transition: background 0.14s ease, color 0.14s ease,
                border-color 0.14s ease, transform 0.08s ease,
                opacity 0.2s ease;
  }
  /* Continent silhouette — fill tracks the text color (parchment, or
     ink when the toggle is selected); brass outline like the globe. */
  .cont-icon {
    width: 1.85em;
    height: 1.85em;
    flex: 0 0 auto;
    fill: currentColor;
    stroke: var(--brass);
  }
  .cont-icon path { vector-effect: non-scaling-stroke; stroke-width: 1; }
  @media (hover: hover) {
    .menu-drill-toggle:hover {
      background: #1c1710;
      border-color: var(--brass-bright);
    }
  }
  .menu-drill-toggle.selected {
    background: var(--brass-bright);
    color: var(--ink-text);
    border-color: var(--brass-bright);
  }
  .menu-drill-toggle.faded { opacity: 0; pointer-events: none; }
  .menu-drill-toggle:active { transform: scale(0.98); }

  /* Play button row — faded in once a continent is selected. */
  .cont-play-row {
    display: flex;
    justify-content: center;
    margin-top: 0.9rem;
    opacity: 0;
    transition: opacity 0.26s ease;
  }
  .cont-play-row.hidden { display: none; }
  .cont-play-row.visible { opacity: 1; }
  .cont-play.mode-btn {
    width: auto;
    align-self: center;
    justify-content: center;
    padding: 0.7rem 2.6rem;
  }

  /* ---- Share modal (Wordle-style results for Daily / Expert) ---- */
  #share-modal {
    position: fixed;
    inset: 0;
    background: rgba(8, 12, 22, 0.78);
    z-index: 100;
    display: none;
    align-items: center;
    justify-content: center;
    padding: 1rem;
  }
  #share-modal.show { display: flex; }
  .share-card {
    /* Dark navy field with brass accents, matching the in-game
       hamburger menu and the title-screen loader cards so the
       end-of-game share popup feels visually consistent with the
       rest of the app rather than an outlier parchment slab. */
    background: var(--ink-deep);
    border: 1px solid var(--brass);
    padding: 1.7rem 1.8rem 1.3rem;
    width: min(380px, 92vw);
    position: relative;
  }
  /* Decorative corner brackets, mirroring .menu-card. The
     overlap of 7px on each axis pulls them into the card's outer
     edge — same overhang the cartographer's-atlas styling uses
     elsewhere in the UI. */
  .share-card::before,
  .share-card::after {
    content: "";
    position: absolute;
    width: 14px;
    height: 14px;
    border: 1px solid var(--brass);
    pointer-events: none;
  }
  .share-card::before {
    top: -7px; left: -7px;
    border-right: none; border-bottom: none;
  }
  .share-card::after {
    bottom: -7px; right: -7px;
    border-left: none; border-top: none;
  }
  .share-title {
    font-family: 'Fraunces', serif;
    font-size: 1.5rem;
    font-weight: 700;
    color: var(--parchment);
    text-align: center;
    margin-bottom: 0.1rem;
    letter-spacing: 0.01em;
  }
  .share-title em { font-style: normal; color: var(--brass-bright); }
  .share-date {
    font-family: 'IBM Plex Mono', monospace;
    font-size: 0.65rem;
    letter-spacing: 0.2em;
    text-transform: uppercase;
    color: var(--brass);
    text-align: center;
    margin-bottom: 0.7rem;
  }
  /* "Completed" pill on daily share modals. Forest-green rounded
     rectangle with parchment text, echoing the mode-btn pill shape
     from the title screen so this badge reads as "the same kind of
     thing — a status stamp" instead of bare text. width:max-content
     hugs the text so the pill scales with its label; margin:auto
     centers the block on its own line under the date. Non-daily
     share modals still hide this entirely (the share JS sets
     display:none). */
  .share-completed {
    display: block;
    width: max-content;
    margin: 0.4rem auto 0.9rem;
    background: var(--forest);
    color: var(--parchment);
    border: 1px solid var(--forest);
    border-radius: 999px;
    padding: 0.45rem 1.4rem;
    font-family: 'Nunito', sans-serif;
    font-size: 0.95rem;
    font-weight: 800;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    line-height: 1;
    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.12);
  }
  .share-score {
    font-family: 'Fraunces', serif;
    font-size: 2.2rem;
    font-weight: 700;
    color: var(--parchment);
    text-align: center;
    margin-bottom: 0.2rem;
    line-height: 1.1;
  }
  .share-score-label {
    font-family: 'IBM Plex Mono', monospace;
    font-size: 0.6rem;
    letter-spacing: 0.25em;
    color: var(--brass-bright);
    text-transform: uppercase;
    text-align: center;
    margin-bottom: 0.9rem;
  }
  .share-grid {
    font-size: 1.3rem;
    line-height: 1.05;
    letter-spacing: 0.04em;
    text-align: center;
    margin-bottom: 0;
    user-select: none;
    white-space: pre-line;
    font-family: 'Apple Color Emoji', 'Segoe UI Emoji', 'Noto Color Emoji', sans-serif;
  }
  /* Daily-only closing message under the emoji grid. Hidden via :empty
     for other modes so they don't reserve a phantom row. Soft italic
     Fraunces with brass-bright accent reads as an aside, not a CTA. */
  .share-message {
    font-family: 'Fraunces', serif;
    font-style: italic;
    font-size: 0.85rem;
    color: var(--parchment);
    text-align: center;
    margin: -0.4rem 0 1.05rem;
    line-height: 1.4;
    letter-spacing: 0.01em;
  }
  .share-message:empty { display: none; }
  .share-message em {
    color: var(--brass-bright);
    font-style: normal;
    font-weight: 600;
  }
  .share-actions {
    display: flex;
    gap: 0.6rem;
  }
  .share-btn {
    flex: 1;
    appearance: none;
    border: 1px solid var(--ink-text);
    background: rgba(26, 22, 18, 0.04);
    color: var(--ink-text);
    padding: 0.7rem 0.45rem;
    font-family: 'IBM Plex Mono', monospace;
    font-size: 0.7rem;
    letter-spacing: 0.13em;
    text-transform: uppercase;
    white-space: nowrap;
    cursor: pointer;
    transition: background 0.15s, color 0.15s, transform 0.1s;
  }
  .share-btn.primary {
    background: var(--ink-text);
    color: var(--parchment);
  }
  .share-btn:hover { transform: translate(-1px, -1px); }
  .share-btn:active { transform: translate(1px, 1px); }
  /* Share-card scope overrides: the share modal is the dark navy
     variant now, so its buttons need brass-on-dark styling. The
     confirm-modal (also uses .share-btn) sits on a parchment card
     and keeps the original parchment-friendly defaults above. */
  .share-card .share-btn {
    border-color: var(--brass);
    background: transparent;
    color: var(--parchment);
    font-family: 'Cinzel', serif;
    font-size: 0.78rem;
    font-weight: 600;
    letter-spacing: 0.18em;
    padding: 0.8rem 0.5rem;
    transition: background 0.15s, color 0.15s, border-color 0.15s, transform 0.1s;
  }
  .share-card .share-btn:hover {
    background: rgba(176, 135, 86, 0.18);
    border-color: var(--brass-bright);
    color: var(--brass-bright);
  }
  .share-card .share-btn.primary {
    background: var(--parchment);
    color: var(--ink-text);
    border-color: var(--brass-bright);
  }
  .share-card .share-btn.primary:hover {
    background: var(--brass-bright);
    color: var(--ink-deep);
    border-color: var(--brass-bright);
  }
  .share-copied {
    text-align: center;
    font-family: 'IBM Plex Mono', monospace;
    font-size: 0.7rem;
    font-weight: 700;
    color: var(--forest-bright);
    margin-top: 0.6rem;
    letter-spacing: 0.15em;
    text-transform: uppercase;
    opacity: 0;
    transition: opacity 0.2s;
  }
  .share-copied.show { opacity: 1; }

  /* ---- Menu modal (hamburger button in the top bar) — re-exposes
         the three game modes after the loader is gone, with a quick
         glance at each mode's progress so the user can switch without
         losing their place. Daily/Expert read their saved progress
         from localStorage; the stale-date check inside loadProgress
         is what resets the strip when a new daily puzzle starts. ---- */
  #menu-modal {
    position: fixed;
    inset: 0;
    background: rgba(8, 12, 22, 0.78);
    z-index: 95;
    display: none;
    align-items: center;
    justify-content: center;
    padding: 1rem;
  }
  #menu-modal.show { display: flex; }
  /* Unified menu card. Shared between the intro screen (#loader) and
     the hamburger menu (#menu-modal) so they're visually identical:
     dark navy field, single brass border line, decorative corner
     brackets at top-left and bottom-right that nod to the antique
     cartographer aesthetic. Don't add box-shadow here — on the dark
     loader backdrop the shadow has nothing to fall on.

     Scrollbars are hidden entirely (display:none on the webkit
     pseudo, scrollbar-width:none on Firefox) instead of reserved
     with scrollbar-gutter. With the gutter approach the reserved
     strip was permanently visible on desktop browsers that don't
     auto-hide. Hidden scrollbars take zero width whether content
     overflows or not, which also stops the original button-jitter
     bug — the layout width is the same in both states. Scrolling
     still works via wheel, trackpad, and touch. */
  .menu-card {
    background: var(--ink-deep);
    border: 1px solid var(--brass);
    padding: 2rem 2.2rem 1.5rem;
    width: min(440px, 92vw);
    max-height: 90vh;
    overflow-y: auto;
    scrollbar-width: none;
    -ms-overflow-style: none;
    position: relative;
    text-align: left;
  }
  .menu-card::-webkit-scrollbar { display: none; }
  .menu-card::before, .menu-card::after {
    content: "";
    position: absolute;
    width: 14px;
    height: 14px;
    border: 1px solid var(--brass);
    pointer-events: none;
  }
  .menu-card::before {
    top: -7px; left: -7px;
    border-right: none; border-bottom: none;
  }
  .menu-card::after {
    bottom: -7px; right: -7px;
    border-left: none; border-top: none;
  }
  .menu-header {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    margin-bottom: 1.3rem;
    padding-bottom: 0.7rem;
    border-bottom: 1px solid rgba(176, 135, 86, 0.4);
  }
  .menu-title {
    font-family: 'Fraunces', serif;
    font-weight: 300;
    font-size: 1.9rem;
    letter-spacing: 0.04em;
    color: var(--parchment);
    font-style: italic;
    line-height: 1.05;
  }
  .menu-close {
    appearance: none;
    border: none;
    background: none;
    color: var(--brass);
    font-size: 1.6rem;
    line-height: 1;
    cursor: pointer;
    padding: 0.1rem 0.45rem;
    transition: color 0.15s;
  }
  .menu-close:hover { color: var(--brass-bright); }

  .menu-modes {
    display: flex;
    flex-direction: column;
    gap: 1.1rem;
  }
  .menu-section {
    display: flex;
    flex-direction: column;
    gap: 0.45rem;
  }
  /* Section header with an inline toggle on the right (e.g. the
     Continents Standard/Expert switch). Without a header wrapper the
     toggle would push the h3's bottom border down on its own row. */
  .menu-section-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 0.6rem;
    padding-bottom: 0.3rem;
    border-bottom: 1px solid rgba(176, 135, 86, 0.35);
  }
  .menu-section-head .menu-section-title {
    border-bottom: none;
    padding-bottom: 0;
  }
  .menu-section-title {
    font-family: 'Cinzel', serif;
    font-size: 0.82rem;
    font-weight: 600;
    letter-spacing: 0.22em;
    text-transform: uppercase;
    color: var(--brass);
    margin: 0;
    padding-bottom: 0.3rem;
    border-bottom: 1px solid rgba(176, 135, 86, 0.35);
  }
  /* Right-aligned annotation that lives in the same row as a section
     title (currently used for today's date next to "20 Daily"). It's
     a sibling of the .menu-section-title inside a .menu-section-head
     flex container, so vertical-align follows the title's baseline. */
  .menu-section-aside {
    font-family: 'IBM Plex Mono', monospace;
    font-size: 0.6rem;
    font-weight: 500;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    color: var(--brass);
  }

  /* "Restart Game" call-to-action shown at the top of the menu only
     when a non-daily run is mid-game. Crimson because the action is
     destructive (wipes the current run's progress), and prominent
     because the player explicitly came here to do something to that
     run. Spans the full width of the card and sits visually distinct
     from the parchment mode buttons below. */
  .restart-btn {
    appearance: none;
    display: block;
    width: 100%;
    margin: 0 0 1.1rem;
    padding: 0.75rem 1rem;
    font-family: 'Cinzel', serif;
    font-size: 0.92rem;
    font-weight: 600;
    letter-spacing: 0.22em;
    text-transform: uppercase;
    color: var(--parchment);
    background: var(--crimson);
    border: 1px solid var(--crimson-bright);
    cursor: pointer;
    transition: background 0.15s, border-color 0.15s;
  }
  .restart-btn:hover {
    background: var(--crimson-bright);
    border-color: var(--brass-bright);
  }
  .restart-btn:active {
    background: var(--crimson);
    transform: translate(1px, 1px);
  }
  /* "Return to Menu" — neutral parchment-on-dark button that pairs
     with the crimson Restart Game button above it. Same dimensions so
     the in-game menu has a tidy two-row stack of full-width actions. */
  .return-btn {
    appearance: none;
    display: block;
    width: 100%;
    margin: 0;
    padding: 0.75rem 1rem;
    font-family: 'Cinzel', serif;
    font-size: 0.92rem;
    font-weight: 600;
    letter-spacing: 0.22em;
    text-transform: uppercase;
    color: var(--parchment);
    background: transparent;
    border: 1px solid var(--brass);
    cursor: pointer;
    transition: background 0.15s, border-color 0.15s, color 0.15s;
  }
  .return-btn:hover {
    background: rgba(176, 135, 86, 0.18);
    border-color: var(--brass-bright);
    color: var(--brass-bright);
  }
  .return-btn:active { transform: translate(1px, 1px); }

  /* Compact menu-card variant for the in-game hamburger popup. Only
     used by #menu-modal now (the title-screen #loader is the regular
     full-width menu-card). Narrower so the small two-button action
     stack doesn't float in a half-empty card. */
  .menu-card-compact {
    width: min(340px, 92vw);
    padding: 1.6rem 1.8rem 1.5rem;
  }
  .menu-actions {
    display: flex;
    flex-direction: column;
    gap: 0.7rem;
  }

  /* Yes/Cancel confirmation modal for destructive actions
     ("Return to Menu" mid-game discards an in-progress run for
     non-daily modes; daily progress is preserved by localStorage but
     the confirmation still gives the player a chance to back out).
     Styled like the share modal — dim backdrop, parchment card,
     centered. */
  #confirm-modal {
    position: fixed;
    inset: 0;
    background: rgba(8, 12, 22, 0.78);
    z-index: 130;
    display: none;
    align-items: center;
    justify-content: center;
    padding: 1rem;
  }
  #confirm-modal.show { display: flex; }
  .confirm-card {
    background: var(--parchment);
    color: var(--ink-text);
    border: 1px solid var(--ink-text);
    box-shadow: 4px 4px 0 var(--ink-mid), 4px 4px 0 1px var(--brass);
    padding: 1.4rem 1.5rem 1.2rem;
    width: min(360px, 92vw);
  }
  .confirm-message {
    font-family: 'Fraunces', serif;
    font-size: 1.05rem;
    font-weight: 500;
    line-height: 1.4;
    text-align: center;
    margin-bottom: 1.2rem;
    color: var(--ink-text);
  }
  .confirm-actions {
    display: flex;
    gap: 0.6rem;
  }
  /* Standard/Expert toggle, themed to the parchment + brass palette.
     The native checkbox is hidden off-screen; the track + thumb
     elements are what the user sees, and they switch state via the
     :checked sibling selector. Clicking anywhere on the label
     (track, thumb, or word "Expert") flips the checkbox. */
  .mode-toggle {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    cursor: pointer;
    user-select: none;
    -webkit-tap-highlight-color: transparent;
  }
  .mode-toggle-input {
    position: absolute;
    opacity: 0;
    pointer-events: none;
    width: 0;
    height: 0;
  }
  .mode-toggle-track {
    position: relative;
    display: inline-block;
    width: 1.85rem;
    height: 1rem;
    border: 1px solid var(--brass);
    border-radius: 999px;
    background: rgba(26, 22, 18, 0.08);
    transition: background 0.15s ease, border-color 0.15s ease;
  }
  .mode-toggle-thumb {
    position: absolute;
    top: 50%;
    left: 0.12rem;
    transform: translateY(-50%);
    width: 0.7rem;
    height: 0.7rem;
    border-radius: 50%;
    background: var(--brass);
    transition: left 0.15s ease, background 0.15s ease;
  }
  .mode-toggle-input:checked ~ .mode-toggle-track {
    background: rgba(176, 135, 86, 0.28);
    border-color: var(--brass-bright, var(--brass));
  }
  .mode-toggle-input:checked ~ .mode-toggle-track .mode-toggle-thumb {
    left: calc(100% - 0.82rem);
    background: var(--crimson);
  }
  .mode-toggle-input:focus-visible ~ .mode-toggle-track {
    outline: 2px solid var(--brass-bright, var(--brass));
    outline-offset: 2px;
  }
  .mode-toggle-text {
    font-family: 'IBM Plex Mono', monospace;
    font-size: 0.6rem;
    font-weight: 600;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    color: var(--brass);
  }
  .menu-buttons {
    display: grid;
    gap: 0.5rem;
  }
  .menu-buttons.menu-pair { grid-template-columns: 1fr 1fr; }
  .menu-buttons.menu-grid { grid-template-columns: 1fr 1fr; }

  /* Compact button used inside the categorized menu — same parchment
     card look as the original, but a tighter padding so two of them
     fit side-by-side, and an inline-flex layout so a status pill can
     sit next to the label without breaking lines. The big-card mode
     used on the original intro is dropped; everything's compact now. */
  .mode-btn.compact {
    padding: 0.55rem 0.75rem;
    box-shadow: 2px 2px 0 var(--ink-mid), 2px 2px 0 1px var(--brass);
  }
  .mode-btn.compact .mode-name {
    font-size: 0.92rem;
    font-weight: 600;
  }
  .mode-btn.compact .mode-btn-header {
    gap: 0.4rem;
  }
  .mode-btn.compact .mode-status {
    font-size: 0.52rem;
    padding: 0.15rem 0.45rem;
  }
  .mode-btn.compact .mode-progress {
    margin-top: 0.4rem;
    gap: 2px;
  }
  @media (hover: hover) {
    .mode-btn.compact:hover {
      box-shadow: 4px 4px 0 var(--ink-mid), 4px 4px 0 1px var(--brass);
    }
  }
  .mode-btn.compact:active {
    box-shadow: 1px 1px 0 var(--ink-mid), 1px 1px 0 1px var(--brass);
  }

  /* ---- Time Attack countdown bar ---- */
  /* Sits at the top of the question panel. Two crimson lines flank a
     centered numeral; both lines collapse toward the numeral as time
     drains (transform-origin pins each line's center-facing edge so
     the outer ends retreat inward). Hidden in non-timed modes. */
  #timer-bar {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    width: 100%;
    padding: 0 0.2rem;
    margin-bottom: 0.55rem;
  }
  #timer-bar.hidden { display: none; }
  /* During the per-round transition (zoom + rotation, plus the
     dive-in stage for tiny countries), the timer bar element is
     hidden via this class so the lines and numeral don't appear
     before the timer actually starts. visibility (not display)
     preserves the layout slot so the question panel doesn't shift
     when the bar reappears. */
  #timer-bar.is-pending { visibility: hidden; }
  .timer-line {
    flex: 1;
    height: 2px;
    background: var(--crimson);
    transform: scaleX(1);
    transition: transform 0.12s linear;
  }
  .timer-line-left  { transform-origin: right center; }
  .timer-line-right { transform-origin: left center; }
  #timer-count {
    font-family: 'Nunito', sans-serif;
    font-size: 1.1rem;
    font-weight: 700;
    color: var(--crimson);
    /* Wide enough to fit "500" (the speed-bonus tally end value)
       without changing width when the value ramps 5 → 500. A
       widening box would drag the count's flex position around
       and slide the 🌟 prefix with it; keeping it stable means
       the 🌟 sits in the same screen spot the whole time. */
    min-width: 3.2ch;
    text-align: center;
    font-variant-numeric: tabular-nums;
    letter-spacing: 0.02em;
    /* Default state: full size, fully opaque. The .is-zero state
       below collapses the numeral when the countdown hits 0,
       transitioning over 1s. inline-block so transform applies.
       position: relative anchors the "🌟" prefix pseudo-element
       that appears after a correct-answer bonus tally finishes. */
    display: inline-block;
    position: relative;
    transform: scale(1);
    opacity: 1;
    transform-origin: center center;
    transition: transform 1s ease-out, opacity 1s ease-out;
  }
  #timer-count.is-zero {
    transform: scale(0);
    opacity: 0;
  }
  /* Snap-reset state used by the preview path (and anywhere else
     that needs to clear count classes without firing transitions).
     Targets both the count's own transitions (transform/opacity
     for is-zero) and the ::before pseudo-element's opacity
     transition (for the 🌟 fade). Applied for one frame, then
     removed after a reflow — see _previewSpeedAnim. */
  #timer-count.is-preview-resetting,
  #timer-count.is-preview-resetting::before {
    transition: none !important;
  }
  /* When .bonus-final is active, the count's textContent is the full
     "🌟500" (or "🌟" alone during the typing phase). Pre-sizing
     min-width to fit "🌟500" means the rounded rect doesn't grow
     as digits type in — it stays at its final dimensions from the
     moment it appears. Symmetric vertical/horizontal padding via
     the ::after rect keeps everything centered. */
  #timer-count.bonus-final {
    font-weight: 900;
    color: var(--parchment);
    min-width: 5ch;
  }
  #timer-count.bonus-final::after {
    /* The rounded crimson rect, drawn behind the text via z-index:
       -1 within the count's stacking context (the count has
       transform: scale(1), which establishes one). Symmetric
       negative offsets give equal padding on all four sides. */
    content: "";
    position: absolute;
    top: -0.2em;
    bottom: -0.2em;
    left: -0.4em;
    right: -0.4em;
    background: var(--crimson);
    border-radius: 1em;
    z-index: -1;
  }
  /* Instant-win variant: when the player answers correctly before
     the round timer has even started ticking (e.g., they recognized
     the country during the camera transition), the bonus is 1000
     instead of the normal 500 max and the rect paints deep navy
     instead of crimson to differentiate the achievement. */
  #timer-count.bonus-final.bonus-instant::after {
    background: #1E2C46;
  }

  /* Shatter-glass effect when the player picks a wrong answer while
     the per-round timer is still ticking. The original timer-bar
     becomes invisible (.is-shattered → visibility: hidden) while
     overlay shards (clipped clones of the bar) animate outward with
     rotation and fade. Lives at the document.body level so the
     pieces aren't clipped by the question-panel's overflow.
     pointer-events:none so they don't intercept any clicks. */
  .timer-shatter-wrapper {
    position: fixed;
    pointer-events: none;
    z-index: 100;
  }
  .timer-shard {
    position: absolute;
    inset: 0;
    transform: translate(0, 0) rotate(0deg);
    opacity: 1;
    /* 400ms feels right — a "snap" rather than a drift. ease-out so
       the pieces decelerate, like glass losing momentum to air. */
    transition: transform 0.4s ease-out, opacity 0.4s ease-out;
    will-change: transform, opacity;
  }
  .timer-shard > #timer-bar {
    /* Override the .hidden / .is-pending classes on the cloned bar
       so the shard renders the bar at full opacity. The clone is
       stripped of these classes in JS already, but if any other
       descendant rule fires, this stays as a backstop. */
    visibility: visible !important;
    display: flex !important;
    width: 100%;
    margin: 0;
  }
  #timer-bar.is-shattered { visibility: hidden; }
  /* Crimson "🌟 500" pill. Used in two places: under the daily
     progress strip on the title screen (when today's run is done)
     and inside the share modal at end-of-game. Layout is horizontal
     — emoji + value sit on one line for an at-a-glance read.
     Parchment text on crimson is the high-contrast combo; the old
     orange-with-black-text was replaced because the orange was off
     the theme's color story. Default styling is block-level with
     auto margins so the pill centers inside the share-card. */
  .speed-bonus-rect {
    display: flex;
    flex-direction: row;
    align-items: center;
    gap: 0;
    width: max-content;
    margin: 0.8rem auto;
    padding: 0.5rem 0.95rem;
    background: var(--crimson);
    color: var(--parchment);
    border-radius: 0.65rem;
    box-shadow: 1px 1px 0 rgba(0, 0, 0, 0.18);
  }
  .speed-bonus-rect .sb-label {
    /* The label is now the 🌟 emoji. Font/letter-spacing don't
       affect emoji glyphs, but we keep a reasonable size and
       line-height: 1 so it sits flush with the value baseline. */
    font-size: 1.05rem;
    line-height: 1;
  }
  .speed-bonus-rect .sb-value {
    font-family: 'Nunito', sans-serif;
    font-weight: 800;
    font-size: 1.15rem;
    font-variant-numeric: tabular-nums;
    line-height: 1;
  }
  .speed-bonus-rect.hidden { display: none; }

  /* Inline title-screen variant — when the pill sits directly inside
     daily-progress-row to the right of the 20-cell strip, scale
     everything to fit the strip's height. align-self: stretch makes
     the pill match the row's height (= the strip's height). Width
     stays implicit from content; the value drives it. */
  .daily-progress-row .speed-bonus-rect {
    align-self: stretch;
    margin: 0;
    padding: 0.22rem 0.55rem;
    gap: 0;
    border-radius: 0.4rem;
    flex-shrink: 0;
    justify-content: center;
  }
  .daily-progress-row .speed-bonus-rect .sb-label {
    font-size: 0.95rem;
    line-height: 1;
  }
  .daily-progress-row .speed-bonus-rect .sb-value {
    font-size: 1rem;
    line-height: 1;
  }

  /* Title block inside the menu modal — mirrors the loader's
     intro title so the two entry points feel like the same screen.
     The accent colors differ from the loader's because the menu
     card sits on a light parchment background, where the loader's
     bright brass-on-dark-blue palette washes out — these are the
     darker, parchment-friendly tones. */
  .menu-header-text { display: flex; flex-direction: column; gap: 0.2rem; }
  .menu-sub-title {
    font-family: 'IBM Plex Mono', monospace;
    font-size: 0.7rem;
    letter-spacing: 0.4em;
    color: var(--brass-bright);
    text-transform: uppercase;
  }
  .menu-title em {
    color: var(--brass-bright);
    font-style: italic;
  }

  /* ---- Debug modal (cmd/ctrl+D) ---- */
  #debug-modal {
    /* Floating panel (NOT a modal anymore): no fullscreen overlay,
       no backdrop, doesn't intercept clicks outside its own bounds.
       Initial position centers horizontally just below the masthead;
       the JS drag handler switches `left`/`top` to absolute pixels
       (and clears the centering transform) the moment the user
       grabs the drag bar. */
    position: fixed;
    top: 80px;
    left: 50%;
    transform: translateX(-50%);
    z-index: 110;
    display: none;
  }
  #debug-modal.show { display: block; }
  .debug-drag-handle {
    /* Anchor row for the drag interaction. Extending the negative
       margins out to the card edges lets the user grab anywhere
       along the top of the card, not just the narrow title text.
       cursor:move signals draggable; user-select:none prevents the
       label text from being highlighted as a drag artifact. */
    cursor: move;
    user-select: none;
    margin: -1.3rem -1.4rem 0.55rem;
    padding: 0.8rem 1.4rem 0.4rem;
    background: rgba(26, 22, 18, 0.05);
    border-bottom: 1px solid rgba(26, 22, 18, 0.12);
  }
  .debug-drag-handle .debug-title,
  .debug-drag-handle .debug-sub {
    margin-bottom: 0;
  }
  .debug-drag-handle .debug-sub { margin-top: 0.05rem; }
  .debug-card {
    background: var(--parchment);
    border: 1px solid var(--ink-text);
    box-shadow: 4px 4px 0 var(--ink-mid), 4px 4px 0 1px var(--brass);
    padding: 1.3rem 1.4rem 1.1rem;
    width: min(340px, 92vw);
    position: relative;
  }
  .debug-title {
    font-family: 'Fraunces', serif;
    font-size: 1.3rem;
    font-weight: 700;
    color: var(--ink-text);
    text-align: center;
    margin-bottom: 0.05rem;
  }
  .debug-sub {
    font-family: 'IBM Plex Mono', monospace;
    font-size: 0.6rem;
    letter-spacing: 0.2em;
    color: var(--ink-mid);
    text-transform: uppercase;
    text-align: center;
    margin-bottom: 1rem;
  }
  .debug-actions {
    display: flex;
    flex-direction: column;
    gap: 0.55rem;
    margin-bottom: 0.7rem;
  }
  .debug-btn {
    appearance: none;
    border: 1px solid var(--ink-text);
    background: rgba(26, 22, 18, 0.04);
    color: var(--ink-text);
    padding: 0.7rem 0.9rem;
    font-family: 'IBM Plex Mono', monospace;
    font-size: 0.75rem;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    cursor: pointer;
    transition: background 0.15s, color 0.15s, transform 0.1s;
  }
  .debug-btn:disabled { opacity: 0.4; cursor: default; }
  .debug-btn:not(:disabled):hover {
    background: var(--ink-text);
    color: var(--parchment);
  }
  .debug-btn:not(:disabled):active { transform: translate(1px, 1px); }
  /* ---------- Tabbed pages inside the debug modal -------------- */
  .debug-tabs {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 0;
    margin-bottom: 0.7rem;
  }
  .debug-tab {
    appearance: none;
    border: 1px solid var(--ink-text);
    background: transparent;
    color: var(--ink-text);
    padding: 0.45rem 0.6rem;
    font-family: 'IBM Plex Mono', monospace;
    font-size: 0.64rem;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    cursor: pointer;
    transition: background 0.15s, color 0.15s;
  }
  .debug-tab:first-child { border-right: none; }
  .debug-tab.active {
    background: var(--ink-text);
    color: var(--parchment);
  }
  .debug-page.hidden { display: none; }

  /* ---------- Palette editor (debug only) ---------------------- */
  .debug-palette-section {
    margin-top: 0.85rem;
    padding-top: 0.7rem;
    border-top: 1px solid rgba(26, 22, 18, 0.15);
  }
  .debug-palette-title {
    font-family: 'IBM Plex Mono', monospace;
    font-size: 0.62rem;
    letter-spacing: 0.2em;
    color: var(--ink-mid);
    text-transform: uppercase;
    text-align: center;
    margin-bottom: 0.55rem;
  }
  /* Ocean FX param sections — only the section matching the
     current mode is visible. data-mode is set on the page itself
     by the mode dropdown's change handler. 'none' hides both. */
  .ocean-fx-section { margin-top: 0.6rem; }
  [data-page="oceanfx"][data-mode="none"]   .ocean-fx-section,
  [data-page="oceanfx"][data-mode="vertex"] .ocean-fx-bump-section,
  [data-page="oceanfx"][data-mode="bump"]   .ocean-fx-vertex-section {
    display: none;
  }
  /* Segmented control for color mode toggle. Active button gets a
     filled background; inactive ones are outlined. */
  .debug-mode-toggle {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 0;
    margin-bottom: 0.55rem;
  }
  .debug-mode-btn {
    appearance: none;
    border: 1px solid var(--ink-text);
    background: transparent;
    color: var(--ink-text);
    padding: 0.45rem 0.6rem;
    font-family: 'IBM Plex Mono', monospace;
    font-size: 0.62rem;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    cursor: pointer;
    transition: background 0.15s, color 0.15s;
  }
  .debug-mode-btn:first-child { border-right: none; }
  .debug-mode-btn.active {
    background: var(--ink-text);
    color: var(--parchment);
  }
  /* Theme picker for color mode 2 — horizontal list of "apply this
     theme" buttons. Each click applies the theme's palette + ocean +
     atmosphere wholesale. Grayed out in mode 1 since themes are
     mode-2 specific. */
  .debug-theme-row {
    display: flex;
    flex-wrap: wrap;
    gap: 0.3rem;
    margin-bottom: 0.55rem;
    transition: opacity 0.15s;
  }
  .debug-theme-row.disabled {
    opacity: 0.4;
    pointer-events: none;
  }
  .debug-theme-btn {
    appearance: none;
    border: 1px solid var(--ink-text);
    background: transparent;
    color: var(--ink-text);
    padding: 0.35rem 0.7rem;
    font-family: 'IBM Plex Mono', monospace;
    font-size: 0.6rem;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    cursor: pointer;
    transition: background 0.15s, color 0.15s;
  }
  .debug-theme-btn:hover {
    background: var(--ink-text);
    color: var(--parchment);
  }
  .debug-theme-btn.active {
    background: var(--ink-text);
    color: var(--parchment);
  }
  /* Seed row for color mode 2's Welsh-Powell shuffle. Slider for
     quick scrub-through, numeric input for typing exact values, and
     a reroll button that picks a random seed. Disabled-looking when
     mode 1 is active (the seed has no effect there) — opacity drop
     plus pointer-events: none. */
  .debug-seed-row {
    display: flex;
    align-items: center;
    gap: 0.45rem;
    margin-bottom: 0.55rem;
    padding: 0.4rem 0.55rem;
    background: rgba(26, 22, 18, 0.04);
    border: 1px solid rgba(26, 22, 18, 0.18);
    transition: opacity 0.15s;
  }
  .debug-seed-row.disabled {
    opacity: 0.4;
    pointer-events: none;
  }
  .debug-seed-row label {
    font-family: 'IBM Plex Mono', monospace;
    font-size: 0.6rem;
    letter-spacing: 0.15em;
    text-transform: uppercase;
    color: var(--ink-mid);
    flex-shrink: 0;
  }
  .debug-seed-row input[type="range"] {
    flex: 1;
    min-width: 0;
    appearance: none;
    height: 4px;
    background: var(--ink-mid);
    border-radius: 2px;
    outline: none;
    cursor: pointer;
  }
  .debug-seed-row input[type="range"]::-webkit-slider-thumb {
    appearance: none;
    width: 12px;
    height: 12px;
    border-radius: 50%;
    background: var(--brass-bright);
    border: 1px solid var(--ink-text);
    cursor: pointer;
  }
  .debug-seed-row input[type="range"]::-moz-range-thumb {
    width: 12px;
    height: 12px;
    border-radius: 50%;
    background: var(--brass-bright);
    border: 1px solid var(--ink-text);
    cursor: pointer;
  }
  .debug-seed-row input[type="number"] {
    width: 52px;
    appearance: none;
    -moz-appearance: textfield;
    border: 1px solid rgba(26, 22, 18, 0.3);
    background: var(--parchment);
    color: var(--ink-text);
    font-family: 'IBM Plex Mono', monospace;
    font-size: 0.65rem;
    padding: 0.15rem 0.25rem;
    text-align: center;
  }
  .debug-seed-row input[type="number"]::-webkit-outer-spin-button,
  .debug-seed-row input[type="number"]::-webkit-inner-spin-button {
    -webkit-appearance: none;
    margin: 0;
  }
  .debug-seed-reroll {
    appearance: none;
    border: 1px solid var(--ink-text);
    background: transparent;
    color: var(--ink-text);
    width: 22px;
    height: 22px;
    border-radius: 3px;
    font-size: 0.85rem;
    line-height: 1;
    cursor: pointer;
    flex-shrink: 0;
  }
  .debug-seed-reroll:hover {
    background: var(--ink-text);
    color: var(--parchment);
  }
  /* Outline ring around the swatch whose color matches a clicked
     country. Box-shadow doesn't disturb the layout (no nudging the
     grid) and stacks visually on top of the swatch's own border. */
  .debug-palette-grid input[type="color"].highlighted {
    box-shadow:
      0 0 0 2px var(--parchment),
      0 0 0 4px var(--crimson),
      0 0 8px 4px rgba(196, 60, 60, 0.5);
  }
  .debug-palette-grid {
    display: grid;
    grid-template-columns: repeat(10, 1fr);
    gap: 3px;
    margin-bottom: 0.6rem;
  }
  /* Native <input type="color"> with the browser's surrounding chrome
     stripped. The swatch becomes the entire input area; the OS native
     color picker still opens on click, which is what the user
     actually needs. -webkit-color-swatch-wrapper / -swatch take care
     of Chrome/Safari; Firefox handles it via default since we set
     padding/border to 0. */
  .debug-palette-grid input[type="color"] {
    appearance: none;
    -webkit-appearance: none;
    width: 100%;
    height: 22px;
    padding: 0;
    border: 1px solid rgba(26, 22, 18, 0.4);
    border-radius: 2px;
    cursor: pointer;
    background: transparent;
  }
  .debug-palette-grid input[type="color"]::-webkit-color-swatch-wrapper {
    padding: 0;
  }
  .debug-palette-grid input[type="color"]::-webkit-color-swatch {
    border: none;
    border-radius: 0;
  }
  .debug-palette-out {
    width: 100%;
    box-sizing: border-box;
    background: rgba(26, 22, 18, 0.04);
    border: 1px solid rgba(26, 22, 18, 0.25);
    color: var(--ink-text);
    font-family: 'IBM Plex Mono', monospace;
    font-size: 0.6rem;
    line-height: 1.4;
    padding: 0.4rem 0.5rem;
    resize: vertical;
    min-height: 60px;
  }
  .debug-palette-out:focus {
    outline: 1px solid var(--brass);
  }
  /* Native <select> styling for blend-mode dropdowns. Matches the
     color-pickers in the same rows so the visual rhythm holds. */
  .debug-select {
    appearance: none;
    -webkit-appearance: none;
    background: rgba(26, 22, 18, 0.04);
    border: 1px solid rgba(26, 22, 18, 0.25);
    color: var(--ink-text);
    font-family: 'IBM Plex Mono', monospace;
    font-size: 0.7rem;
    padding: 0.25rem 0.5rem;
    border-radius: 3px;
    cursor: pointer;
    min-width: 100px;
  }
  /* Layout for the lighting JSON output: textarea fills the row,
     copy button hangs at the bottom-right. Stacks vertically for
     simplicity since the textarea already takes full width. */
  .debug-light-output-row {
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
    margin-top: 0.8rem;
  }
  .debug-light-output-row .debug-btn {
    align-self: flex-end;
  }
  /* Brief flash to confirm the Copy button copied successfully. */
  .debug-btn.flash-copied {
    background: var(--brass);
    color: var(--ink-deep);
  }
  .debug-close {
    appearance: none;
    border: none;
    background: transparent;
    color: var(--ink-mid);
    font-family: 'IBM Plex Mono', monospace;
    font-size: 0.65rem;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    cursor: pointer;
    display: block;
    margin: 0 auto;
    padding: 0.3rem 0.6rem;
  }
  .debug-close:hover { color: var(--ink-text); }
  .debug-slider-row {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    margin: 0.8rem 0 0.6rem;
    padding: 0.5rem 0.6rem;
    background: rgba(26, 22, 18, 0.04);
    border: 1px solid rgba(26, 22, 18, 0.18);
  }
  /* Ocean/atmosphere single-color picker row. Same chrome as the
     slider row so the debug panel reads as one consistent block;
     the swatch sits flush right, label left. */
  .debug-color-row {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    margin: 0 0 0.45rem;
    padding: 0.45rem 0.6rem;
    background: rgba(26, 22, 18, 0.04);
    border: 1px solid rgba(26, 22, 18, 0.18);
  }
  .debug-color-row label {
    font-family: 'IBM Plex Mono', monospace;
    font-size: 0.6rem;
    letter-spacing: 0.15em;
    text-transform: uppercase;
    color: var(--ink-mid);
    flex: 1;
  }
  .debug-color-row input[type="color"] {
    appearance: none;
    -webkit-appearance: none;
    width: 36px;
    height: 22px;
    padding: 0;
    border: 1px solid rgba(26, 22, 18, 0.4);
    border-radius: 2px;
    cursor: pointer;
    background: transparent;
  }
  .debug-color-row input[type="color"]::-webkit-color-swatch-wrapper {
    padding: 0;
  }
  .debug-color-row input[type="color"]::-webkit-color-swatch {
    border: none;
    border-radius: 0;
  }
  /* Small "↺" reset button rendered next to specific color pickers
     (ocean, grid, border, atmosphere, both hover colors, crimson).
     Click sets the picker's value back to its data-default and
     dispatches an 'input' event so the picker's wired apply()
     callback runs and propagates the reset everywhere. */
  .debug-reset-btn {
    background: transparent;
    border: 1px solid var(--brass);
    color: var(--brass-bright);
    width: 22px;
    height: 22px;
    padding: 0;
    cursor: pointer;
    border-radius: 50%;
    font-size: 0.95rem;
    line-height: 1;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    transition: background 0.1s, color 0.1s, transform 0.05s;
  }
  .debug-reset-btn:hover {
    background: var(--brass);
    color: var(--ink-text);
  }
  .debug-reset-btn:active {
    transform: scale(0.92);
  }
  .debug-slider-row label {
    font-family: 'IBM Plex Mono', monospace;
    font-size: 0.6rem;
    letter-spacing: 0.15em;
    text-transform: uppercase;
    color: var(--ink-mid);
    flex-shrink: 0;
  }
  .debug-slider-row input[type="range"] {
    flex: 1;
    min-width: 0;
    appearance: none;
    height: 4px;
    background: var(--ink-mid);
    border-radius: 2px;
    outline: none;
    cursor: pointer;
  }
  .debug-slider-row input[type="range"]::-webkit-slider-thumb {
    appearance: none;
    width: 14px;
    height: 14px;
    border-radius: 50%;
    background: var(--brass-bright);
    border: 1px solid var(--ink-text);
    cursor: pointer;
  }
  .debug-slider-row input[type="range"]::-moz-range-thumb {
    width: 14px;
    height: 14px;
    border-radius: 50%;
    background: var(--brass-bright);
    border: 1px solid var(--ink-text);
    cursor: pointer;
  }
  .debug-slider-row span {
    font-family: 'IBM Plex Mono', monospace;
    font-size: 0.7rem;
    color: var(--ink-text);
    min-width: 5ch;
    text-align: right;
    flex-shrink: 0;
  }

  /* ---- Linear progress strip (Daily/Expert) — replaces the
         accuracy bar so the user sees their answer history at a
         glance: filled green/red squares for answered, hollow
         outline squares for upcoming. Locked to a 10-column grid
         so all 20 always land as exactly two rows of ten, even on
         narrow viewports — the squares shrink, the layout doesn't. ---- */
  .progress-strip {
    display: grid;
    grid-template-columns: repeat(9, 1fr);
    gap: 3px;
    margin-top: 0.4rem;
  }
  .progress-strip.hidden { display: none; }
  .progress-strip .pe {
    width: 100%;
    aspect-ratio: 1 / 1;
    border-radius: 1.5px;
    box-sizing: border-box;
  }
  .progress-strip .pe-correct { background: var(--forest); }
  .progress-strip .pe-wrong   { background: var(--crimson); }
  .progress-strip .pe-pending {
    background: transparent;
    border: 1.5px solid var(--ink-mid);
    opacity: 0.55;
  }
  .progress-strip .pe-current {
    background: transparent;
    border: 1.5px solid var(--brass-bright);
    box-shadow: 0 0 4px var(--brass);
  }

  /* ---- Expert mode input + autocomplete dropdown ---- */
  .expert-input-row {
    display: flex;
    gap: 0.6rem;
    margin: 0 auto 0.3rem;
    position: relative;
    max-width: 440px;
    justify-content: center;
  }
  .expert-input-wrap {
    position: relative;
    flex: 1;
    min-width: 0;
    display: flex;
  }
  #expert-input {
    flex: 1;
    appearance: none;
    border: 1px solid var(--ink-text);
    background: var(--parchment);
    color: var(--ink-text);
    padding: 0.85rem 1rem;
    font-family: 'Fraunces', serif;
    font-size: 1rem;
    font-weight: 600;
    letter-spacing: 0.02em;
    outline: none;
    min-width: 0;
    transition: background 0.18s ease, color 0.18s ease, border-color 0.18s ease;
  }
  #expert-input:focus {
    box-shadow: 0 0 0 2px var(--brass);
  }
  #expert-input:disabled {
    opacity: 0.55;
    cursor: default;
  }
  /* After submit, the input becomes a result chip — green or red
     filled rectangle with parchment text. These rules override the
     :disabled opacity so the highlight reads as a final answer rather
     than a greyed-out field. We also collapse the row's flex so the
     chip shrinks to fit the country name (driven by `size=` set in JS)
     plus its own padding, instead of stretching across the panel. */
  #expert-input.correct,
  #expert-input.wrong {
    opacity: 1;
    color: var(--parchment);
    font-weight: 700;
    flex: 0 0 auto;
    width: auto;
    text-align: center;
  }
  .expert-input-wrap.answered {
    flex: 0 0 auto;
    min-width: 0;
  }
  #expert-input.correct {
    background: var(--forest);
    border-color: var(--forest);
  }
  #expert-input.wrong {
    background: var(--crimson);
    border-color: var(--crimson);
  }
  /* The MCQ-style corner badge: positioned at the upper-right of the
     input via the wrap. Same .option-badge styling reused so the
     check/X icon, pop animation, and parchment ring all match the
     MCQ result. */
  .expert-input-wrap .option-badge {
    top: -10px;
    right: -10px;
  }
  #expert-submit {
    appearance: none;
    border: 1px solid var(--ink-text);
    background: var(--ink-text);
    color: var(--parchment);
    padding: 0 1.1rem;
    font-family: 'IBM Plex Mono', monospace;
    font-size: 0.75rem;
    font-weight: 700;
    letter-spacing: 0.2em;
    text-transform: uppercase;
    cursor: pointer;
    transition: opacity 0.15s, transform 0.1s;
  }
  #expert-submit:disabled {
    opacity: 0.35;
    cursor: default;
  }
  #expert-submit:not(:disabled):active {
    transform: translate(1px, 1px);
  }
  #expert-dropdown {
    position: absolute;
    top: calc(100% + 0.3rem);
    left: 0;
    right: 0;
    background: var(--parchment);
    border: 1px solid var(--ink-text);
    box-shadow: 3px 3px 0 var(--ink-mid), 3px 3px 0 1px var(--brass);
    max-height: 240px;
    overflow-y: auto;
    z-index: 50;
    display: none;
  }
  #expert-dropdown.show { display: block; }
  .expert-item {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0.5rem 0.8rem;
    cursor: pointer;
    border-bottom: 1px solid rgba(26, 22, 18, 0.08);
    font-family: 'Fraunces', serif;
    font-size: 0.95rem;
    font-weight: 500;
  }
  .expert-item:last-child { border-bottom: none; }
  .expert-item:hover, .expert-item.active {
    background: var(--ink-text);
    color: var(--parchment);
  }
  .expert-item img {
    height: 18px;
    width: auto;
    max-width: 28px;
    border: 1px solid rgba(26, 22, 18, 0.3);
    flex-shrink: 0;
  }
  .expert-feedback {
    margin-top: 0.6rem;
    font-family: 'IBM Plex Mono', monospace;
    font-size: 0.7rem;
    color: var(--crimson);
    letter-spacing: 0.1em;
    text-transform: uppercase;
    text-align: center;
    min-height: 1em;
    transition: opacity 0.18s;
  }

  /* ============ MAIN UI ============ */
  #ui {
    position: fixed;
    inset: 0;
    pointer-events: none;
    z-index: 10;
  }
  #ui > * { pointer-events: auto; }

  /* ============ MASTHEAD ============ */
  #masthead {
    position: absolute;
    top: 1.8rem;
    left: 50%;
    transform: translateX(-50%);
    text-align: center;
    user-select: none;
  }
  .masthead-line {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 1rem;
    color: var(--brass);
    font-size: 0.65rem;
    letter-spacing: 0.5em;
    text-transform: uppercase;
    margin-bottom: 0.4rem;
  }
  .masthead-line::before, .masthead-line::after {
    content: "";
    width: 40px; height: 1px;
    background: var(--brass);
  }
  .masthead-title {
    font-family: 'Fraunces', serif;
    font-weight: 700;
    font-style: italic;
    font-size: 1.6rem;
    color: var(--parchment);
    letter-spacing: 0.04em;
  }
  .masthead-title em { color: var(--brass-bright); font-weight: 800; font-style: normal; }
  /* Version tag — sits next to the "Country Quiz" logo in both the
     intro menu-title and the in-game masthead-title. Small,
     parchment-colored, with a touch of vertical-align so it floats
     slightly above the baseline like a superscript. font-style:
     normal so it doesn't inherit the masthead's italic. */
  .version-tag {
    font-family: 'IBM Plex Mono', monospace;
    font-size: 0.55rem;
    font-weight: 500;
    font-style: normal;
    letter-spacing: 0.08em;
    color: var(--parchment);
    opacity: 0.65;
    margin-left: 0.45em;
    vertical-align: 0.7em;
    user-select: none;
  }
  /* Optional sub-line under the title — currently used for the
     continent name during continent mode runs. Hidden by default
     (no text → empty space) and shown when setMode populates it. */
  .masthead-subtitle {
    font-family: 'IBM Plex Mono', monospace;
    font-size: 0.7rem;
    font-weight: 500;
    letter-spacing: 0.32em;
    text-transform: uppercase;
    color: var(--brass-bright);
    margin-top: 0.35rem;
    min-height: 0.85rem; /* avoid layout shift when toggled */
  }
  .masthead-subtitle:empty { display: none; }
  /* Pill variant — used by the Continents category to show a small
     parchment-on-dark "Continents" tag below the masthead title.
     Inline-block sizes the background to the text width so it
     centers inside the text-align:center masthead. */
  .masthead-subtitle.is-pill {
    display: inline-block;
    background: var(--parchment);
    color: var(--ink-text);
    border-radius: 999px;
    padding: 0.2rem 0.95rem;
    letter-spacing: 0.2em;
    font-weight: 600;
    min-height: 0;
    box-shadow: inset 0 0 0 1px rgba(176, 135, 86, 0.45);
  }

  /* ============ SCORE PANEL (top-left) ============ */
  #score-panel {
    position: absolute;
    top: 1.5rem;
    left: 1.5rem;
    background: var(--parchment);
    color: var(--ink-text);
    padding: 1.2rem 1.4rem 1rem;
    border: 1px solid var(--ink-text);
    box-shadow: 4px 4px 0 var(--ink-mid),
                4px 4px 0 1px var(--brass);
    width: 260px;
  }
  /* Parchment grain */
  #score-panel::before {
    content: "";
    position: absolute;
    inset: 0;
    pointer-events: none;
    background-image:
      radial-gradient(circle at 20% 30%, rgba(176, 135, 86, 0.08) 0%, transparent 40%),
      radial-gradient(circle at 80% 70%, rgba(139, 46, 42, 0.05) 0%, transparent 40%);
  }

  /* Star counter — crimson pill that hangs from the bottom of
     #score-panel, half-overlapping the panel's lower edge. Visible
     only in modes that bank speed-bonus stars (daily / continent /
     marathon). Updated by the count-up driver in
     _kickStarCounterAnim during the card-swap delay between
     rounds. tabular-nums keeps digit columns from jittering as
     the value ticks up. */
  /* ----- Tiny-country screen-space glare ----- */
  /* Used when a country's bounding sphere is below the tinyThreshold
     param. _startCountryGlare hides the mesh-based band and shows
     these DOM overlays instead, positioned per frame from _tick to
     track the pin's projected screen coordinates. Hidden by default;
     the .visible class is added at glare start and removed on cleanup. */
  #glare-glow {
    position: fixed;
    top: 0;
    left: 0;
    width: 100px;
    height: 100px;
    border-radius: 50%;
    pointer-events: none;
    z-index: 50;
    /* translate-50% centers the gradient on the (left, top) coords
       JS writes each frame. */
    transform: translate(-50%, -50%);
    background: radial-gradient(circle,
      rgba(255, 255, 255, 0.65) 0%,
      rgba(255, 255, 255, 0.32) 38%,
      rgba(255, 255, 255, 0) 78%);
    opacity: 0;
    will-change: opacity, transform, width, height;
  }
  #glare-glow.hidden { display: none; }

  /* Reticle container — positioned absolutely around the country's
     projected screen bbox (with min size). Corners are absolutely
     placed within so each one stays at fixed pixel size regardless
     of frame size. */
  #glare-reticle {
    --bracket-size: 22px;
    --fill-thickness: 2px;
    --outline-thickness: 1px;
    --fill-color: #ffffff;
    --outline-color: #000000;
    position: fixed;
    top: 0;
    left: 0;
    width: 100px;
    height: 100px;
    pointer-events: none;
    z-index: 50;
    transform: translate(-50%, -50%);
    opacity: 0;
    will-change: opacity, transform, width, height;
  }
  #glare-reticle.hidden { display: none; }

  /* Reticle corner SVGs — one per corner, positioned at the
     respective edge of the reticle frame. Size and viewBox are set
     from JS in _applyReticleStyle so they match bracketSize exactly.
     Each SVG contains two paths drawing the SAME L-shape: outline
     (thicker, drawn first) and fill (thinner, drawn on top). Since
     both strokes share a centerline, the result is reliably an
     outlined L regardless of slider values. */
  .reticle-corner {
    position: absolute;
    overflow: visible;
    pointer-events: none;
  }
  .reticle-tl { top: 0; left: 0; }
  .reticle-tr { top: 0; right: 0; }
  .reticle-bl { bottom: 0; left: 0; }
  .reticle-br { bottom: 0; right: 0; }

  .reticle-outline {
    fill: none;
    stroke: var(--outline-color);
    stroke-width: calc(var(--fill-thickness) + 2 * var(--outline-thickness));
    stroke-linecap: butt;
    stroke-linejoin: miter;
    shape-rendering: crispEdges;
  }
  .reticle-fill {
    fill: none;
    stroke: var(--fill-color);
    stroke-width: var(--fill-thickness);
    stroke-linecap: butt;
    stroke-linejoin: miter;
    shape-rendering: crispEdges;
  }

  .star-counter {
    position: absolute;
    bottom: -1.2rem;
    left: 50%;
    transform: translateX(-50%);
    padding: 0.35rem 0.95rem;
    background: var(--crimson);
    color: var(--parchment);
    border-radius: 0.65rem;
    font-family: 'Nunito', sans-serif;
    font-weight: 800;
    font-size: 1rem;
    line-height: 1;
    font-variant-numeric: tabular-nums;
    box-shadow: 1px 1px 0 rgba(0, 0, 0, 0.18);
    white-space: nowrap;
    z-index: 2;
  }
  .star-counter.hidden { display: none; }

  .score-header {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    margin-bottom: 0.6rem;
    border-bottom: 1px dashed rgba(26, 22, 18, 0.4);
    padding-bottom: 0.5rem;
    gap: 0.6rem;
  }
  /* Mode-name label on the left of the header. Matched to the
     title-screen .mode-btn .mode-name (Fraunces 700) so the player
     reads "this is the same thing you picked on the menu" — same
     typeface, same weight, smaller for the score-panel scale. */
  .score-label {
    font-family: 'Fraunces', serif;
    font-weight: 700;
    font-size: 1.05rem;
    color: var(--ink-text);
    letter-spacing: 0.02em;
    line-height: 1.1;
  }
  /* Right-side counter ("12/20", "53/54", "8 / 15"). Italic Fraunces
     so it reads as supporting metadata rather than competing with
     the bold serif label on the left. */
  .score-num {
    font-family: 'Fraunces', serif;
    font-weight: 600;
    font-style: italic;
    font-size: 0.95rem;
    color: var(--ink-text);
    letter-spacing: 0.05em;
    flex-shrink: 0;
  }
  /* Daily-20 variant — score-num doubles as a date stamp. Same font
     and treatment as the title-screen .mode-btn-stamp so the row
     "Daily 20 | MAY 15, 2026" matches the menu pill exactly. */
  .score-num.is-date {
    font-family: 'IBM Plex Mono', monospace;
    font-weight: 600;
    font-style: normal;
    font-size: 0.7rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
  }
  /* Bar + inline percent live on the same row. The bar takes the
     remaining horizontal space (flex: 1 1 auto) and the percent
     hugs its content on the right, so the bar automatically
     shrinks to fit whatever width is left after the percent —
     no manual width math, no "Accuracy" label gobbling vertical
     space below. */
  .score-bar-row {
    display: flex;
    align-items: center;
    gap: 0.55rem;
  }
  .score-bar-row.hidden { display: none; }
  .score-bar-wrap {
    flex: 1 1 auto;
    height: 10px;
    background: rgba(26, 22, 18, 0.12);
    border: 1px solid var(--ink-text);
    position: relative;
    overflow: hidden;
  }
  .score-bar-fill {
    position: absolute;
    inset: 0 100% 0 0;
    background: linear-gradient(90deg, var(--forest) 0%, var(--brass) 100%);
    transition: right 0.6s cubic-bezier(0.2, 0.9, 0.3, 1);
  }
  .score-bar-fill::after {
    content: "";
    position: absolute;
    inset: 0;
    background-image: repeating-linear-gradient(
      45deg,
      transparent 0 4px,
      rgba(255,255,255,0.12) 4px 6px
    );
  }
  .score-pct {
    font-family: 'Fraunces', serif;
    font-style: italic;
    font-size: 0.9rem;
    color: var(--ink-text);
    letter-spacing: 0.05em;
    flex-shrink: 0;
    line-height: 1;
  }
  /* Time-Attack score row. Composite formula (see _renderScore) that
     rewards both raw correct count and accuracy — accuracy multiplies
     into the raw count so the score grows superlinearly with
     correctness. Hidden by default, shown only when modeCfg.timed. */
  .ta-score-row {
    display: none;
    align-items: baseline;
    justify-content: space-between;
    margin-top: 0.5rem;
    padding-top: 0.5rem;
    border-top: 1px solid rgba(26, 22, 18, 0.18);
  }
  .ta-score-row.show { display: flex; }
  .ta-score-label {
    font-family: 'Fraunces', serif;
    font-weight: 700;
    font-size: 1.05rem;
    color: var(--ink-text);
    letter-spacing: 0.02em;
  }
  .ta-score-value {
    font-family: 'Fraunces', serif;
    font-weight: 700;
    font-style: italic;
    font-size: 1.35rem;
    color: var(--ink-text);
    line-height: 1;
    font-variant-numeric: tabular-nums;
  }

  /* ============ QUESTION PANEL (bottom-center) ============ */
  #question-panel {
    position: absolute;
    bottom: 2rem;
    left: 50%;
    transform: translateX(-50%);
    background: var(--parchment);
    color: var(--ink-text);
    padding: 1.4rem 1.8rem 1.6rem;
    border: 1px solid var(--ink-text);
    box-shadow: 5px 5px 0 var(--ink-mid),
                5px 5px 0 1px var(--brass);
    min-width: 540px;
    max-width: 720px;
    width: calc(100% - 4rem);
  }

  .question-prompt {
    text-align: center;
    margin-bottom: 1rem;
    padding-bottom: 0.8rem;
    border-bottom: 1px solid rgba(26, 22, 18, 0.2);
    position: relative;
  }
  .question-prompt::before, .question-prompt::after {
    content: "✦";
    position: absolute;
    bottom: -8px;
    color: var(--brass);
    background: var(--parchment);
    padding: 0 8px;
    font-size: 0.7rem;
  }
  .question-prompt::before { left: 50%; transform: translateX(-50%); }
  .question-prompt::after { display: none; }

  /* Timer-mode (daily / continent / marathon / time-attack):
     question-prompt loses its bottom ornament and dividing line —
     the #timer-bar (positioned between the prompt and the option
     cards in the DOM) takes over as the divider. Panel padding-top
     tightens because the prompt no longer needs breathing room
     above the now-omitted divider line. Endless mode (no
     with-timer-bar class) keeps the original style. */
  body.with-timer-bar #question-panel {
    padding-top: 1rem;
  }
  body.with-timer-bar .question-prompt {
    margin-bottom: 0;
    padding-bottom: 0;
    border-bottom: none;
  }
  body.with-timer-bar .question-prompt::before,
  body.with-timer-bar .question-prompt::after {
    display: none;
  }
  /* Spacing around the timer-bar in its new mid-panel slot.
     Vertically centered between prompt and options via equal
     margin top and bottom. */
  body.with-timer-bar #timer-bar {
    margin: 0.7rem 0;
  }
  .prompt-main {
    font-family: 'Fraunces', serif;
    font-weight: 400;
    font-style: normal;
    font-size: 1.25rem;
    color: var(--ink-text);
    letter-spacing: 0.02em;
    /* Reserve 2 lines worth of vertical space at all times so the
       question-panel doesn't grow/shrink between rounds when the
       continent name pushes the prompt across the wrap point.
       Centered with flex so 1-line prompts sit middle of the
       reserved slot rather than top-anchored with empty space
       below. */
    min-height: 2.4em;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .prompt-main em { font-weight: 700; color: var(--crimson); font-style: normal; }

  /* Viewport for the sliding option grids. Default overflow is
     VISIBLE so option-badge corners (top:-10px / right:-10px)
     can extend above the buttons after the player picks an
     answer — the badge sits in the small gap between this row
     and the prompt above. During the swap animation _render
     Options applies overflow:hidden inline so the two stacked
     grids are clipped at the slot boundaries while sliding;
     the inline style is cleared after 360ms, restoring visible
     overflow for the steady state. min-height matches a single
     grid row of option buttons. */
  #options {
    position: relative;
    overflow: visible;
    min-height: 4.2rem;
  }
  /* Expert-mode (free-text input) doesn't slide; the autocomplete
     dropdown also needs to overflow, so visible-overflow + no
     min-height override. */
  #options.expert-mode {
    overflow: visible;
    min-height: 0;
  }
  .options-grid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 0.7rem;
    /* Positioned within #options so two grids (outgoing + incoming)
       can stack on top of each other during the swap. The viewport
       clips them at top and bottom. */
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
  }
  /* Slide animation states — toggled by JS in _renderOptions. The
     "animating" class adds the transition so applying or removing
     "incoming"/"outgoing" lerps over 350ms. Without "animating",
     transform changes snap (used for the start frame of the
     incoming grid before the transition class is added). */
  .options-grid.options-grid-animating {
    transition: transform 350ms cubic-bezier(0.4, 0, 0.2, 1);
  }
  .options-grid.options-grid-incoming {
    transform: translateY(-100%);
  }
  .options-grid.options-grid-outgoing {
    transform: translateY(100%);
  }
  .option-btn {
    appearance: none;
    border: 1px solid var(--ink-text);
    background: rgba(26, 22, 18, 0.04);
    color: var(--ink-text);
    padding: 0.9rem 1rem;
    font-family: 'IBM Plex Mono', monospace;
    font-size: 0.85rem;
    font-weight: 500;
    letter-spacing: 0.03em;
    cursor: pointer;
    text-align: center;
    transition: all 0.18s ease;
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.7rem;
    /* Reserve 2 lines of opt-name height + padding so the row
       doesn't grow taller when one of the three names wraps
       (e.g., "São Tomé and Príncipe") while another round's
       options are all short (Chad / Cuba / Mali). Without this
       the question-panel shifted vertically between rounds. */
    min-height: 4.2rem;
  }
  .option-badge {
    position: absolute;
    top: -10px;
    right: -10px;
    width: 26px;
    height: 26px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    border: 2px solid var(--parchment);
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
    z-index: 5;
    pointer-events: none;
    transform: scale(0);
    animation: badge-pop 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
  }
  .option-badge.correct { background: var(--forest); }
  .option-badge.wrong   { background: var(--crimson); }
  .option-badge svg {
    width: 16px;
    height: 16px;
    stroke: #fff;
    stroke-width: 2.6;
    fill: none;
    stroke-linecap: round;
    stroke-linejoin: round;
  }
  @keyframes badge-pop {
    0%   { transform: scale(0); }
    70%  { transform: scale(1.15); }
    100% { transform: scale(1); }
  }
  .option-btn .opt-flag {
    height: 18px;
    width: auto;
    max-width: 28px;
    flex-shrink: 0;
    border: 1px solid rgba(26, 22, 18, 0.3);
    box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15);
    display: block;
    object-fit: cover;
  }
  .option-btn .opt-name {
    font-family: 'Fraunces', serif;
    font-size: 1rem;
    font-weight: 600;
    line-height: 1.2;
    min-width: 0;
  }
  @media (hover: hover) {
    .option-btn:hover:not(:disabled) {
      background: var(--option-hover-bg);
      color: var(--option-hover-fg);
      transform: translate(-2px, -2px);
      box-shadow: 2px 2px 0 var(--brass);
    }
  }
  .option-btn:disabled { cursor: default; }
  .option-btn.dim {
    opacity: 0.35;
    background: rgba(26, 22, 18, 0.02);
  }
  .option-btn.correct {
    background: var(--forest);
    color: var(--parchment);
    border-color: var(--forest);
    animation: correct-pulse 0.7s ease-out;
  }
  .option-btn.wrong {
    background: var(--crimson);
    color: var(--parchment);
    border-color: var(--crimson);
    animation: wrong-shake 0.5s ease-in-out;
  }
  @keyframes correct-pulse {
    0%   { transform: scale(1); box-shadow: 0 0 0 0 rgba(106, 138, 100, 0.7); }
    50%  { transform: scale(1.04); box-shadow: 0 0 0 16px rgba(106, 138, 100, 0); }
    100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(106, 138, 100, 0); }
  }
  @keyframes wrong-shake {
    0%, 100% { transform: translateX(0); }
    20% { transform: translateX(-6px); }
    40% { transform: translateX(6px); }
    60% { transform: translateX(-4px); }
    80% { transform: translateX(4px); }
  }

  /* ============ TIME-ATTACK ANSWER FEEDBACK BURST ============
     A big correct/wrong badge that pops up at viewport center the
     instant the player picks an answer in Time-Attack, then scales
     up (×1.5) and fades out over 1 second. Sized in vh because the
     pulsating ring around the pin is roughly camDist-stabilised to a
     fixed fraction of viewport height (the ring's screen diameter
     works out to ~25vh at the Time-Attack zoom distance), so 19vh
     here lands at the requested ≈75% of the ring's visible diameter
     across viewport sizes. Replaces the older "history strip" which
     cloned the just-answered options row under the prompt — that
     conflicted with how compressed Time-Attack already is and ate
     vertical space the options column wanted. */
  #feedback-burst {
    position: absolute;
    left: 50%;
    top: 50%;
    width: 19vh;
    height: 19vh;
    max-width: 220px;
    max-height: 220px;
    min-width: 90px;
    min-height: 90px;
    border-radius: 50%;
    border: 4px solid var(--parchment);
    box-shadow: 0 4px 22px rgba(0, 0, 0, 0.45);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 20;
    pointer-events: none;
    opacity: 0;
    /* Default state: scaled to its rendered size at viewport center,
       invisible. Adding .show triggers the burst animation that
       carries it to scale(1.5) + opacity 0 over 1 second. */
    transform: translate(-50%, -50%) scale(1);
  }
  #feedback-burst.correct { background: var(--forest); }
  #feedback-burst.wrong   { background: var(--crimson); }
  #feedback-burst.show {
    animation: feedback-burst-pop 0.77s ease-out forwards;
  }
  #feedback-burst svg {
    width: 58%;
    height: 58%;
    stroke: #fff;
    stroke-width: 2.6;
    fill: none;
    stroke-linecap: round;
    stroke-linejoin: round;
  }
  @keyframes feedback-burst-pop {
    0%   { opacity: 1; transform: translate(-50%, -50%) scale(1); }
    100% { opacity: 0; transform: translate(-50%, -50%) scale(1.5); }
  }

  /* Verdict line that appears after answer. Doubles as the
     home for the desktop keyboard-hint badges between rounds — the
     verdict element swaps innerHTML between the keys (default,
     standard MCQ only, .has-keys) and the answer feedback text
     (.has-text). Both share the same reserved min-height so the
     swap doesn't bounce the panel up and down. */
  .verdict {
    margin-top: 0.7rem;
    text-align: center;
    font-family: 'Fraunces', serif;
    font-style: italic;
    font-size: 0.95rem;
    letter-spacing: 0.04em;
    min-height: 28px;
    display: flex;
    align-items: center;
    justify-content: center;
    opacity: 0;
    transition: opacity 0.4s ease;
  }
  .verdict.show { opacity: 1; }
  .verdict.right { color: var(--forest); }
  .verdict.wrong { color: var(--crimson); }
  .verdict .verdict-truth {
    color: var(--ink-text);
    font-style: normal;
    font-weight: 600;
  }
  /* Keys mode: 3-column grid mirroring .options-grid so each key
     sits centered under its corresponding option button. Always
     opaque (no fade) — keys are a passive shortcut cue, not an
     animated feedback element. font-style/letter-spacing/font-family
     are reset because the verdict's italic-Fraunces defaults
     would propagate to the .kbd-key children. */
  .verdict.has-keys {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 0.7rem;
    opacity: 1;
    font-style: normal;
    letter-spacing: 0;
  }
  .verdict.has-keys .kbd-cell {
    display: flex;
    justify-content: center;
    align-items: center;
  }
  .kbd-key {
    /* Chunky bottom border fakes a 3D physical-key affordance — same
       trick CLI docs and design systems use for keyboard-hint badges.
       Bronze on parchment matches the brass accent palette of the
       panel; the subtle tinted fill plus the heavy bottom edge make
       it read as a physical key without competing with the option
       pills above. */
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 24px;
    height: 24px;
    padding: 0 5px;
    border: 1px solid var(--brass);
    border-bottom-width: 3px;
    border-radius: 4px;
    background: rgba(176, 135, 86, 0.07);
    color: var(--brass);
    font-family: 'IBM Plex Mono', monospace;
    font-size: 0.72rem;
    font-weight: 700;
    line-height: 1;
    user-select: none;
  }
  /* Wide spacebar — same affordance as .kbd-key, just stretched. Used
     in the TA share modal as a desktop-only "press Space = Play Again"
     hint. Lower-case label keeps the visual closer to a real spacebar
     legend (most keyboards don't shout "SPACE" in caps). Letter-
     spacing widens the word so it doesn't look cramped at the larger
     key width. */
  .kbd-spacebar {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 110px;
    height: 22px;
    padding: 0 12px;
    border: 1px solid var(--brass);
    border-bottom-width: 3px;
    border-radius: 4px;
    background: rgba(176, 135, 86, 0.07);
    color: var(--brass-bright);
    font-family: 'IBM Plex Mono', monospace;
    font-size: 0.62rem;
    font-weight: 600;
    line-height: 1;
    letter-spacing: 0.22em;
    text-transform: lowercase;
    user-select: none;
  }
  .share-key-hint {
    display: flex;
    justify-content: center;
    margin-top: 0.7rem;
  }
  .share-key-hint.hidden { display: none; }
  /* Time-attack hides only the verdict-TEXT variant. The keys
     variant stays visible during a TA run so the shortcut hints
     still appear on desktop — TA has no other verdict feedback
     (the feedback-burst centered on the globe replaces it). */
  body.time-attack-active .verdict.has-text { display: none; }

  /* ============ CORNER ORNAMENTS ============ */
  .corner-mark {
    position: absolute;
    color: var(--brass);
    font-size: 0.55rem;
    letter-spacing: 0.3em;
    text-transform: uppercase;
    opacity: 0.6;
    user-select: none;
    pointer-events: none;
  }
  .corner-mark.tr { display: none; }  /* superseded by #top-controls */
  .corner-mark.bl { bottom: 1.8rem; left: 1.8rem; }
  .corner-mark.br { bottom: 1.8rem; right: 1.8rem; text-align: right; }
  .corner-mark .glyph {
    font-family: 'Fraunces', serif;
    font-size: 1.6rem;
    color: var(--brass-bright);
    display: block;
    margin-bottom: 0.2rem;
    letter-spacing: 0;
  }

  /* Hide question panel until first round ready */
  #question-panel.hidden, #score-panel.hidden { opacity: 0; pointer-events: none; }
  #question-panel, #score-panel { transition: opacity 0.5s ease; }

  /* ---------- Top controls (volume + menu cluster, top-right) ---------- */
  #top-controls {
    position: absolute;
    top: 1.5rem;
    right: 1.5rem;
    z-index: 11;
    display: flex;
    gap: 0.5rem;
    align-items: flex-start;
    transition: opacity 0.5s ease;
  }
  #top-controls.hidden { opacity: 0; pointer-events: none; }

  /* Recenter button — separate element, positioned above the question
     -panel's top-left corner by JS (see _repositionRecenterBtn). It's
     intentionally outside #top-controls so it can stay live on the
     title screen, where #top-controls is hidden by body.intro-active.
     Pill-shaped (compass icon + "Recenter" label) rather than the
     circular .round-btn the other top-bar buttons use, so it doesn't
     try to compete with them for "icon-only round button" mind-space.
     Visibility is opacity-driven so the button fades in when the
     user starts dragging the globe and fades out on click. The
     fallback bottom/left below position it sensibly until JS lays
     it on top of the panel for the first time. */
  #btn-recenter {
    position: absolute;
    z-index: 11;
    bottom: 14rem;
    left: 2rem;
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.3rem 0.95rem 0.3rem 0.35rem;
    border: 1px solid var(--ink-text);
    border-radius: 999px;
    background: var(--parchment);
    cursor: pointer;
    font-family: 'Fraunces', serif;
    box-shadow: 3px 3px 0 var(--ink-mid), 3px 3px 0 1px var(--brass);
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.2s ease, transform 0.1s ease, box-shadow 0.1s ease;
  }
  #btn-recenter.show {
    opacity: 1;
    pointer-events: auto;
  }
  #btn-recenter:active {
    transform: translate(2px, 2px);
    box-shadow: 1px 1px 0 var(--ink-mid), 1px 1px 0 1px var(--brass);
  }
  #btn-recenter svg {
    width: 34px;
    height: 34px;
    flex-shrink: 0;
    display: block;
  }
  #btn-recenter .recenter-label {
    font-weight: 700;
    font-size: 0.95rem;
    letter-spacing: 0.02em;
    color: var(--ink-text);
    white-space: nowrap;
    line-height: 1;
  }
  @media (max-width: 720px) {
    #btn-recenter {
      bottom: 8rem;
      left: 0.6rem;
      padding: 0.25rem 0.8rem 0.25rem 0.3rem;
      box-shadow: 2px 2px 0 var(--ink-mid), 2px 2px 0 1px var(--brass);
    }
    #btn-recenter svg { width: 28px; height: 28px; }
    #btn-recenter .recenter-label { font-size: 0.85rem; }
    #btn-recenter:active {
      box-shadow: 1px 1px 0 var(--ink-mid), 1px 1px 0 1px var(--brass);
    }
  }

  /* Return button — title-screen only, shown when a country is
     focused. Clears the focus, restores the wide-idle view, and
     re-fades the menu back in. Mirrors #btn-recenter's structure
     exactly (parchment pill, 34x34 SVG icon, text label) so the
     two buttons read as a matched pair in the lower-left cluster. */
  #btn-return {
    position: absolute;
    z-index: 11;
    bottom: 2rem;
    left: 2rem;
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.3rem 0.95rem 0.3rem 0.35rem;
    border: 1px solid var(--ink-text);
    border-radius: 999px;
    background: var(--parchment);
    cursor: pointer;
    font-family: 'Fraunces', serif;
    box-shadow: 3px 3px 0 var(--ink-mid), 3px 3px 0 1px var(--brass);
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.2s ease, transform 0.1s ease, box-shadow 0.1s ease;
  }
  #btn-return.show {
    opacity: 1;
    pointer-events: auto;
  }
  #btn-return:active {
    transform: translate(2px, 2px);
    box-shadow: 1px 1px 0 var(--ink-mid), 1px 1px 0 1px var(--brass);
  }
  #btn-return svg {
    width: 34px;
    height: 34px;
    flex-shrink: 0;
    display: block;
  }
  #btn-return .return-label {
    font-weight: 700;
    font-size: 0.95rem;
    letter-spacing: 0.02em;
    color: var(--ink-text);
    white-space: nowrap;
    line-height: 1;
  }
  @media (max-width: 720px) {
    #btn-return {
      bottom: 0.6rem;
      left: 0.6rem;
      padding: 0.25rem 0.8rem 0.25rem 0.3rem;
      box-shadow: 2px 2px 0 var(--ink-mid), 2px 2px 0 1px var(--brass);
    }
    #btn-return svg { width: 28px; height: 28px; }
    #btn-return .return-label { font-size: 0.85rem; }
    #btn-return:active {
      box-shadow: 1px 1px 0 var(--ink-mid), 1px 1px 0 1px var(--brass);
    }
  }

  .round-btn {
    width: 44px;
    height: 44px;
    border-radius: 50%;
    border: 1px solid var(--ink-text);
    background: var(--parchment);
    color: var(--ink-mid);
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    box-shadow: 3px 3px 0 var(--ink-mid),
                3px 3px 0 1px var(--brass);
    transition: transform 0.1s ease, box-shadow 0.1s ease,
                background 0.15s ease, color 0.15s ease, opacity 0.2s ease;
    font-family: inherit;
  }
  .round-btn svg {
    width: 22px;
    height: 22px;
    color: var(--ink-text);
    display: block;
  }
  .round-btn:not(:disabled):active {
    transform: translate(2px, 2px);
    box-shadow: 1px 1px 0 var(--ink-mid),
                1px 1px 0 1px var(--brass);
  }
  .round-btn:disabled {
    opacity: 0.35;
    cursor: default;
  }
  @media (hover: hover) {
    .round-btn:not(:disabled):hover {
      background: var(--ink-text);
    }
    .round-btn:not(:disabled):hover svg { color: var(--parchment); }
  }
  /* Volume button: hide whichever icon doesn't apply */
  #btn-volume .icon-vol-off { display: none; }
  #btn-volume.muted .icon-vol-on { display: none; }
  #btn-volume.muted .icon-vol-off { display: block; }

  /* Volume popover (desktop only — hover-capable devices) */
  #volume-wrap { position: relative; }
  #volume-popover {
    position: absolute;
    top: calc(100% + 0.5rem);
    right: 0;
    background: var(--parchment);
    border: 1px solid var(--ink-text);
    box-shadow: 3px 3px 0 var(--ink-mid),
                3px 3px 0 1px var(--brass);
    padding: 0.6rem 0.75rem 0.7rem;
    width: 160px;
    opacity: 0;
    pointer-events: none;
    transform: translateY(-4px);
    transition: opacity 0.16s ease, transform 0.16s ease;
  }
  #volume-popover.show {
    opacity: 1;
    pointer-events: auto;
    transform: translateY(0);
  }
  .volume-popover-label {
    font-family: 'IBM Plex Mono', monospace;
    font-size: 0.6rem;
    letter-spacing: 0.25em;
    text-transform: uppercase;
    color: var(--crimson);
    font-weight: 700;
    margin-bottom: 0.45rem;
  }
  /* Aged-atlas styled range slider */
  #volume-slider {
    -webkit-appearance: none;
    appearance: none;
    width: 100%;
    height: 6px;
    background: rgba(26, 22, 18, 0.12);
    border: 1px solid var(--ink-text);
    outline: none;
    cursor: pointer;
  }
  #volume-slider::-webkit-slider-thumb {
    -webkit-appearance: none;
    appearance: none;
    width: 14px;
    height: 14px;
    border-radius: 50%;
    background: var(--brass);
    border: 1px solid var(--ink-text);
    cursor: pointer;
  }
  #volume-slider::-moz-range-thumb {
    width: 14px;
    height: 14px;
    border-radius: 50%;
    background: var(--brass);
    border: 1px solid var(--ink-text);
    cursor: pointer;
  }

  /* Mobile adjustments */
  @media (max-width: 720px) {
    #masthead { top: 0.7rem; }
    .masthead-title { font-size: 1rem; letter-spacing: 0.03em; }
    #score-panel {
      top: 2.6rem;
      left: 0.7rem;
      right: auto;
      bottom: auto;
      transform: none;
      width: calc(50vw - 0.85rem);
      max-width: none;
      padding: 0.5rem 0.75rem 0.6rem;
      box-shadow: 3px 3px 0 var(--ink-mid), 3px 3px 0 1px var(--brass);
    }
    #score-panel .score-header { margin-bottom: 0.35rem; padding-bottom: 0.3rem; }
    #score-panel .score-num { font-size: 0.85rem; }
    #score-panel .score-num.is-date { font-size: 0.6rem; }
    #score-panel .score-label { font-size: 0.9rem; }
    #score-panel .score-pct { font-size: 0.8rem; }
    #score-panel .score-bar-wrap { height: 7px; }
    #score-panel .score-bar-row { gap: 0.45rem; }
    #score-panel .ta-score-row { margin-top: 0.35rem; padding-top: 0.35rem; }
    #score-panel .ta-score-label { font-size: 0.85rem; }
    #score-panel .ta-score-value { font-size: 1.05rem; }

    /* Top buttons sit on the right half on mobile, vertically centered
       against the score panel for visual balance. */
    #top-controls {
      top: 2.6rem;
      right: 0.7rem;
      gap: 0.45rem;
    }
    .round-btn {
      width: 40px;
      height: 40px;
      box-shadow: 2px 2px 0 var(--ink-mid), 2px 2px 0 1px var(--brass);
    }
    .round-btn svg { width: 20px; height: 20px; }
    .round-btn:not(:disabled):active {
      box-shadow: 1px 1px 0 var(--ink-mid), 1px 1px 0 1px var(--brass);
    }

    #question-panel {
      min-width: 0;
      /* Equal padding on all four sides now that the verdict (which
         used to sit below the options) is hidden on mobile — the
         options grid is the last element, and this gives it the
         same breathing room below as the prompt has above. */
      padding: 0.75rem;
      /* Float above the home-indicator / safe-area zone. On real
         phones (notched iPhones, gesture-nav Android) the bottom of
         the viewport is occupied by the home indicator; without this
         the panel's bottom padding sits inside that zone and reads
         as "no padding". Device simulators usually report a zero
         inset, which is exactly why the padding showed there but
         vanished on hardware — env() falls back to 0 on those, so
         the original 0.6rem gap is unchanged where there's no
         inset. */
      bottom: calc(0.6rem + env(safe-area-inset-bottom, 0px));
      width: calc(100% - 1.2rem);
      box-shadow: 3px 3px 0 var(--ink-mid), 3px 3px 0 1px var(--brass);
    }
    .question-prompt {
      margin-bottom: 0.55rem;
      padding-bottom: 0.45rem;
    }
    .question-prompt::before { display: none; }
    /* Prompt is a single line on mobile. nowrap stops it wrapping to
       two lines (the desktop layout reserves 2 lines via min-height:
       2.4em — not wanted here), and _fitPromptMain() scales the font
       down if a long "...in North America" prompt would overflow the
       panel width. min-height drops to one line since wrapping can no
       longer happen. */
    .prompt-main {
      font-size: 1.1rem;
      white-space: nowrap;
      min-height: 0;
    }

    .options-grid {
      grid-template-columns: repeat(3, 1fr);
      gap: 0.4rem;
    }
    /* JS (_syncOptionsHeight) pins #options to the live grid height
       so the panel padding sits correctly under wrapped cards. Drop
       the desktop 4.2rem floor to a single mobile row here so short-
       name rounds stay compact instead of being forced to 4.2rem
       (which would re-introduce an uneven bottom gap). */
    #options { min-height: 3rem; }
    .option-btn {
      padding: 0.7rem 0.4rem;
      min-height: 3rem;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: flex-start;
      gap: 0.35rem;
    }
    /* Flag pinned at the top of every card. With justify-content:
       flex-start on the parent + this rule together, the flag's
       vertical position is identical across all three cards in a
       row, even when one card's name wraps to more lines than the
       others. Previously the parent's justify-content: center moved
       the flag down on short-name cards (centering the whole flag+
       name group within a taller-than-needed card), so the flags
       in a mixed-length row didn't line up. */
    .option-btn .opt-flag { flex: 0 0 auto; }
    /* Name fills remaining vertical space and centers its text
       within. Short 1-line names sit vertically centered in the
       leftover area below the flag; longer wrapping names expand
       to fill it. Either way the flag stays put. */
    .option-btn .opt-name {
      flex: 1 1 auto;
      display: flex;
      align-items: center;
      justify-content: center;
      font-size: 0.92rem;
      line-height: 1.15;
      overflow-wrap: break-word;
      word-break: normal;
    }

    /* Mobile hides the verdict text completely. The card already
       flips to reveal the answer, so the extra "Not quite. The
       territory was X" line is redundant on a phone — and removing
       it (rather than reserving the old 2.6em min-height for a
       potential 2-3 line wrap) shortens the question panel, which
       is the whole point here. Desktop keeps the verdict: this
       rule is inside the max-width:720px query only. */
    .verdict { display: none; }

    .corner-mark { display: none; }
  }

/* ---------------------------------------------------------------
   Daily sub-page (v1.24.0) — drill-in to Discovery / Star Rush.
   Mirrors the continents page structurally; .menu-drill-toggle
   styles (shared with continents) carry the toggle look. The
   reveal fades in once a mode is selected; rundown content
   (squares + maybe star pill + Play) is centered.
   --------------------------------------------------------------- */
.menu-daily-modes {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.7rem;
}
.daily-reveal {
  opacity: 0;
  transition: opacity 0.26s ease;
  margin-top: 0.9rem;
}
.daily-reveal.hidden { display: none; }
.daily-reveal.visible { opacity: 1; }
.daily-date {
  font-family: 'Fraunces', serif;
  font-size: 0.85rem;
  color: var(--brass-bright);
  text-align: center;
  margin-bottom: 0.7rem;
  opacity: 0.9;
}
.daily-rundown {
  display: flex;
  flex-direction: column;
  align-items: center;   /* centers both the squares and the star pill */
  gap: 0.7rem;
}
.daily-play-row {
  display: flex;
  justify-content: center;
  margin-top: 0.9rem;
}
.daily-play.mode-btn {
  width: auto;
  align-self: center;
  justify-content: center;
  padding: 0.7rem 2.6rem;
}

/* Discovery clue line — under the prompt. Lives on the parchment
   panel, so the text is ink. Slightly heavier (600) to read clearly
   alongside the bolder main prompt line. */
.prompt-clue {
  margin-top: 0.4rem;
  font-family: 'Fraunces', serif;
  font-weight: 600;
  font-size: 0.95rem;
  color: var(--ink-text);
  opacity: 0.85;
  text-align: center;
}
.prompt-clue.hidden { display: none; }

/* Cultural-icon / flag emoji embedded inside a clue. `transform:
   scale()` enlarges the glyph visually (~2× the surrounding text)
   without touching the layout box — so the .prompt-clue line stays
   at its base text height and nothing above or below shifts. The
   horizontal margin breathes the scaled glyph past adjacent
   punctuation; vertical bleed lands harmlessly in the panel's
   padding-bottom and the gap above the clue line. */
.clue-emoji {
  display: inline-block;
  transform: scale(2);
  transform-origin: center center;
  margin: 0 0.6em;
}

/* ---------------------------------------------------------------
   Discovery debug tab (v1.25.0) — clue weight editors, current-
   question path breakdown, and today's clue-type distribution.
   --------------------------------------------------------------- */
.debug-clue-note {
  font-size: 0.72rem;
  opacity: 0.75;
  margin: 0.3rem 0;
  line-height: 1.35;
}
.debug-clue-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.72rem;
  margin-top: 0.2rem;
}
.debug-clue-table th,
.debug-clue-table td {
  text-align: left;
  padding: 0.18rem 0.4rem;
  border-bottom: 1px solid rgba(255,255,255,0.08);
}
.debug-clue-table th { opacity: 0.6; font-weight: 600; }
.debug-clue-table tr.dbg-candidate td { color: #8fd39a; }
.debug-clue-table tr.dbg-clash td     { color: #e0b15a; }
.debug-clue-table tr.dbg-absent td    { opacity: 0.4; }
.debug-clue-table tr.dbg-reveal td    { color: #d57878; }   /* country-name giveaway, excluded */