/* ══════════════════════════════════════════════════════════════════════════
   GLASS LAGOON — visual skin for the Multiply.Cards student game (mdxs/)
   ──────────────────────────────────────────────────────────────────────────
   PURE ADDITIVE OVERRIDE. This file is loaded AFTER times-tables.css,
   aquarium.css and rewards.css and re-skins the existing DOM only.
   No selectors here require any JS or markup change.

   Design source of truth: docs/mockups/student-game-glass-prototype.html
   Material language:  frosted glass  = chrome (header, panels, shop cases)
                       candy bevel    = touchable (cards, buttons, coins)
                       water gradient = the world behind everything

   HARD RULES respected (docs/GLASS_LAGOON_IMPLEMENTATION_SCOPING.md):
   1. Card faces keep their literal-hex gradient backgrounds (Safari repaint
      bug) — this file NEVER sets `background` on .card-front/.card-back.
      Bevels are box-shadow + free pseudo-elements only.
      (.card-back::after is taken by the mixed-mode shared-answer dot, so the
       back-face gloss lives on .card-back::before instead.)
   2. No CSS containment is added to .grid or .card.
   3. The aquarium tank is never blurred/overlaid and #aquarium::before is
      untouched (it belongs to the purchased backgrounds). Only the frame
      and the chrome around/over it are skinned.
   4. `!important` is used only where the app writes inline styles at runtime
      (applyColorScheme → .menu-container / slide-menu .menu-header) or where
      the app's own rule is already !important (matched-card shadow, coin tab).
   ══════════════════════════════════════════════════════════════════════════ */

/* ═══════════════ 0. iOS FIXED-NAV FIX ═══════════════
   times-tables.js (~line 10503) applies `transform: scale(0.9995)` to <html> as
   a Safari repaint workaround for language-URL arrivals (/jp/, /cn/, …). A
   transform on <html> makes position:fixed elements (the top nav) resolve
   against <html> instead of the viewport — on iOS Safari this shifts the nav and
   clips the top card row a moment after load (the workaround runs on a 500ms
   timer). Its trigger is over-broad (ANY non-'default' voice fires it), and the
   paint bug it guards never occurs at /mdxs/ (not a language URL) — so switch it
   off here. !important beats the JS-set inline style. */
html {
  transform: none !important;
}

/* ═══════════════ 1. TOKENS ═══════════════ */
:root {
  /* ink + foam */
  --gl-ink: #1B2440;
  --gl-foam: #F4FBFF;
  --gl-foam-70: rgba(244, 251, 255, 0.72);
  --gl-foam-50: rgba(244, 251, 255, 0.5);

  /* sun-candy (primary action) */
  --gl-sun: #FC8008;
  --gl-sun-hi: #FFA547;
  --gl-sun-edge: #C95F00;

  /* gold candy coins */
  --gl-gold-hi: #FFE08A;
  --gl-gold-lo: #F5A623;
  --gl-gold-ink: #6B3D00;
  --gl-gold-edge: #C97F10;

  /* lagoon water world */
  --gl-water-0: #8CE8F4;
  --gl-water-1: #14B9D9;
  --gl-water-2: #1B66DE;
  --gl-water-3: #1D3AA8;

  /* blues / greens for candy buttons */
  --gl-blue: #4281F2;
  --gl-blue-deep: #2563EB;
  --gl-blue-l: #70a7ef;
  --gl-blue-edge: #0C46A8;
  /* the logo's royal blue (sampled from resources/times-logo.png) — used for
     the main nav tab pills (active fill + inactive label) */
  --gl-royal: #425FF0;
  --gl-royal-hi: #6178f6;
  --gl-royal-deep: #3450e6;
  --gl-royal-edge: #2035ad;
  --gl-royal-ink: #1f2f8a; /* inactive labels — deep blue (between navy + royal) */
  --gl-grn-l: #34d399;
  --gl-grn-d: #059669;
  --gl-grn-edge: #0E7A36;

  /* frosted glass recipe parts */
  --gl-glass-hi: rgba(255, 255, 255, 0.20);
  --gl-glass-lo: rgba(255, 255, 255, 0.06);
  --gl-glass-line: rgba(255, 255, 255, 0.36);
  --gl-glass-border: rgba(255, 255, 255, 0.28);
  --gl-glass-shadow: 0 10px 22px rgba(5, 18, 58, 0.28), inset 0 1px 0 rgba(255, 255, 255, 0.3);

  /* light-frost (content sheets, modals) */
  /* frosted-card recipe shared by the profile dropdown, the Settings/Languages
     modals and the getting-started slideshow. Opacity matches the bottom popups
     (.slide-menu: 0.98 → 0.96); the shadow is a layered lift (faint edge ring +
     near + far) so each card sits clearly above the content behind it. Each
     box-shadow usage adds !important so it survives flat-experiment.css. */
  --gl-frost-bg: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(240, 249, 255, 0.96));
  --gl-frost-border: rgba(255, 255, 255, 0.9);
  --gl-frost-shadow:
    0 0 0 1px rgba(5, 18, 58, 0.06),
    0 6px 14px rgba(5, 18, 58, 0.22),
    0 16px 34px rgba(5, 18, 58, 0.30),
    inset 0 1px 0 rgba(255, 255, 255, 0.9);

  /* fonts */
  --gl-font-display: 'Fredoka', 'Nunito', 'Noto Sans SC', 'Microsoft YaHei', 'PingFang SC', sans-serif;
  --gl-font-body: 'Nunito', 'Noto Sans SC', 'Microsoft YaHei', 'PingFang SC', 'Hiragino Sans GB', 'Heiti SC', 'WenQuanYi Micro Hei', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;

  /* retune the app's own theme tokens toward the lagoon
     (used by .slide-tab.active, .tab-indicator, .slide-menu .btn, inline
      activate button, etc. — colour-only, no behaviour) */
  --menu-blue: #2563EB;
  --button-blue: #4281F2;
  --button-blue-hover: #2E66D9;
  --panel-blue: #EAF4FF;
}

/* ═══════════════ 2. FONTS ═══════════════ */
body {
  font-family: var(--gl-font-body);
  color: var(--gl-ink);
}

h1, h2, h3, h4,
.slide-title,
.welcome-title,
.mobile-mode-label,
.menu-header h2,
.ws-slide-title,
.ps-title,
.pt-slide-title,
.shop-item-name,
.ws-btn,
.ps-title + .ps-body strong {
  font-family: var(--gl-font-display);
  font-weight: 600;
}

/* card numerals + practice slideshow numerals */
.card-front .card-content,
.card-back .card-content,
.card-back .card-content span,
.slideshow-card .card-content,
.performance-display .percentage-text {
  font-family: var(--gl-font-display);
}

/* buttons + pills speak Fredoka */
.play-button,
.modal-button,
.tab-buttons button,
.score,
.buy-button,
.category-tab,
.welcome-continue-btn,
.primary-btn,
.slide-menu .btn,
.tab-grid-button,
.smart-hint {
  font-family: var(--gl-font-display);
}

/* ═══════════════ 3. THE WATER WORLD ═══════════════ */
/* applyColorScheme() writes an inline gradient onto <html>, so the lagoon
   lives on a fixed body::before layer that paints over the canvas but
   behind every piece of content (body has no stacking context; negative
   z-index children sit above the root background, below all content). */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -2;
  pointer-events: none;
  background:
    radial-gradient(150% 54% at 50% -16%, rgba(210, 248, 255, 0.72), transparent 58%),
    linear-gradient(180deg, var(--gl-water-0) 0%, var(--gl-water-1) 24%, var(--gl-water-2) 60%, var(--gl-water-3) 100%);
}

/* the app container floats in that water */
.times-tables-app-container {
  background-color: transparent;
}

/* ambient god-rays + drifting bubbles BEHIND all content.
   One compositor-friendly layer: transform/opacity animation only. */
body::after {
  content: "";
  position: fixed;
  top: -12%;
  bottom: -12%;
  left: -6%;
  right: -6%;
  z-index: -1;
  pointer-events: none;
  opacity: 0.55;
  background:
    /* god rays */
    linear-gradient(78deg, transparent 28%, rgba(255, 255, 255, 0.16) 36%, rgba(255, 255, 255, 0.03) 45%, transparent 50%),
    linear-gradient(104deg, transparent 55%, rgba(255, 255, 255, 0.11) 63%, transparent 71%),
    /* soft bubbles */
    radial-gradient(9px 9px at 12% 76%, rgba(255, 255, 255, 0.55), rgba(255, 255, 255, 0) 70%),
    radial-gradient(6px 6px at 33% 58%, rgba(255, 255, 255, 0.45), rgba(255, 255, 255, 0) 70%),
    radial-gradient(11px 11px at 72% 82%, rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0) 70%),
    radial-gradient(5px 5px at 88% 52%, rgba(255, 255, 255, 0.4), rgba(255, 255, 255, 0) 70%),
    radial-gradient(7px 7px at 53% 68%, rgba(255, 255, 255, 0.45), rgba(255, 255, 255, 0) 70%);
  animation: glAmbientDrift 18s ease-in-out infinite alternate;
}

@keyframes glAmbientDrift {
  from { transform: translate3d(0, 18px, 0); opacity: 0.4; }
  to   { transform: translate3d(10px, -22px, 0); opacity: 0.62; }
}

/* ═══════════════ 4. FLOATING GLASS HEADER ═══════════════ */
/* !important beats the inline gradient applyColorScheme() writes on load */
.menu-container {
  background: linear-gradient(165deg, rgba(255, 255, 255, 0.22), rgba(255, 255, 255, 0.07)) !important;
  -webkit-backdrop-filter: blur(12px) saturate(1.2);
  backdrop-filter: blur(12px) saturate(1.2);
  border-bottom: 1px solid var(--gl-glass-line);
  box-shadow: 0 10px 22px rgba(5, 18, 58, 0.28), inset 0 1px 0 rgba(255, 255, 255, 0.3);
}

.app-logo {
  filter: drop-shadow(0 3px 6px rgba(6, 20, 64, 0.35));
}

.mobile-mode-label {
  /* The glass header is now light frosted glass (.menu-container above), so the
     old white label + dark shadow (designed for the retired dark-blue header)
     read as near-invisible. Recolour to the same dark navy as .gl-sheet-title
     and swap the dark drop-shadow for a soft light lift. Beats the mobile
     `color: white` in times-tables.css (later stylesheet, equal specificity). */
  /* Slimmer rounded nav font (Nunito, already loaded — much lighter than the
     Fredoka display face) + indigo #271ECC, per request. !important beats the
     mobile `color: white` in times-tables.css (later stylesheet, equal spec). */
  font-family: 'Nunito', 'Segoe UI', Arial, sans-serif;
  font-weight: 700 !important;
  color: #271ECC !important;
  text-shadow: 0 1px 2px rgba(255, 255, 255, 0.6);
}

/* tab buttons → glass pills; active tab → sun candy */
.tab-buttons button {
  border-radius: 999px;
  padding: 8px 18px;
  /* rounded nav font (Nunito) + indigo, matching the mobile label */
  font-family: 'Nunito', 'Segoe UI', Arial, sans-serif;
  font-weight: 700;
  font-size: 15.5px;
  color: #271ECC; /* inactive label → indigo on the glass pill */
  background: rgba(255, 255, 255, 0.10);
  border: 1px solid rgba(255, 255, 255, 0.24);
  text-shadow: 0 1px 1px rgba(255, 255, 255, 0.45); /* light emboss suits the dark ink on glass */
  opacity: 1;
}

.tab-buttons button:hover:not(.active-tab) {
  background: rgba(255, 255, 255, 0.18);
  opacity: 1;
}

.tab-buttons button.active-tab {
  background: linear-gradient(160deg, var(--gl-royal-hi), var(--gl-royal-deep));
  border-color: transparent;
  color: #fff; /* white on the royal-blue pill (matches the logo) */
  box-shadow: inset 0 2px 2px rgba(255, 255, 255, 0.45), 0 3px 0 var(--gl-royal-edge), 0 8px 14px rgba(18, 30, 96, 0.35);
  opacity: 1;
  text-shadow: 0 1px 3px rgba(12, 22, 80, 0.4);
}

/* round icon buttons (settings gear, hamburger) → glass bubbles */
.icon-button {
  background: rgba(255, 255, 255, 0.14);
  border: 1px solid rgba(255, 255, 255, 0.3);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.35);
}

/* settings gear + hamburger → near-black icons for contrast on the light header (per request).
   Lucide renders these as <svg stroke="currentColor">, so colour + explicit stroke both apply.
   Scoped to these two header buttons by id, so icon-buttons over dark backgrounds are unaffected. */
#settingsToggleBtn,
#mobileMenuToggle {
  color: #1a1a1a;
}
#settingsToggleBtn svg,
#mobileMenuToggle svg {
  stroke: #1a1a1a;
}

/* Profile avatar: the nav-bar "Account" button shows the anonymous-user.webp
   portrait instead of the Lucide circle-user glyph. The image fills the round
   button (object-fit: cover on the img); the Lucide <svg>/<i> is hidden. Kept
   as an additive override so the base markup (and createIcons()) is untouched.
   Falls back to nothing if the image is missing, so ship resources/anonymous-user.webp. */
#settingsToggleBtn {
  padding: 0;
  background-image: url("/resources/anonymous-user.webp");
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  border-radius: 50%;
  overflow: hidden;
}
#settingsToggleBtn svg,
#settingsToggleBtn i {
  display: none !important; /* replaced by the webp avatar above */
}

.icon-button:hover {
  background-color: rgba(255, 255, 255, 0.26);
}

/* ── score pill (#scoreDisplay, one element at every width) → gold candy coin ── */
/* neutral frosted pill (was gold candy — hard to read the coloured metric
   numbers on it). White glass + near-black numbers reads cleanly on the
   light header; the icons stay colour-coded. */
.score {
  color: #1a1a1a;
  background: rgba(255, 255, 255, 0.82);
  -webkit-backdrop-filter: blur(10px) saturate(1.1);
  backdrop-filter: blur(10px) saturate(1.1);
  border: 1px solid rgba(255, 255, 255, 0.7);
  border-radius: 999px;
  box-shadow: 0 2px 8px rgba(5, 18, 58, 0.12); /* flat-experiment suppresses this; kept for the 3D look */
  font-weight: 600;
}

/* all three metric numbers near-black for readability (icons stay colour-coded) */
.score-item-value,
.score-item-value.incorrect {
  color: #1a1a1a;
}

/* Score tally icons → Lucide glyphs (times-tables.js builds inline SVGs we can't
   edit; swap them in the override layer via CSS mask). Correct → Lucide `check`
   in the same green; wrong → Lucide `x` in the same red. Matched by each SVG's
   stroke colour so tally order / rebuilds don't matter.

   The wrong glyph used to be `triangle-alert` here while the mobile layer masked
   an X over it, so the same stat wore two different icons depending on viewport.
   It is the X in both now, set once — the mobile rule only recolours it.

   stroke-width 3 (Lucide ships 2) so both marks hold their weight at the 20px
   they render at; at 2 the check in particular read as thin next to the tally
   numbers, which are bold. */
.score-icon-wrapper:has(> svg[stroke="#28a745"]) > svg,
.score-icon-wrapper:has(> svg[stroke="#dc3545"]) > svg {
  display: none;
}
.score-icon-wrapper:has(> svg[stroke="#28a745"])::before,
.score-icon-wrapper:has(> svg[stroke="#dc3545"])::before {
  content: "";
  width: 20px;
  height: 20px;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: center;
  mask-position: center;
  -webkit-mask-size: contain;
  mask-size: contain;
}
.score-icon-wrapper:has(> svg[stroke="#28a745"])::before {
  background-color: #28a745; /* same green as the original check */
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E");
}
.score-icon-wrapper:has(> svg[stroke="#dc3545"])::before {
  background-color: #dc3545; /* same red as the original cross */
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M18 6 6 18'/%3E%3Cpath d='m6 6 12 12'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M18 6 6 18'/%3E%3Cpath d='m6 6 12 12'/%3E%3C/svg%3E");
}

.score .score-coin-count {
  color: #1a1a1a !important; /* override the JS inline #d97706 / #aaa so the coin count matches */
}

/* the little progress capsule (the % bar) — deep-water ring, white text */
.score-progress-bar {
  background-color: #0E2B66;
  box-shadow: inset 0 2px 4px rgba(4, 14, 44, 0.6);
}

/* Centre the score pill in the header in mobile play mode (the bottom dock
   owns navigation now, so the top bar has room). .menu-bar is
   position:relative; inset:0 + margin:auto centres it WITHOUT a transform, so
   the score-pulse scale animation doesn't fight the positioning. The cat stays
   left and the gear stays right in the flex flow. */
@media (max-width: 768px) {
  #scoreDisplay.visible {
    position: absolute;
    inset: 0;
    margin: auto;
    width: -moz-fit-content;
    width: fit-content;
    height: -moz-fit-content;
    height: fit-content;
  }
}

/* ═══════════════ 5. DROP PANEL, SELECTS, PLAY BUTTON ═══════════════ */
.menu-panel-container {
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.93), rgba(236, 247, 255, 0.88));
  border-bottom-left-radius: 22px;
  border-bottom-right-radius: 22px;
}

.menu-panel-container.visible {
  border-top: 1px solid rgba(255, 255, 255, 0.85);
  box-shadow: 0 18px 34px rgba(5, 18, 58, 0.28), inset 0 1px 0 #fff;
}

/* Desktop (>768px): the game tabs' wide pop-down strip becomes a COMPACT
   FROSTED CARD dropped just under the tab bar, left-justified to the active
   tab's pill — the same light-frost recipe as the Settings card below, so
   every top tab opens the same-looking card. desktop-tab-menus.js sets the
   exact inline left/top (anchor = the tab button carrying .active-tab) and
   clamps the card to the right viewport edge; the values here are only
   first-paint fallbacks. position:fixed resolves against .menu-container
   (its backdrop-filter makes the fixed header the containing block), which
   is full-width at top:0, so viewport-px coordinates land exactly (see the
   Settings-face NOTE further down). The app's own .visible toggle still owns
   open/close — only the presentation changes: the max-height accordion is
   retired (the card is out of flow now) in favour of a small fade/drop on
   open and an instant hide on close (no transition in the hidden state, so
   closing never shows an empty husk — the app empties the panel in the same
   tick). Mobile (≤768px) is untouched: times-tables.css force-hides this
   container there and the bottom sheets own the options UI. */
@media (min-width: 769px) {
  .menu-panel-container {
    position: fixed;
    top: calc(var(--desktop-menu-bar-height, 65px) + 8px); /* JS refines */
    left: 12px;                                            /* JS refines */
    width: 344px;
    max-width: calc(100vw - 16px);
    max-height: none;
    margin: 0;
    padding: 14px;
    overflow: visible; /* the games-mode custom dropdown may drop past the card edge */
    /* same frosted-card recipe as the profile dropdown / modals (shared vars):
       popup-matched opacity + layered lift shadow. !important on the shadow so it
       survives flat-experiment.css's universal `* { box-shadow: none !important }`. */
    background: var(--gl-frost-bg);
    border: 1px solid var(--gl-frost-border);
    border-radius: 18px;
    box-shadow: var(--gl-frost-shadow) !important;
    visibility: hidden;
    opacity: 0;
    transform: translateY(-8px);
    pointer-events: none;
    transition: none;
  }

  .menu-panel-container.visible {
    max-height: none;
    padding: 14px;
    border: 1px solid var(--gl-frost-border);
    border-radius: 18px;
    box-shadow: var(--gl-frost-shadow) !important;
    visibility: visible;
    opacity: 1;
    transform: translateY(0);
    pointer-events: auto;
    transition: opacity 0.2s ease, transform 0.2s ease;
    /* sit above the frosted scrim added below (scrim 1490 < card 1495; the
       header at z 1500 stays crisp above both, exactly like the help modal) */
    z-index: 1495;
  }

  /* Lightly darken the game grid behind the open tab card (Practice/Play/
     Progress), the same dim the mobile options bottom sheet already casts
     (.menu-overlay) — no blur. The card is out of flow (position:fixed +
     transform), so its own ::before can't cover the viewport. Anchor the scrim
     on .times-tables-app-container::after instead (body's own ::before/::after
     already paint the lagoon water + god-rays). That wrapper has no transform,
     so the fixed pseudo resolves to the viewport and stacks by z-index in the
     root context. z 1490 sits below the card (1495) and the crisp header
     (1500). pointer-events:none keeps the app's existing outside-click /
     mouse-leave close untouched. Rewards opens no panel, so it never shows. */
  body:has(#menuPanelContainer.visible) .times-tables-app-container::after {
    content: "";
    position: fixed;
    inset: 0;
    z-index: 1490;
    background: rgba(6, 20, 59, 0.45);
    pointer-events: none;
    animation: glTabScrimIn 0.2s ease both;
  }
  @keyframes glTabScrimIn {
    from { opacity: 0; }
    to   { opacity: 1; }
  }

  /* ─── Guard: never show the tab-dropdown card as an empty white "husk" ─────
     The panel is emptied (innerHTML="") on tab-switch and repopulated a tick
     later by the base app + table-picker.js / play-start-btn.js. In that gap
     (seen "sometimes" when opening Play and clicking around) the frosted card
     — and its grid scrim — could flash as an empty white rounded bar over the
     top cards. Only present the card + scrim when the open container actually
     holds a VISIBLE, POPULATED panel (same :has() approach as the scrim rule
     above; browsers without :has() keep the prior behaviour). When populated,
     :has(.menu-panel.visible:not(:empty)) is true and these rules don't match. */
  .menu-panel-container.visible:not(:has(.menu-panel.visible:not(:empty))) {
    visibility: hidden !important;
    opacity: 0 !important;
    box-shadow: none !important;
    border-color: transparent !important;
    background: transparent !important;
    pointer-events: none !important;
  }
  body:has(#menuPanelContainer.visible):not(:has(.menu-panel.visible:not(:empty))) .times-tables-app-container::after {
    display: none !important;
  }

  /* inside the card the tab's controls stack VERTICALLY
     (the wide strip used to spread them in one row) */
  .menu-panel.visible {
    flex-direction: column;
    align-items: stretch;
    gap: 12px;
    min-height: 0;
  }

  .menu-panel .table-selector,
  .menu-panel .custom-game-mode-dropdown {
    width: 100%;
    min-width: 0;
  }

  .menu-panel .play-button {
    width: 100%;
    flex: 1 1 auto;
    margin: 0;
  }

  /* progress card: the view toggle and the heart/coin pills can't share one
     row at card width — let their inline-styled flex row wrap into rows,
     and keep the pills row itself inside the card (it's a hair wider than
     the card's content box once the help button is counted) */
  #progressPanel .aquarium-view-buttons {
    flex-wrap: wrap;
    row-gap: 10px;
  }

  #progressPanel .progress-header {
    max-width: 100%;
    flex-wrap: wrap;
    row-gap: 8px;
  }
}

/* selects → soft white pills (background-image arrow preserved:
   only background-color is touched) */
.table-selector,
.slide-menu .form-select,
#settingsDropdown .settings-select,
.gl-pref-modal .settings-select {
  border: 1px solid #D6E6FF;
  border-radius: 12px;
  background-color: #fff;
  color: var(--gl-ink);
  font-weight: 700;
  box-shadow: inset 0 1px 2px rgba(27, 36, 64, 0.06), 0 2px 6px rgba(5, 18, 58, 0.08);
}

.table-selector:focus,
.slide-menu .form-select:focus,
#settingsDropdown .settings-select:focus,
.gl-pref-modal .settings-select:focus {
  outline: none;
  border-color: var(--gl-blue);
  box-shadow: 0 0 0 3px rgba(66, 129, 242, 0.25);
}

/* the Play button → THE candy button, now the same blue used in the popups
   (matches the sheet's Start Game button: --gl-blue-l → --gl-blue-deep) */
.play-button {
  background: linear-gradient(160deg, var(--gl-blue-l), var(--gl-blue-deep));
  border-radius: 14px;
  font-weight: 600;
  font-size: 17px;
  box-shadow: inset 0 2px 2px rgba(255, 255, 255, 0.5), 0 4px 0 var(--gl-blue-edge), 0 10px 16px rgba(6, 24, 70, 0.35);
  text-shadow: 0 1px 3px rgba(6, 24, 70, 0.35);
}

.play-button:hover {
  background: linear-gradient(160deg, #85b4f2, var(--gl-blue-deep));
  filter: brightness(1.04);
}

.play-button:active {
  transform: translateY(3px);
  box-shadow: inset 0 2px 2px rgba(255, 255, 255, 0.5), 0 1px 0 var(--gl-blue-edge), 0 4px 8px rgba(6, 24, 70, 0.3);
}

/* desktop panel help (?) icons → small + centred. Practice/Play were pushed to
   the bottom-right by an inline margin-left:auto (looked isolated); align-self
   centres them in the column regardless. Shrink all three to match the neat
   Progress-panel look. */
#desktop-practice-help,
#desktop-games-help {
  align-self: center !important;
  margin-left: 0 !important;
  margin-right: 0 !important;
}
#desktop-practice-help i, #desktop-practice-help svg,
#desktop-games-help i, #desktop-games-help svg,
#desktop-progress-help i, #desktop-progress-help svg {
  width: 30px !important;
  height: 30px !important;
}

/* Play Again keeps its green identity — candy green */
#playAgainButtonUnique {
  background: linear-gradient(160deg, var(--gl-grn-l), var(--gl-grn-d));
  box-shadow: inset 0 2px 2px rgba(255, 255, 255, 0.45), 0 4px 0 var(--gl-grn-edge), 0 10px 16px rgba(14, 122, 54, 0.35);
}

#playAgainButtonUnique:hover {
  background: linear-gradient(160deg, #4ade80, var(--gl-grn-d));
}

/* settings dropdown → light-frost sheet */
#settingsDropdown {
  background: var(--gl-frost-bg);
  border: 1px solid var(--gl-frost-border);
  border-radius: 18px;
  box-shadow: var(--gl-frost-shadow) !important;
}

#settingsDropdown button:hover {
  background-color: #EAF2FF;
}

#settingsDropdown .settings-section {
  border-top-color: rgba(27, 36, 64, 0.1);
}

#settingsDropdown .settings-heading {
  color: #3A4A6B;
}

/* ── PROFILE menu extras ────────────────────────────────────────────────────
   The header person button opens #settingsDropdown (account actions). Its
   ⚙ Settings row (#openSettingsModalBtn, wired by settings-modal.js) opens
   the #settingsModal preferences modal — styled in §7b below. The row itself
   inherits the dropdown's standard button look (times-tables.css); only the
   emoji gear glyph needs sizing so it aligns like the lucide icons beside it. */
/* the Settings / Languages rows both use an emoji glyph span; size them
   alike so they align like the lucide icons on the other dropdown rows */
#settingsDropdown .settings-gear-glyph {
  width: 20px;
  flex-shrink: 0;
  font-size: 17px;
  line-height: 1;
  text-align: center;
}

/* ── mode popups carry NO account chrome ────────────────────────────────────
   Account actions (Sign In / Player Active / Sign Out / Switch User) live
   ONLY in the top-right profile menu (#settingsDropdown). The per-tab
   options surfaces reuse the retired drawer's markup (#slideMenu), whose
   head still ships account chrome — hide it at EVERY width (times-tables.css
   repeats this inside its ≤768px block; here it is the width-independent
   guarantee).

   The account controls live in the menu's 2nd element (the .form-group right
   after the header). license-handler.js's updatePremiumInterface() SWAPS that
   form-group's innerHTML at runtime — signed-out shows #mobileUnlockBtn
   ("Sign In"); a share-code user like the screenshot's gets a fresh
   #mobileShareCodeLogoutBtn ("Sign Out") + a "Manage account" <a href="/auth/">;
   a licence user gets #mobileManageBtn. So the OLD :has(> #mobileUnlockBtn)
   rule stopped matching once that button was replaced, leaving the block
   visible. Match the container by POSITION (it is never re-created, only its
   contents are) AND by every control it can hold, then also hide the sibling
   Switch-User section and bare Sign-Out button. */
#slideMenu > .form-group:nth-child(2),
#slideMenu > .form-group:has(> #mobileUnlockBtn),
#slideMenu > .form-group:has(> #mobileManageBtn),
#slideMenu > .form-group:has(> #mobileShareCodeLogoutBtn),
#slideMenu > .form-group:has(> a[href="/auth/"]),
#slideMenu #mobileUnlockBtn,
#slideMenu #mobileQuickLogoutSection,
#slideMenu #signOutBtnMobile {
  display: none !important;
}

/* ═══════════════ 6. CANDY-BEVEL CARDS ═══════════════ */
/* Backgrounds are OWNED by times-tables.css (literal-hex Safari rule).
   The candy comes from radius + shadows + a specular gloss.
   Literal colours are used in the card shadows on purpose. */
.card-inner,
.card-front,
.card-back {
  border-radius: 16px;
}

.card.waiting-for-match .card-inner {
  border-radius: 16px;
}

/* soft rim instead of the hard white sticker border (width untouched) */
.card-front,
.card-back {
  border-color: rgba(255, 255, 255, 0.75);
}

/* base bevel (face-down "?" card is the pale-orange candy) */
.card-front {
  box-shadow:
    inset 0 2px 2px rgba(255, 255, 255, 0.85),
    inset 0 -3px 0 rgba(180, 110, 20, 0.18),
    0 6px 0 #DFA24E,
    0 12px 16px rgba(6, 24, 70, 0.30);
}

/* neutral bevel for any card back before a mode-specific edge applies */
.card-back {
  box-shadow:
    inset 0 2px 2px rgba(255, 255, 255, 0.5),
    inset 0 -3px 0 rgba(6, 24, 70, 0.18),
    0 6px 0 rgba(9, 30, 80, 0.35),
    0 12px 16px rgba(6, 24, 70, 0.30);
}

/* specular gloss — .card-front::after and .card-back::before are unused by
   the app (.card-back::after is the shared-answer dot and stays intact) */
.card-front::after,
.card-back::before {
  content: "";
  position: absolute;
  top: 7%;
  left: 9%;
  width: 44%;
  height: 26%;
  border-radius: 50%;
  background: radial-gradient(closest-side, rgba(255, 255, 255, 0.65), transparent);
  filter: blur(1px);
  pointer-events: none;
  z-index: 3;
}

/* the "?" bubble on face-down cards — glossy white pearl */
.card-front .card-content {
  box-shadow:
    inset 0 -4px 8px rgba(196, 62, 0, 0.16),
    inset 0 2px 3px rgba(255, 255, 255, 0.9),
    0 3px 8px rgba(6, 24, 70, 0.18);
  font-weight: 600;
}

/* ── per-mode candy edges (mirror the app's own colour keys exactly) ── */
/* blue question faces */
body[data-active-tab="practice"] .card.flipped .card-back.card-question,
body[data-mode="findResults"] .card.flipped .card-back.card-question {
  box-shadow:
    inset 0 2px 2px rgba(255, 255, 255, 0.5),
    inset 0 -3px 0 rgba(4, 20, 70, 0.28),
    0 6px 0 #0C46A8,
    0 12px 16px rgba(6, 24, 70, 0.36);
}

/* green result faces (practice) */
body[data-active-tab="practice"] .card.flipped .card-back.card-answer {
  box-shadow:
    inset 0 2px 2px rgba(255, 255, 255, 0.5),
    inset 0 -3px 0 rgba(6, 60, 30, 0.28),
    0 6px 0 #0E7A36,
    0 12px 16px rgba(6, 24, 70, 0.36);
}

/* purple answer faces (Match Results) */
body[data-mode="findEquations"] .card.flipped .card-back.card-answer {
  box-shadow:
    inset 0 2px 2px rgba(255, 255, 255, 0.5),
    inset 0 -3px 0 rgba(40, 8, 90, 0.28),
    0 6px 0 #5B21B6,
    0 12px 16px rgba(6, 24, 70, 0.36);
}

/* pink question faces (Match Factors) */
body[data-mode="findFactors"] .card.flipped .card-back.card-question {
  box-shadow:
    inset 0 2px 2px rgba(255, 255, 255, 0.5),
    inset 0 -3px 0 rgba(90, 10, 30, 0.28),
    0 6px 0 #8A2438,
    0 12px 16px rgba(6, 24, 70, 0.36);
}

/* pale-orange hidden faces (all three match modes) */
body[data-mode="findResults"] .card.flipped .card-back.card-answer,
body[data-mode="findEquations"] .card.flipped .card-back.card-question,
body[data-mode="findFactors"] .card.flipped .card-back.card-answer {
  box-shadow:
    inset 0 2px 2px rgba(255, 255, 255, 0.85),
    inset 0 -3px 0 rgba(180, 110, 20, 0.18),
    0 6px 0 #DFA24E,
    0 12px 16px rgba(6, 24, 70, 0.30);
}

/* matched cards — green candy. No shadow: this rule's !important shadow was the
   one card shadow that survived flat-experiment.css's flattening, so matched
   cards showed a green 3D edge while every other card is flat. Drop it so a
   matched card is cleanly flat-green like the rest (border kept). */
.card.matched .card-front,
.card.matched .card-back {
  border-color: rgba(255, 255, 255, 0.85) !important;
  box-shadow: none !important;
}

/* ═══════════════ 7. MODALS & WELCOME ═══════════════ */
.modal-overlay-container {
  background-color: rgba(6, 14, 40, 0.55);
  -webkit-backdrop-filter: blur(3px);
  backdrop-filter: blur(3px);
}

.menu-overlay {
  background-color: rgba(6, 14, 40, 0.55) !important;
}

.modal-content-box {
  background: var(--gl-frost-bg);
  border: 1px solid var(--gl-frost-border);
  box-shadow: var(--gl-frost-shadow) !important;
  color: var(--gl-ink);
}

.modal-button {
  background: linear-gradient(160deg, var(--gl-blue-l), var(--gl-blue-deep));
  border-radius: 999px;
  font-weight: 600;
  letter-spacing: 0.02em;
  box-shadow: inset 0 2px 2px rgba(255, 255, 255, 0.45), 0 4px 0 var(--gl-blue-edge), 0 10px 16px rgba(6, 24, 70, 0.3);
}

.modal-button:hover {
  background: linear-gradient(160deg, #85b4f2, var(--gl-blue-deep));
}

.modal-button:active {
  transform: translateY(3px);
  box-shadow: inset 0 2px 2px rgba(255, 255, 255, 0.45), 0 1px 0 var(--gl-blue-edge), 0 4px 8px rgba(6, 24, 70, 0.25);
}

/* ═══════════════ 7b. SETTINGS MODAL ═══════════════ */
/* One responsive preferences modal for ALL widths, opened from the profile
   menu's ⚙ Settings row (settings-modal.js). It rides the app's own modal
   recipe — .modal-overlay-container (fixed, flex-centred, z 2000: above the
   dock 1700 + bottom sheets 1600, same tier as Sign In etc.) and the frosted
   .modal-content-box above — so it sits over everything exactly like the
   app's other modals. The rules here reshape that centred card into a
   left-aligned settings sheet: header + X, scrollable body, and the row /
   heading / select styles the controls had inside #settingsDropdown
   (times-tables.css scopes those by that id, so they're re-provided here). */
.gl-pref-modal .settings-modal-box {
  width: min(400px, 92vw);
  max-width: none;          /* the base .modal-content-box caps at 300px */
  margin: 0 4vw;
  text-align: left;         /* base is centred */
  padding: 0;
  display: flex;
  flex-direction: column;
  max-height: min(85vh, 680px);
  max-height: min(85dvh, 680px); /* dynamic viewport clears mobile browser chrome */
  overflow: visible;        /* let the custom voice/language dropdowns overlay
                               PAST the box rather than being clipped (which
                               forced an awkward modal scroll to reveal them) */
  border-radius: 22px;
}

.gl-pref-modal .settings-modal-header {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 14px 14px 12px 20px;
  border-bottom: 1px solid rgba(27, 36, 64, 0.1);
}

.gl-pref-modal .settings-modal-header h2 {
  margin: 0;
  font-family: var(--gl-font-display);
  font-weight: 600;
  font-size: 20px;
  color: var(--gl-ink);
}

.settings-modal-close {
  appearance: none;
  -webkit-appearance: none;
  flex: 0 0 auto;
  width: 34px;
  height: 34px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid rgba(27, 36, 64, 0.12);
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.75);
  color: #3A4A6B;
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
  box-shadow: 0 2px 6px rgba(5, 18, 58, 0.10);
}

.settings-modal-close:hover {
  background: #EAF2FF;
}

.gl-pref-modal .settings-modal-body {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  gap: 5px; /* the rhythm the rows had in #settingsDropdown */
  padding: 10px 10px calc(16px + env(safe-area-inset-bottom, 0px));
  /* visible (not auto-scroll): these modals hold only 2–4 short controls, so
     nothing overflows on its own — and scrolling here is exactly what clipped
     the open dropdowns. Let the absolute dropdown lists overlay instead. */
  overflow: visible;
}

/* rows — same recipe the controls had under #settingsDropdown
   (times-tables.css base + this skin's hover), phone-comfortable heights */
.gl-pref-modal .settings-modal-body button {
  background: none;
  border: none;
  color: #333;
  padding: 12px 15px;
  min-height: 48px;
  font-size: 16px;
  cursor: pointer;
  border-radius: 10px;
  transition: background-color 0.3s ease;
  text-align: left;
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
}

.gl-pref-modal .settings-modal-body button:hover {
  background-color: #EAF2FF;
}

.gl-pref-modal .settings-modal-body button i,
.gl-pref-modal .settings-modal-body button svg {
  width: 20px;
  height: 20px;
  color: #555;
  stroke: currentColor;
  flex-shrink: 0;
}

.gl-pref-modal .setting-text {
  flex-grow: 1;
}

.gl-pref-modal .settings-section {
  border-top: 1px solid rgba(27, 36, 64, 0.1);
  margin-top: 8px;
  padding-top: 8px;
  width: 100%;
}

/* when the section is the FIRST thing in the body (the Languages modal, which
   has no toggles above it) its top divider doubles up with the header's bottom
   border — drop it so only one line sits under the title */
.gl-pref-modal .settings-modal-body > .settings-section:first-child {
  border-top: none;
  margin-top: 0;
  padding-top: 0;
}

.gl-pref-modal .settings-heading {
  margin: 5px 0 10px 0;
  padding: 0 15px;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: #3A4A6B;
}

/* descriptive subtitle under each heading (e.g. "Listen to different voices").
   Negative top margin tucks it close under the uppercase label; softer weight
   and colour keep the label the primary line. */
.gl-pref-modal .gl-pref-desc {
  margin: -6px 0 8px 0;
  padding: 0 15px;
  font-size: 13px;
  font-weight: 500;
  line-height: 1.35;
  color: #5A6B8C;
}

/* Voices / Language title rows: an icon to the LEFT of the heading +
   description pair. The icon is sized to the VISIBLE text (roughly the caps of
   the heading down to the description) rather than the full line-boxes — Nunito's
   generous line-height makes a line-box-matched icon read as too tall next to
   the shorter glyphs. A small fixed square, vertically centred on the two lines,
   keeps it level and compact. Fixed height (not stretch) also stops the image's
   large intrinsic size from inflating the flex row. object-fit:contain guards
   against distortion. Tune GL_ICON_H below if it needs to be bigger/smaller. */
.gl-pref-modal .gl-pref-titlerow {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 0 15px;
}
.gl-pref-modal .gl-pref-titlerow .settings-heading,
.gl-pref-modal .gl-pref-titlerow .gl-pref-desc {
  padding-left: 0;
  padding-right: 0;
}
.gl-pref-modal .gl-pref-titletext {
  display: flex;
  flex-direction: column;
  min-width: 0;
}
.gl-pref-modal .gl-pref-icon {
  flex: 0 0 auto;
  height: 30px;   /* GL_ICON_H — visible size of the icon square (width:auto
                     follows, so it stays proportional) */
  width: auto;
  object-fit: contain;
  transform: translateY(-2px);   /* pins the top level with the title line as the
                                    height changes (t = height/2 - 17) */
}

/* extra blank row so the last selector isn't tight against the modal bottom */
.gl-pref-modal .gl-pref-bottom-space {
  height: 18px;
}

/* selects — the structural recipe from times-tables.css's
   #settingsDropdown .settings-select (arrow included); the frost skin comes
   from the shared pill rule in §5 above */
.gl-pref-modal .settings-select {
  width: calc(100% - 30px);
  margin: 5px 15px 0;
  padding: 10px;
  min-height: 46px;
  border-radius: 12px;
  font-size: 16px;
  font-family: inherit;
  cursor: pointer;
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%23555' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 8px center;
}

/* gentle pop on open (display:none → flex restarts the animation) */
.gl-pref-modal .settings-modal-box {
  animation: glSettingsPop 0.22s cubic-bezier(0.22, 0.9, 0.34, 1) both;
}

@keyframes glSettingsPop {
  from {
    transform: scale(0.94) translateY(12px);
    opacity: 0;
  }
  to {
    transform: none;
    opacity: 1;
  }
}

@media (prefers-reduced-motion: reduce) {
  .gl-pref-modal .settings-modal-box {
    animation: none;
  }
}

/* welcome / sign-in flow buttons → candy green */
.welcome-continue-btn,
.primary-btn {
  background: linear-gradient(160deg, var(--gl-grn-l), var(--gl-grn-d));
  border-radius: 14px;
  font-family: var(--gl-font-display);
  font-weight: 600;
  box-shadow: inset 0 2px 2px rgba(255, 255, 255, 0.4), 0 4px 0 var(--gl-grn-edge), 0 10px 16px rgba(14, 122, 54, 0.3);
}

.welcome-continue-btn:hover,
.primary-btn:hover {
  transform: translateY(-1px);
  box-shadow: inset 0 2px 2px rgba(255, 255, 255, 0.4), 0 5px 0 var(--gl-grn-edge), 0 12px 20px rgba(14, 122, 54, 0.4);
}

.welcome-continue-btn:disabled,
.primary-btn:disabled {
  box-shadow: none;
}

/* share-code input → friendly Fredoka pill */
#welcomeUnlockCodeInput,
.form-input {
  font-family: var(--gl-font-display);
  border-radius: 14px;
  border-color: #D6E6FF;
  background-color: #fff;
  color: var(--gl-ink);
}

#welcomeUnlockCodeInput:focus,
.form-input:focus {
  border-color: var(--gl-blue);
  box-shadow: 0 0 0 3px rgba(66, 129, 242, 0.25);
}

/* ═══════════════ 8. TOASTS & HINTS ═══════════════ */
.message {
  border-radius: 16px;
  border: 1px solid #fcd34d;
  font-weight: 500;
  box-shadow: 0 10px 22px rgba(5, 18, 58, 0.25), inset 0 1px 0 rgba(255, 255, 255, 0.6);
}

.smart-hint {
  background-color: rgba(9, 16, 40, 0.78);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  border: 1px solid rgba(255, 255, 255, 0.25);
  border-radius: 999px;
  font-weight: 500;
  box-shadow: 0 10px 24px rgba(0, 0, 0, 0.35);
}

.smart-hint::after {
  border-color: rgba(9, 16, 40, 0.78) transparent transparent transparent;
}

.smart-hint.tip-up::after {
  border-color: transparent transparent rgba(9, 16, 40, 0.78) transparent;
}

/* ═══════════════ 9. SLIDES VIEW (Practice → Show Slides) ═══════════════ */
#slideshowContainer {
  background-color: rgba(16, 38, 110, 0.72);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
}

.slideshow-card {
  border-radius: 24px;
  background: var(--gl-frost-bg);
  border: 1px solid var(--gl-frost-border);
  box-shadow: var(--gl-frost-shadow) !important;
}

.slideshow-card .card-type {
  border-radius: 999px;
  font-weight: 700;
  box-shadow: inset 0 1px 1px rgba(255, 255, 255, 0.4), 0 2px 0 rgba(6, 24, 70, 0.25);
}

#slideshowQuestionCard .card-type {
  background: linear-gradient(160deg, var(--gl-blue-l), #1564dc);
}

#slideshowAnswerCard .card-type {
  background: linear-gradient(160deg, #4ade80, #16a34a);
}

.slideshow-nav-btn {
  background-color: rgba(9, 16, 40, 0.5);
  border: 1px solid rgba(255, 255, 255, 0.28);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
}

.slideshow-nav-btn:hover {
  background-color: rgba(9, 16, 40, 0.7);
}

/* ── Slides: use the available space + bigger, kid-friendly arrows ────────────
   The base caps the cards small (max-width 380px / min-height 300px) which
   leaves a lot of empty space. Grow them responsively so the flashcards fill
   the screen, and enlarge the prev/next hit targets for small fingers. */
.slideshow-inner-container {
  max-width: 1200px !important;
  min-height: 0 !important;
}
.slideshow-content {
  max-width: min(1120px, 94vw) !important;
  gap: clamp(18px, 3.5vw, 48px) !important;
}
.slideshow-card {
  max-width: 560px !important;
  min-height: min(62vh, 560px) !important;
  height: min(62vh, 560px) !important;
}
/* bigger round arrows (was 60px) — comfortable for a child to tap */
.slideshow-nav-btn {
  width: 78px !important;
  height: 78px !important;
}
.slideshow-nav-btn i,
.slideshow-nav-btn svg {
  width: 44px !important;
  height: 44px !important;
}
#slideshowPrev { left: -34px !important; }
#slideshowNext { right: -34px !important; }

/* ── Numerals scale with the (now larger) cards ───────────────────────────────
   The base JS (adjustSlideshowCard) writes a fixed inline font-size on the span
   (48px equation / 120px answer) that was tuned for the small base cards, so in
   the enlarged cards the text looks tiny — especially the equation. Override
   with responsive clamps; `!important` beats the JS's inline (non-important)
   font-size. Clamps are conservative so the widest content ("12 × 12" / "144")
   still fits the card without overflowing (the base autofit can't rescue an
   !important size). */
#slideshowQuestionCard .card-content span {
  font-size: clamp(56px, 7vw, 108px) !important;
  line-height: 1.15 !important;
}
#slideshowAnswerCard .card-content span {
  font-size: clamp(104px, 13vw, 210px) !important;
  line-height: 1.05 !important;
}

@media (max-width: 768px) {
  /* Reserve the bottom dock's height so the slideshow ends ABOVE the dock
     (the base JS stretches the container to bottom:0, which the z-1700 dock
     otherwise overlaps — cropping the lower card). !important beats the JS's
     inline bottom:0. */
  #slideshowContainer {
    bottom: var(--mc-dock-space, 60px) !important;
  }
  /* stacked cards: fill the vertical space, don't sit small at the top */
  .slideshow-content {
    max-width: 94vw !important;
    gap: 14px !important;
  }
  .slideshow-card {
    max-width: 100% !important;
    min-height: 37vh !important;   /* was 39vh — trim so both cards + arrows
                                      clear the header and the reserved dock */
    height: 37vh !important;
  }
  /* the arrows straddle the divider between the two stacked cards */
  .slideshow-nav-btn {
    width: 70px !important;
    height: 70px !important;
  }
  .slideshow-nav-btn i,
  .slideshow-nav-btn svg {
    width: 40px !important;
    height: 40px !important;
  }
  #slideshowPrev { left: -6px !important; }
  #slideshowNext { right: -6px !important; }
  /* full-width stacked cards → size the numerals off viewport width */
  #slideshowQuestionCard .card-content span {
    font-size: clamp(52px, 16vw, 104px) !important;
  }
  #slideshowAnswerCard .card-content span {
    font-size: clamp(96px, 30vw, 200px) !important;
  }
}

/* ═══════════════ 9b. PLAY SHEET — game-mode dropdown ═══════════════ */
/* The progress "bars" (the % pills) 10% longer for legibility (60 → 66px),
   and a little breathing room between the pill and the caret in the collapsed
   (selected) row. Applies on desktop + mobile. */
.game-mode-progress-bar {
  width: 66px !important; /* was 60px */
}
.custom-game-mode-dropdown .dropdown-selected .dropdown-arrow {
  margin-left: 12px !important; /* gap between the % pill and the caret */
}

@media (max-width: 768px) {
  /* The game-mode options open UPWARD as a floating overlay. Downward-absolute
     fell behind the fixed dock (last option unreachable); flowing them inline
     shoved the Start Game button and left odd space. Opening up keeps the sheet
     layout static (no shift, no extra space) and lands the list in the clear
     area above the control. The dropdown container is the positioning context. */
  #custom-games-mode-dropdown-mobile {
    position: relative !important;
  }
  #custom-games-mode-dropdown-mobile .dropdown-options {
    top: auto !important;
    bottom: calc(100% + 4px) !important;
    max-height: 46vh !important;
    overflow-y: auto !important;
  }
  /* an extra blank row of breathing space above the Play / Start Game button
     (Practice sheet #slide-play-button, Play sheet #slide-start-game-button) */
  #slideMenu #slide-play-button,
  #slideMenu #slide-start-game-button {
    margin-top: 32px !important;
  }
}

/* ═══════════════ 10. REWARDS SHOP — the reef arcade ═══════════════ */
/* category pills → glass over water; active → candy blue */
.category-tab {
  background: rgba(255, 255, 255, 0.12);
  border: 1px solid rgba(255, 255, 255, 0.3);
  border-radius: 999px;
  /* lighter than the base 600 (rewards.css) — Fredoka 600 read too heavy on the
     shop pills. Excluded from the app-wide --gl-fredoka-weight sweep for this. */
  font-weight: 500;
  color: #fff;
  text-shadow: 0 1px 4px rgba(6, 20, 64, 0.35);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
}

.category-tab:hover {
  background: rgba(255, 255, 255, 0.2);
}

.category-tab.active {
  background: linear-gradient(160deg, var(--gl-blue-l), var(--gl-blue-deep));
  border-color: transparent;
  color: #fff;
  box-shadow: inset 0 2px 2px rgba(255, 255, 255, 0.45), 0 3px 0 var(--gl-blue-edge), 0 8px 14px rgba(6, 24, 70, 0.3);
}

/* Same legibility fix for the Rewards sub-tabs: at >768px the inactive
   pills sit on the light header top and washed out in white. Give them the
   dark ink (active blue pill + gold coin pill keep their own colours via
   the :not() guards). Left as-is ≤768px, where the layout differs. */
@media (min-width: 769px) {
  .category-tab:not(.active):not(.coin-tab) {
    color: #1a1a1a; /* match the main nav — near-black */
    text-shadow: 0 1px 1px rgba(255, 255, 255, 0.45);
  }
}

/* inactive shop category pills: tint their icon + label a little darker than
   the active pill (they were faint white on the translucent pills). Later than
   the >768 rule above, so it wins at every width. Icons inherit currentColor. */
.category-tab:not(.active):not(.coin-tab) {
  color: var(--gl-royal-ink);
  text-shadow: none;
}

/* coin balance tab → THE gold candy coin pill (original rules are
   !important, so these must be too) */
.category-tab.coin-tab {
  position: relative;
  overflow: hidden;
  background: linear-gradient(165deg, var(--gl-gold-hi), var(--gl-gold-lo)) !important;
  border: 1px solid rgba(255, 255, 255, 0.55) !important;
  color: var(--gl-gold-ink) !important;
  box-shadow: inset 0 2px 2px rgba(255, 255, 255, 0.65), 0 3px 0 var(--gl-gold-edge), 0 8px 16px rgba(245, 166, 35, 0.35);
  text-shadow: 0 1px 0 rgba(255, 255, 255, 0.35);
}

.category-tab.coin-tab:hover {
  background: linear-gradient(165deg, var(--gl-gold-hi), var(--gl-gold-lo)) !important;
}

/* travelling glint across the gold pill */
.category-tab.coin-tab::after {
  content: "";
  position: absolute;
  top: -30%;
  left: -45%;
  width: 34%;
  height: 160%;
  background: linear-gradient(105deg, transparent, rgba(255, 255, 255, 0.85), transparent);
  transform: skewX(-18deg);
  pointer-events: none;
  animation: glGlint 4.8s ease-in-out infinite;
}

@keyframes glGlint {
  0%, 64% { left: -45%; }
  82%, 100% { left: 130%; }
}

/* the single spendable-gold number, in the pill's own gold ink */
.coin-tab #rewardsCoinBalance {
  color: var(--gl-gold-ink);
}

/* shop item cards → frosted glass display cases */
.shop-item-card {
  background: linear-gradient(165deg, rgba(255, 255, 255, 0.22), rgba(255, 255, 255, 0.08));
  border: 1px solid var(--gl-glass-border);
  border-top-color: var(--gl-glass-line);
  border-radius: 20px;
  -webkit-backdrop-filter: blur(9px);
  backdrop-filter: blur(9px);
  box-shadow: var(--gl-glass-shadow);
}

.shop-item-card:hover {
  box-shadow: 0 16px 32px rgba(5, 18, 58, 0.38), inset 0 1px 0 rgba(255, 255, 255, 0.35);
}

.shop-item-card.owned {
  border: 3px solid rgba(105, 208, 109, 0.7);
  box-shadow: 0 10px 22px rgba(5, 18, 58, 0.28), inset 0 1px 0 rgba(255, 255, 255, 0.3), 0 0 14px rgba(105, 208, 109, 0.35);
}

.shop-item-card.owned::before {
  background: linear-gradient(160deg, #9BE89E, #69D06D);
  color: #19712C;
  box-shadow: inset 0 1px 1px rgba(255, 255, 255, 0.5), 0 2px 0 #3FA344;
}

/* legendary items glow gold (progressive enhancement via :has) */
.shop-item-card:has(.rarity-legendary) {
  border-color: rgba(255, 214, 106, 0.65);
  box-shadow: 0 10px 22px rgba(5, 18, 58, 0.28), inset 0 1px 0 rgba(255, 255, 255, 0.3), 0 0 18px rgba(245, 166, 35, 0.4);
}

.shop-item-name {
  color: #0F2244;
  text-shadow: none;
  /* !important: .shop-item-name is an <h4>, so the app-wide sweep's
     `h1,h2,h3,h4 { font-weight: … !important }` catches it by TAG even though
     the class is excluded from the sweep. A class-level !important (class beats
     tag among !important decls) restores the intended lighter title weight. */
  font-weight: 500 !important;
}

.shop-item-description {
  color: rgba(15, 34, 68, 0.7);
}

/* bottom info panel — clean solid light surface */
.shop-item-info {
  background: #F4F7FF;
}

.shop-item-footer {
  background: #F4F7FF;
  border-top: 1px solid rgba(15, 34, 68, 0.08);
}

.item-price {
  color: #0F2244;
  text-shadow: none;
}

/* the item showcase window — blue gradient top zone */
.shop-item-preview {
  background: linear-gradient(180deg, #1E6FD9 0%, #134587 100%);
  box-shadow: inset 0 -12px 22px rgba(5, 18, 58, 0.25), inset 0 2px 6px rgba(255, 255, 255, 0.18);
}

/* rarity badges — candy chips, legendary glows */
.rarity-badge {
  border-radius: 8px;
  box-shadow: 0 2px 6px rgba(5, 18, 58, 0.3);
}

.rarity-legendary {
  background: linear-gradient(160deg, var(--gl-gold-hi), var(--gl-gold-lo));
  color: var(--gl-gold-ink);
  box-shadow: 0 2px 8px rgba(251, 191, 36, 0.5), 0 0 16px rgba(245, 166, 35, 0.6);
}

/* buy buttons → candy (state colours keep their meaning) */
.buy-button {
  border-radius: 999px;
  font-weight: 600;
  background: linear-gradient(160deg, var(--gl-grn-l), var(--gl-grn-d));
  box-shadow: inset 0 2px 2px rgba(255, 255, 255, 0.45), 0 4px 0 var(--gl-grn-edge), 0 8px 14px rgba(14, 122, 54, 0.35);
}

.buy-button:hover {
  transform: none;
  filter: brightness(1.06);
  box-shadow: inset 0 2px 2px rgba(255, 255, 255, 0.45), 0 4px 0 var(--gl-grn-edge), 0 10px 18px rgba(14, 122, 54, 0.45);
}

.buy-button:active {
  transform: translateY(3px);
  box-shadow: inset 0 2px 2px rgba(255, 255, 255, 0.45), 0 1px 0 var(--gl-grn-edge), 0 4px 8px rgba(14, 122, 54, 0.3);
}

.buy-button.insufficient {
  background: linear-gradient(160deg, #f87171, #dc2626);
  box-shadow: inset 0 2px 2px rgba(255, 255, 255, 0.35), 0 4px 0 #991B1B, 0 8px 14px rgba(153, 27, 27, 0.35);
}

.buy-button.showing-button {
  background: linear-gradient(160deg, #a78bfa, #7c3aed);
  box-shadow: inset 0 2px 2px rgba(255, 255, 255, 0.4), 0 4px 0 #5B21B6, 0 8px 14px rgba(91, 33, 182, 0.35);
}

.buy-button.not-showing-button {
  background: linear-gradient(160deg, #94a3b8, #475569);
  box-shadow: inset 0 2px 2px rgba(255, 255, 255, 0.35), 0 4px 0 #1E293B, 0 8px 14px rgba(30, 41, 59, 0.35);
}

.buy-button.sign-in-button {
  background: linear-gradient(160deg, var(--gl-blue-l), var(--gl-blue-deep));
  box-shadow: inset 0 2px 2px rgba(255, 255, 255, 0.4), 0 4px 0 var(--gl-blue-edge), 0 8px 14px rgba(6, 24, 70, 0.35);
}

.buy-button:disabled,
.buy-button.owned-button {
  background: rgba(255, 255, 255, 0.22);
  color: var(--gl-foam-70);
  border: 1px solid rgba(255, 255, 255, 0.3);
  box-shadow: none;
  text-shadow: none;
}

/* …but a shop card's footer is a LIGHT panel (#F4F7FF), not glass over the
   blue. The foam-on-glass treatment above puts near-white text
   (rgba(244,251,255,.72)) on a near-white pill (rgba(255,255,255,.22)) over
   near-white — so "Need More" vanishes and every item a child can't yet
   afford looks like a card whose Buy button is simply MISSING. Restore the
   base skin's intent inside the card: a solid muted pill with white text —
   visibly a button, visibly not pressable. */
.shop-item-footer .buy-button:disabled,
.shop-item-footer .buy-button.owned-button {
  background: linear-gradient(160deg, #aab6cc, #7c8aa6);
  color: #ffffff;
  border: none;
  box-shadow: inset 0 2px 2px rgba(255, 255, 255, 0.3), 0 3px 0 #5d6a85;
  text-shadow: 0 1px 1px rgba(30, 41, 59, 0.35);
  opacity: 1;
}

/* ═══════════════ 11. AQUARIUM — frame + chrome ONLY ═══════════════ */
/* The tank interior (fish, purchased creatures, backgrounds, #aquarium's
   ::before light rays) is deliberately untouched. */
/* Full-bleed tank: fill the whole content area edge-to-edge instead of sitting
   as a rounded floating card. The margin trick breaks the tank out of the
   centred, 20px-padded body so it reaches both viewport edges; square corners
   + no border/drop-shadow remove the "card" look. Interior is untouched. */
#aquarium {
  /* !important beats the inline width/max-width/margin aquarium.js sets to keep
     the tank aligned with the practice grid — here we want it full-bleed. */
  width: 100vw !important;
  max-width: 100vw !important;
  margin-left: calc(50% - 50vw) !important;
  margin-right: calc(50% - 50vw) !important;
  border-radius: 0;
  border: none;
  box-shadow: none;
}

#aquarium:fullscreen,
#aquarium:-webkit-full-screen,
#aquarium.aquarium-fullscreen {
  border-radius: 0;
  border: none;
}

#aquariumFullscreenBtn {
  background-color: rgba(9, 16, 40, 0.45);
  border-radius: 12px;
}

/* overview: each fish sits on a frosted-edge glass tile — a milky rim that
   fades to a clearer core, so the tank reads through the centre while the
   frame stays legible. Built from GRADIENTS ONLY, deliberately NO backdrop
   blur: 12 blurred tiles over the tank would jank classroom devices (the
   original tint-only decision). The radial layer is the frost→clear ramp;
   the linear layer is a faint floor so white stat text never loses contrast
   over a light tank. Rim highlight + soft inset seat the "glass edge". */
.overview-fish-item {
  background:
    radial-gradient(128% 104% at 50% 40%,
      rgba(255, 255, 255, 0.06) 26%,
      rgba(255, 255, 255, 0.30) 52%,
      rgba(255, 255, 255, 0.62) 76%,
      rgba(255, 255, 255, 0.80) 100%),
    linear-gradient(165deg, rgba(255, 255, 255, 0.16), rgba(255, 255, 255, 0.11));
  border: 1px solid rgba(255, 255, 255, 0.34);
  border-radius: 16px;
  box-shadow:
    0 6px 14px rgba(5, 18, 58, 0.18),
    inset 0 1px 0 rgba(255, 255, 255, 0.55),
    inset 0 0 0 1px rgba(255, 255, 255, 0.14);
}

/* hide the overview scrollbar (the list still scrolls) — the visible track
   over the tank looked out of place. */
#overviewFishContainerAqua {
  scrollbar-width: none;      /* Firefox */
  -ms-overflow-style: none;   /* old Edge/IE */
}
#overviewFishContainerAqua::-webkit-scrollbar {
  width: 0;
  height: 0;
  display: none;              /* WebKit (Chrome/Safari) */
}

/* Larger overview panel behind the fish tiles → more transparent so the tank
   reads through it (per request). Overrides the base aquarium.css value
   (rgba(255,255,255,0.35)); same ID specificity, glass-skin.css loads later. */
#overviewOverlayAqua {
  background-color: rgba(6, 20, 59, 0.44);
}

/* Overview readability on resize: the base grid uses `height:95%` + fixed
   `grid-template-rows: repeat(N, auto)` (N varies by the 2/3/4-column
   breakpoint), so at short window heights the rows get squeezed and the tiles
   crunch (unreadable). Instead, size every row to the tile's own CONTENT
   (`grid-auto-rows: max-content`) and pack rows from the top (`align-content:
   start`). Rows can't shrink below the content, so tiles stay readable and never
   overlap; when they don't all fit, the container just scrolls (overflow-y:auto
   is already set; the scrollbar is hidden above). Scoped to >768px — at ≤768px
   the base mobile grid uses fixed-height rows that already scroll. */
@media (min-width: 769px) {
  /* Top-align the overview inside the tank. The tank (#aquarium) has a
     min-height:400px, so on a SHORT window it's taller than the viewport; when
     the overlay centred the grid, the grid's bottom (and its scroll region) got
     pushed below the fold and the last row was unreachable. Top-aligning + the
     viewport height cap below keeps the whole scrollable grid on-screen. */
  #overviewOverlayAqua {
    align-items: flex-start !important;
  }
  #overviewFishContainerAqua {
    grid-template-rows: none !important;
    /* minmax(180px, 1fr): a readable floor of 180px (the tile's own content is
       ~175px, so the floor is safely above it — a tile can never overflow its
       row into the next, which was the overlap bug), and 1fr so rows GROW to
       fill the height when there's room. When the rows can't all fit at 180px
       the container just scrolls (overflow-y:auto; scrollbar hidden above). */
    grid-auto-rows: minmax(180px, 1fr) !important;
    overflow-y: auto !important;
    /* never taller than the visible area below the desktop nav, so the internal
       scroll always reaches the last row (the grid starts ~75px down). */
    max-height: calc(100dvh - 88px) !important;
  }
}

/* Progress-overview coin-rate badge styling removed (2026-07-22): the badge
   itself was removed from aquarium.js — kids misread its "×N" as the table
   number. coin-badge.js is retired from index.html with it. */

/* Mobile: aquarium.js strips each tile's chrome with inline styles
   (background/border/radius/shadow → none). Re-apply the same frosted panel
   here so phones get the framed tiles too; !important beats those inline
   styles. The pill-centering rule below is broadened to all widths to match. */
@media (max-width: 768px) {
  .overview-fish-item {
    background:
      radial-gradient(128% 104% at 50% 40%,
        rgba(255, 255, 255, 0.06) 26%,
        rgba(255, 255, 255, 0.30) 52%,
        rgba(255, 255, 255, 0.62) 76%,
        rgba(255, 255, 255, 0.80) 100%),
      linear-gradient(165deg, rgba(255, 255, 255, 0.16), rgba(255, 255, 255, 0.11)) !important;
    border: 1px solid rgba(255, 255, 255, 0.34) !important;
    border-radius: 16px !important;
    box-shadow:
      0 6px 14px rgba(5, 18, 58, 0.18),
      inset 0 1px 0 rgba(255, 255, 255, 0.55),
      inset 0 0 0 1px rgba(255, 255, 255, 0.14) !important;
  }

  /* Tighten the tile: aquarium.css fixes it at height:180px with
     justify-content:space-between, which pins the fish to the top and the stats
     to the bottom — a big empty gap below the fish. Shrink to 160px and centre
     the group so that gap roughly halves and sits balanced. */
  #overviewFishContainerAqua .overview-fish-item {
    height: 160px !important;
    justify-content: center !important;
  }
  #overviewFishContainerAqua .overview-fish-item .fish {
    margin-bottom: 11px !important;
  }
  #overviewFishContainerAqua .overview-fish-item .performance-display {
    margin-top: 11px !important;
  }
}

/* Centre the 🔵🟣🔴 mastery-pill row inside the glass tile.
   aquarium.js gives .game-stats-container inline width:130%; margin-left:-15%
   (built to overflow the OLD borderless tiles), which would make the pills hug
   / cross the framed card's left edge. Pull the row back to the tile width,
   centred. Now the tiles are framed at ALL widths (mobile panel added above),
   so this applies at all widths too. Inline styles → !important required. */
#overviewFishContainerAqua .overview-fish-item .game-stats-container {
  width: 100% !important;
  margin-left: 0 !important;
}

/* mastery pill track under each fish (fill colours come from JS — kept) */
#aquarium .progress-bar-container {
  background-color: rgba(7, 20, 58, 0.45);
  border-color: rgba(255, 255, 255, 0.35);
  box-shadow: inset 0 1.5px 3px rgba(4, 14, 44, 0.5);
}

.performance-display .percentage-text {
  font-weight: 600;
  text-shadow: 0 1px 3px rgba(4, 14, 44, 0.7);
}

/* Overview fish: grow DOWNWARD, never into the top frame.
   Fish scale from 0.9→1.8 (MAX_FISH_SCALE) with progress via `transform`,
   which is visual-only and reserves no layout space. With the default
   center origin a maxed fish (plus its hat/fins, which sit above the body
   box) overflows upward and bumps the panel's top edge — worst on the short
   mobile tiles. Anchoring the transform to the TOP fixes the fish's top line
   at every scale, so all growth extends downward instead; the reserved
   margin-bottom gives the enlarged body room before the stats row (verified
   no overlap up to 1.8×). Scoped to the overview grid — the swimming-tank
   fish are untouched. This is the desktop/base behaviour; the phone block
   below overrides it with a centred layout better suited to the short tile. */
#overviewFishContainerAqua .overview-fish-item .fish {
  transform-origin: center top !important;
  margin-top: 4px !important;
  margin-bottom: 24px !important;
}
/* Phones: the short tile makes the desktop "pin-top + reserved margin-bottom"
   read as a small fish floating at the very top with dead space beneath it.
   Instead, CENTRE the fish in the space ABOVE the stats and let it grow
   symmetrically. `margin: auto` (vertical) centres the fish's box in the free
   space while the stats stay pinned below, and a centre transform-origin keeps
   the VISUAL fish centred at every scale — its centre line holds steady from
   0.9× to 1.8×, so it reads balanced when small (the 0% case everyone sees)
   yet still clears the top frame and the stats row when maxed (verified across
   0.9×–1.8×). A little extra tile height gives the biggest fish air on both
   sides. Overrides the all-width rule above on phones only; desktop keeps the
   pin-top behaviour. */
@media (max-width: 768px) {
  #overviewFishContainerAqua .overview-fish-item {
    height: 172px !important;
  }
  #overviewFishContainerAqua .overview-fish-item .fish {
    transform-origin: center center !important;
    margin-top: auto !important;
    margin-bottom: auto !important;
  }
  #overviewFishContainerAqua .overview-fish-item .performance-display {
    margin-top: 0 !important;
  }
}

/* ═══════════════ 12. MOBILE SLIDE-OUT MENU ═══════════════ */
/* header gets an inline gradient from applyColorScheme() → !important */
.menu-header {
  background: linear-gradient(135deg, var(--gl-water-2) 0%, #02c8c8 100%) !important;
}

.slide-menu {
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(240, 249, 255, 0.96)) !important;
}

.close-menu {
  background-color: rgba(255, 255, 255, 0.18);
  border: 1px solid rgba(255, 255, 255, 0.35);
}

.tab-scroll-container,
.tab-grid-container {
  background-color: #F1F7FF;
  border-bottom: 1px solid rgba(27, 36, 64, 0.08);
}

.tab-grid-button {
  border-radius: 14px;
  color: #51608A;
}

.tab-grid-button.active {
  color: var(--gl-blue-deep);
  font-weight: 600;
}

.tab-grid-indicator {
  background-color: #DCEBFF;
  border-radius: 14px;
  box-shadow: inset 0 1px 0 #fff, 0 2px 6px rgba(37, 99, 235, 0.18);
}

.slide-tab {
  font-family: var(--gl-font-display);
  font-weight: 500;
}

.slide-menu .btn {
  border-radius: 12px;
  background: linear-gradient(160deg, var(--gl-blue-l), var(--gl-blue-deep));
  box-shadow: inset 0 2px 2px rgba(255, 255, 255, 0.4), 0 3px 0 var(--gl-blue-edge);
}

.slide-menu .btn:hover {
  background: linear-gradient(160deg, #85b4f2, var(--gl-blue-deep));
}

.slide-menu .icon-button {
  border-radius: 12px;
  background-color: #F1F7FF;
  border-color: #D6E6FF;
}

.slide-menu .icon-button.active {
  background: linear-gradient(160deg, var(--gl-blue-l), var(--gl-blue-deep));
  border-color: transparent;
  box-shadow: inset 0 1px 1px rgba(255, 255, 255, 0.4), 0 2px 0 var(--gl-blue-edge);
}

/* ═══════════════ 13. ONBOARDING SLIDER BUTTONS (ws-/ps-/pt-) ═══════════════ */
.ws-btn-primary,
.ws-btn-green {
  font-family: var(--gl-font-display);
}

/* ═══════════════ 14. REDUCED MOTION ═══════════════ */
@media (prefers-reduced-motion: reduce) {
  body::after {
    animation: none;
  }
  .category-tab.coin-tab::after {
    animation: none;
    display: none;
  }
}

/* ═══════════════ 15. SMALL SCREENS ═══════════════ */
@media (max-width: 768px) {
  /* slightly shallower candy drop so small cards keep their face area */
  .card-front,
  .card-back {
    border-radius: 14px;
  }
  .card-inner,
  .card.waiting-for-match .card-inner {
    border-radius: 14px;
  }
}

/* ═══════════════ 16. PROGRESS OPTIONS — TIDY STAT CARD ═══════════════
   The Progress tab's options sheet stacked three mismatched, free-floating
   controls — a wide heart%, a narrower coin count, and an orphaned help ? —
   in a big empty void, so it read as unbalanced and random. Group them into
   one frosted card capped to the View Mode toggle's width: heart + coins as
   TWO EQUAL pills side by side (same width and height), with the help ? as a
   small centred disc on its own row beneath.

   Layout-only. The panel's buttons bind their handlers by id/class, untouched;
   times-tables.js markup is unchanged (its inline styles are overridden with
   !important). Scoped to #slideMenu so ONLY the options sheet is affected —
   the desktop progress view uses different markup. */
/* centre the whole panel as a column so the label, the View Mode toggle and
   the stat card all share one centre line. The app's .form-group is
   display:block, which left-pins the inline-flex toggle once its max-width
   bites on wide tablets; making the group a centred flex column fixes that AND
   activates the toggle's own inline align-self:center. */
#slideMenu #progress-content .form-group {
  display: flex !important;
  flex-direction: column !important;
  align-items: center !important;
  /* reserve room beneath the card for the help ? that is lifted out of it
     below; the sheet is auto-height flex, so this grows it to fit and keeps
     the ? clear of the dock */
  padding-bottom: 66px !important;
}

/* the View Mode toggle is content-box in the inline markup, so its
   width:100% + padding + border would render 10px past the card below it;
   border-box pulls it back so the toggle and card share one width. Centring is
   handled by the flex-column .form-group above — margin:auto could NOT centre
   the toggle because it is display:inline-flex (auto side-margins have used
   value 0 on inline-level boxes; the card centres fine as it is display:grid). */
#slideMenu #mobile-progress-toggle {
  box-sizing: border-box !important;
}

/* the metrics container → a centred frosted card, same width as the toggle.
   box-sizing:border-box is REQUIRED: with content-box, width:100% + padding
   overflows the form-group and margin:auto resolves to a negative right
   margin, shoving the card off-centre. auto-fit + minmax(120px,1fr) gives two
   EQUAL cells side by side where they fit (≈390px+ phones and the 320px cap)
   and cleanly stacks them to one full-width column on very narrow phones so
   the heart pill never has to squeeze its contents. */
#slideMenu .mobile-progress-header {
  display: grid !important;
  position: relative !important; /* anchor for the help ? lifted just below it */
  grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)) !important;
  align-items: stretch !important;           /* pills share one height */
  gap: 10px !important;
  box-sizing: border-box !important;
  width: 100% !important;
  max-width: 320px !important;
  margin: 16px auto 0 !important;
  padding: 14px !important;
  background: linear-gradient(165deg, rgba(255, 255, 255, 0.55), rgba(255, 255, 255, 0.3)) !important;
  border: 1px solid var(--gl-glass-line, rgba(255, 255, 255, 0.5)) !important;
  border-radius: 18px !important;
  box-shadow: 0 8px 20px rgba(6, 20, 59, 0.12), inset 0 1px 0 rgba(255, 255, 255, 0.6) !important;
  -webkit-backdrop-filter: blur(8px) saturate(1.15) !important;
  backdrop-filter: blur(8px) saturate(1.15) !important;
}

/* the two stat pills → fill their equal cell, content centred, snug padding
   (they keep their own pink / gold candy gradients from the inline markup).
   min-width:0 lets each honour its equal grid track instead of inflating to
   min-content; overflow:hidden guards the rounded edge. */
#slideMenu .mobile-progress-header .heart-streak-pill,
#slideMenu .mobile-progress-header .coin-pill.mobile-coin-pill {
  width: 100% !important;
  min-width: 0 !important;
  overflow: hidden !important;
  margin: 0 !important;
  box-sizing: border-box !important;
  justify-content: center !important;
  gap: 6px !important;
  padding: 8px 8px !important;
  min-height: 44px !important; /* equal height in BOTH side-by-side and stacked
                                  modes — grid stretch only equalises a shared
                                  row, so the 24px coin box would otherwise sit
                                  4px taller than the 20px heart when stacked */
}

/* protect the fixed-size glyphs so a tight cell can never squeeze them out of
   existence (the heart icon shrank to 0px at 360px before this), and trim the
   heart pill's internals — bar 48→32, inner gap 8→6, smaller % with a lower
   floor — so its content fits the 2-up cell with slack even when the value is
   "100%". The coin count gets a matching floor so both values reserve alike. */
#slideMenu .heart-streak-pill > svg.heart-icon,
#slideMenu .coin-pill.mobile-coin-pill > div:first-child {
  flex-shrink: 0 !important;
}
#slideMenu .heart-streak-pill > div {
  gap: 6px !important;
  min-width: 0 !important;
}
#slideMenu .heart-streak-pill > div > div:first-child {
  width: 32px !important;
  flex-shrink: 0 !important;
}
#slideMenu .mobile-progress-percent {
  min-width: 32px !important;
  font-size: 15px !important;
}
#slideMenu .mobile-coin-count {
  min-width: 32px !important;
}

/* help ? → lifted OUT of the card to sit centred just below it, matching the
   standalone 38px ? at the foot of the Practice / Play popups. Absolute so it
   leaves the card's grid (the card then frames only the two pills); the
   form-group's padding-bottom reserves its space in the auto-height sheet. The
   inline black icon colour is kept, only its box is reset to a plain button. */
#slideMenu .mobile-progress-header #mobile-progress-help {
  position: absolute !important;
  top: calc(100% + 14px) !important;
  left: 50% !important;
  transform: translateX(-50%) !important;
  grid-column: auto !important;
  width: auto !important;
  height: auto !important;
  margin: 0 !important;
  padding: 4px !important;
  background: none !important;
  border: none !important;
  border-radius: 0 !important;
  box-shadow: none !important;
}
#slideMenu .mobile-progress-header #mobile-progress-help i,
#slideMenu .mobile-progress-header #mobile-progress-help svg {
  width: 38px !important; /* same size as the other popups' help icon */
  height: 38px !important;
}

/* ═══════════════ 17. GETTING-STARTED SLIDERS (help-only) ═══════════════
   The per-tab sliders now open ONLY from the ? help icon (see help-modals.js).
   Remove their "don't show again" rows (the app already hides them on a
   fromHelp show; this guarantees it), and lift the overlays above the bottom
   dock (1700) and options sheet (1600) so a help-triggered slider covers them
   instead of hiding behind the dock at its stock z-index of 1400. */
#psDontShow,
#ptDontShow,
#wsDontShow {
  display: none !important;
}
#playSliderOverlay,
#practiceSliderOverlay,
#welcomeSliderOverlay {
  z-index: 2000 !important;
}

/* ═══════════════ 18. POPUP CLOSE ✕ + PROFILE-MENU ROWS ═══════════════ */

/* small round ✕ close, top-right of each options popup (injected by
   nav-behavior.js) — an extra visual cue alongside tap-outside-to-close.
   The popups (#slideMenu, #menuPanelContainer) are position:fixed, so this
   absolute button anchors to each popup's own top-right corner. */
.gl-popup-close {
  position: absolute;
  top: 10px;
  right: 12px;
  z-index: 5;
  /* 44px = Apple's minimum reliable touch target; the old 30px was easy for
     small fingers to miss ("erratic" taps). touch-action:manipulation drops the
     300ms double-tap delay so a single tap registers immediately. */
  width: 44px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border-radius: 9999px;
  border: 1px solid var(--gl-glass-line, rgba(255, 255, 255, 0.6));
  background: rgba(255, 255, 255, 0.9);
  color: var(--gl-ink, #1b2440);
  font-size: 26px;
  line-height: 1;
  cursor: pointer;
  touch-action: manipulation;
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  box-shadow: 0 2px 8px rgba(5, 18, 58, 0.18);
  transition: background-color 0.15s ease, transform 0.15s ease;
}
.gl-popup-close:hover {
  background: rgba(255, 255, 255, 1);
}
.gl-popup-close:active {
  transform: scale(0.92);
}

/* Manage account / Sign-up link → a proper menu row like the ⚙ Settings row
   (leading person glyph, left-aligned, no underline), not a small centred
   underlined link. Overrides the inline styles license-handler.js injects. */
#settingsDropdown a[href="/auth/"] {
  display: flex !important;
  align-items: center !important;
  gap: 10px !important;
  box-sizing: border-box !important;
  width: 100% !important;
  /* no side margin: nav-behavior.js lifts this link to be a direct child of
     #settingsDropdown (a sibling of the Settings row), so matching the base
     button's 10px 15px padding makes its glyph left-align with Settings */
  margin: 0 !important;
  padding: 10px 15px !important;
  text-align: left !important;
  text-decoration: none !important;
  font-size: 15px !important;
  font-weight: 500 !important;
  color: #333 !important;
  border-radius: 6px !important;
  transition: background-color 0.3s ease !important;
}
#settingsDropdown a[href="/auth/"]:hover {
  background-color: #eaf2ff !important;
}
#settingsDropdown a[href="/auth/"]::before {
  /* resources/parent_arrow.webp, replacing the Lucide "user-round" glyph. Still
     a ::before because the link is built by license-handler.js, so there is no
     markup here to hang an <img> on.

     Drawn as a background-image, NOT a CSS mask like the glyph it replaces: a
     mask only reads alpha, which would flatten this orange-circle-with-white-
     arrow into a flat #555 blob. That also means it does not follow the row's
     text colour — correct here, since the artwork carries its own.

     20px matches #settingsDropdown button svg (times-tables.css:635), which is
     what sizes the globe / gear / door glyphs on the rows below. */
  content: "";
  width: 20px;
  height: 20px;
  flex-shrink: 0;
  background: url("/resources/parent_arrow.webp") center / contain no-repeat;
}

/* ═══════════════ 19. OPTIONS-SHEET TITLES ═══════════════
   The bottom dock shows icons only; each per-tab options panel carries its
   name as a centred title with a short description (injected by
   sheet-headers.js). Scoped to the mobile sheet (#slideMenu). */
#slideMenu .gl-sheet-header {
  /* extra bottom padding gives a little more breathing room between the
     title/description and the first field below (requests #1/#2) */
  padding: 2px 16px 20px;
  text-align: center;
}
#slideMenu .gl-sheet-title {
  /* larger + dark navy for readability (test feedback #2). Bumped up from the
     14px field-label size so the sheet's name reads clearly as a heading. */
  margin: 0 0 5px;
  font-family: inherit;
  font-weight: 700;
  font-size: 19px;
  line-height: 1.2;
  color: #0F2244;
}

/* title + relocated ? help icon on one centred line (help-modals.js keeps its
   delegated handler; sheet-headers.js moved the button here out of its own
   full-width row). */
#slideMenu .gl-sheet-titlerow {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  margin-bottom: 5px;
}
#slideMenu .gl-sheet-titlerow .gl-sheet-title {
  margin: 0; /* the row owns the 5px gap to the description now */
}
/* shrink the help glyph (it was a 38px stand-alone row icon) to sit as a small
   secondary mark beside the title, and mute its colour so the title leads. */
#slideMenu .gl-sheet-help {
  padding: 0 !important;
  flex-shrink: 0;
}
#slideMenu .gl-sheet-help svg,
#slideMenu .gl-sheet-help i {
  width: 22px !important;
  height: 22px !important;
  color: #5b6b8c !important;
  stroke: #5b6b8c !important;
}
#slideMenu .gl-sheet-desc {
  /* larger + darker than before so the description is comfortable to read */
  margin: 0;
  font-family: inherit;
  font-weight: 500;
  font-size: 15px;
  line-height: 1.4;
  color: #475569;
}

/* The injected sheet header (sheet-headers.js) now names each panel, so hide
   the panels' own redundant in-content chrome:
   • Progress — the "View Mode" field label above the Overview/Aquarium toggle.
     The toggle keeps its inline margin-top plus the header's bottom padding, so
     it stays clearly separated from the title/description.
   • Rewards — the panel's own "Rewards Shop" <h3> and description <p>, PLUS the
     "Open Shop" button: switching to the Rewards tab already opens the shop
     behind the sheet, so the button only closed the sheet — redundant. The
     :not(.gl-sheet-desc) guard is essential: the injected header description is
     also a <p> inside #rewards-content and must stay visible. The gift glyph
     remains as the sheet's single visual. */
#slideMenu #progress-content .form-label[data-i18n="view_mode"],
#slideMenu #rewards-content h3,
#slideMenu #rewards-content p:not(.gl-sheet-desc),
#slideMenu #rewards-content #openRewardsShopBtn {
  display: none !important;
}

/* ═══════════════ 20. DESKTOP PROGRESS DROPDOWN — MATCH THE SHEET ═══════════
   The desktop top-tab Progress dropdown packed the toggle and the metric pills
   into one flex row (times-tables.js .aquarium-view-buttons), so the toggle
   truncated and the pills sat unequal. Re-lay it out like the mobile options
   sheet: toggle full-width on top, the two metric pills EQUAL-width in a
   frosted card, help centred below. Scoped to #menuPanelContainer. */
#menuPanelContainer .aquarium-view-buttons {
  display: flex !important;
  flex-direction: column !important;
  align-items: stretch !important;
  gap: 12px !important;
  width: 100% !important;
}
#menuPanelContainer #desktop-progress-toggle {
  box-sizing: border-box !important;
  width: 100% !important;
  max-width: none !important;
}
#menuPanelContainer .progress-header {
  display: grid !important;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) !important;
  align-items: stretch !important;
  gap: 10px !important;
  margin: 0 !important;
  padding: 12px !important;
  background: linear-gradient(165deg, rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.28)) !important;
  border: 1px solid var(--gl-glass-line, rgba(255, 255, 255, 0.5)) !important;
  border-radius: 16px !important;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.6) !important;
}
#menuPanelContainer .progress-header .heart-streak-pill,
#menuPanelContainer .progress-header .coin-pill {
  width: 100% !important;
  min-width: 0 !important;
  box-sizing: border-box !important;
  justify-content: center !important;
  gap: 6px !important;
  padding: 8px 10px !important;
}
/* protect the fixed glyphs + trim the heart bar so both pills fit the equal
   cell (same recipe as the mobile card) */
#menuPanelContainer .progress-header .heart-streak-pill > svg.heart-icon,
#menuPanelContainer .progress-header .coin-pill > div:first-child {
  flex-shrink: 0 !important;
}
#menuPanelContainer .progress-header .heart-streak-pill > div {
  gap: 6px !important;
  min-width: 0 !important;
}
#menuPanelContainer .progress-header .heart-streak-pill > div > div:first-child {
  width: 32px !important;
  flex-shrink: 0 !important;
}
#menuPanelContainer .progress-header .progress-percent,
#menuPanelContainer .progress-header .coin-count {
  min-width: 32px !important;
  font-size: 15px !important;
}
#menuPanelContainer .progress-header #desktop-progress-help {
  grid-column: 1 / -1 !important;
  justify-self: center !important;
  padding: 2px !important;
}
#menuPanelContainer .progress-header #desktop-progress-help i,
#menuPanelContainer .progress-header #desktop-progress-help svg {
  width: 30px !important;
  height: 30px !important;
}

/* ── Rewards options sheet: remove the decorative swimming-fish animation ──
   Per request. display:none also stops the CSS keyframes from ticking, so it's
   a clean removal (no lingering animation cost), and it's fully additive — the
   base .rewards-fish-container markup in the sheet stays untouched. */
.rewards-fish-container {
  display: none !important;
}

/* ═══════════════ FREDOKA WEIGHT — app-wide slim experiment ═══════════════
   Try one uniform, lighter weight on EVERY element that renders in the Fredoka
   display face (set directly as 'Fredoka' or via --gl-font-display) — headings,
   flashcard/percentage numerals, buttons + pills, the onboarding sliders, and
   the aquarium + voices chrome. Their original per-rule weights ran 400–800;
   this collapses them to one value.

   One knob: retune with --gl-fredoka-weight below, or delete this whole block to
   restore the original weights. EXCLUDED (tuned separately, lighter): the two
   NAV labels (#mobileModeLabel + .tab-buttons button, now on Nunito per an
   earlier request) and the SHOP item titles + category pills (.shop-item-name +
   .category-tab), which read too heavy at the sweep weight — see below. */
:root { --gl-fredoka-weight: 600; }

.ws-slide-title, .ws-game-pill-pct, .ws-tap-pill-pct, .ws-btn,
.ws-btn-primary, .ws-btn-green,
.ps-title, .ps-btn, .ps-gc, .ps-game-label, .ps-pill-text, .ps-streak,
.ps-coin-reward, .ps-title + .ps-body strong,
.pt-slide-title, .pt-btn, .pt-ap-card, .pt-toggle-pill,
h1, h2, h3, h4, .slide-title, .welcome-title, .menu-header h2,
.card-front .card-content, .card-back .card-content,
.card-back .card-content span, .slideshow-card .card-content,
.performance-display .percentage-text,
.play-button, .modal-button, .score, .buy-button,
.welcome-continue-btn, .primary-btn, .slide-menu .btn, .tab-grid-button,
.smart-hint, .slide-tab,
.gl-pref-modal .settings-modal-header h2,
#welcomeUnlockCodeInput, .form-input,
.voices-av-ini, .voices-titlerow h2 {
  font-weight: var(--gl-fredoka-weight) !important;
}
