:root {
  /* Redesign pass, referencing Duolingo/Kahoot-style kids-learning UI: one
     saturated "point" color (accent) carries every primary action and
     score/badge everywhere, so it's never ambiguous what the point color
     is; brand blue steps back to an identity/secondary role (logo, active
     tab, links). Every action color now has a "-dark" shade one step
     darker, used only for the pressed-button 3D edge below (see
     button.pill) — the tactile, satisfying-to-tap look these apps are
     known for, not just a flat rectangle. */
  --bg: #eef9f5;
  --card-bg: #ffffff;
  --primary: #2d6cff;
  --primary-dark: #1e4fcc;
  --accent: #22c9a3;
  --accent-dark: #0e9c7d;
  --success: #22c55e;
  --success-dark: #159647;
  --error: #ff4b4b;
  --error-dark: #d93434;
  --text: #333a37;
  --muted: #7c8985;
  --radius: 16px;
  --shadow: 0 8px 24px rgba(14, 156, 125, 0.16);
  /* The "bold outline cartoon" pass: every card/button/tab now gets a real
     dark rim (this color) plus a flat, direction-only drop shadow in the
     same color, instead of a diffuse blur — the tactile, illustrated look
     kids' apps in this genre use, and the piece that was missing when only
     colors/typography changed in earlier passes. One shared outline color
     (rather than each element inventing its own border tint) is what makes
     every card/button/tab read as one consistent system. Set to the exact
     same mint as --accent-dark (the Login button's own rim color) so the
     whole site's outline/shadow system reads as one unified mint, not a
     separate near-black tone next to it. */
  --outline: var(--accent-dark);
  /* Fredoka is the kid-friendly, rounded display face for anything short —
     titles, buttons, labels, numbers. Its Latin glyphs are wide-set and
     read as loose/awkward across a full sentence or Hangul text, so body
     copy (definitions, example sentences, longer descriptions) stays on
     the plain UI font below instead. Segoe UI/Trebuchet cover Korean; the
     Fredoka stack is Latin-only, so it silently falls through to them for
     Hangul labels rather than mis-rendering. */
  --font-heading: "Fredoka", "Segoe UI", "Trebuchet MS", Verdana, sans-serif;
  --font-body: "Segoe UI", "Trebuchet MS", Verdana, sans-serif;
}

* {
  box-sizing: border-box;
}

html {
  font-size: 14px;
}

body {
  margin: 0;
  font-family: var(--font-body);
  /* A warm glow behind the header instead of the old cool blue-gray wash —
     the point color (accent) now sets the mood even before you reach a
     button. */
  background: radial-gradient(1100px 520px at 50% -12%, #c9f2e6 0%, var(--bg) 60%);
  color: var(--text);
  min-height: 100vh;
}

header {
  padding: 12px 16px 4px;
  text-align: center;
  position: relative;
}

.upgrade-ready-banner {
  display: block;
  width: calc(100% - 24px);
  margin: 0 12px 10px;
  border: none;
  border-radius: 12px;
  background: linear-gradient(90deg, var(--accent), #5fe0c2);
  color: #0a2e26;
  font-weight: 700;
  font-size: 0.85rem;
  padding: 10px 14px;
  cursor: pointer;
  box-shadow: var(--shadow);
}

/* A class selector setting display: block would otherwise tie with (and, as
   the later rule, beat) the browser's own [hidden] { display: none } rule —
   this keeps the hidden attribute actually hiding the banner. */
.upgrade-ready-banner[hidden] {
  display: none;
}

/* Back to opposite corners (left = language, right = sign-in) — stacking
   both in one corner read as clutter. Log In gets the real visual weight
   (filled point color, its own drop shadow) since it's the one worth
   noticing; language is a quiet, muted utility toggle. */
.lang-toggle {
  position: absolute;
  top: 10px;
  left: 12px;
  border: 1px solid var(--outline);
  background: #fff;
  color: var(--muted);
  font-weight: 700;
  padding: 4px 11px;
  font-size: 0.75rem;
  border-radius: 999px;
  cursor: pointer;
  box-shadow: 0 2px 0 var(--outline);
}

.lang-toggle:active {
  transform: translateY(1px);
  box-shadow: none;
}

.auth-toggle-wrap {
  position: absolute;
  top: 10px;
  right: 12px;
}

.auth-toggle {
  border: 1px solid var(--accent-dark);
  background: var(--accent);
  color: #fff;
  font-weight: 700;
  padding: 5px 13px;
  font-size: 0.75rem;
  border-radius: 999px;
  cursor: pointer;
  /* The same pressed-button "3D" edge as button.pill (see below) — a solid
     dark rim along the bottom rather than a diffuse shadow, so it reads as a
     real tappable button, not just a colored label. The rim is this
     button's own accent-dark (or success-dark once signed in, below) rather
     than the shared --outline used on white surfaces — a filled button's rim
     matches its own fill instead of a generic dark line sitting on it. */
  box-shadow: 0 2px 0 var(--accent-dark);
  max-width: 45vw;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.auth-toggle:active {
  transform: translateY(1px);
  box-shadow: none;
}

.auth-toggle.auth-toggle-active {
  background: #dff5ee;
  border-color: var(--accent);
  color: var(--accent-dark);
  box-shadow: 0 2px 0 var(--accent);
}

.auth-toggle.auth-toggle-active:active {
  box-shadow: none;
}

.auth-menu {
  position: absolute;
  top: calc(100% + 6px);
  /* Anchored to the wrap's right edge (not left:0) now that the wrap
     itself sits in the top-right corner — a left-anchored menu would hang
     off the edge of the viewport. */
  right: 0;
  z-index: 20;
  background: #fff;
  border-radius: 12px;
  box-shadow: var(--shadow);
  display: flex;
  flex-direction: column;
  overflow: hidden;
  min-width: 140px;
}

.auth-menu[hidden] {
  display: none;
}

.auth-menu button {
  border: none;
  background: none;
  text-align: left;
  padding: 10px 14px;
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--text);
  cursor: pointer;
}

.auth-menu button:hover {
  background: var(--bg);
}

.auth-menu button + button {
  border-top: 1px solid #eef0f6;
}

header h1 {
  margin: 0;
  font-family: var(--font-heading);
  font-size: 1.3rem;
}

header h1 .emoji {
  font-size: 1.2rem;
}

header h1 .brand-logo {
  width: 34px;
  height: 34px;
  vertical-align: -9px;
}

.kid-modal-icon img {
  width: 64px;
  height: 64px;
  display: block;
  margin: 0 auto;
}

header p {
  margin: 3px 0 0;
  color: var(--muted);
  font-size: 0.85rem;
}

main {
  max-width: 720px;
  margin: 0 auto;
  padding: 10px 14px 20px;
}

.tabs-bar {
  display: flex;
  align-items: flex-start;
  justify-content: center;
  gap: 5px;
  margin: 10px 0 14px;
}

/* The landing tile grid already lists every menu once — showing the
   persistent nav row (and, on mobile, the bottom tab bar) on top of it would
   just repeat the same menus a second time. body.on-landing is toggled by
   goToTab() in app.js. Both bars come back the moment you open any section,
   where they're the quick way to jump straight to another one. */
body.on-landing .tabs-bar,
body.on-landing .bottom-tabs {
  display: none;
}

nav.tabs {
  display: flex;
  /* Shrinks (never grows) so it hugs its own buttons rather than stretching
     to fill .tabs-bar and pushing the More button away from it — but it can
     still shrink below its content width on a narrow screen, at which point
     overflow-x below takes over as a horizontal-scroll fallback. */
  flex: 0 1 auto;
  min-width: 0;
  flex-wrap: nowrap;
  overflow-x: auto;
  gap: 5px;
  /* "safe" centers the row when it fits, but falls back to start-alignment
     when it overflows so the first tab never scrolls out of reach */
  justify-content: safe center;
  /* nav.tabs needs overflow-x: auto for its horizontal-scroll fallback,
     which forces overflow-y to auto as an unavoidable side effect — without
     headroom above the buttons, that turns nav.tabs into a scroll container
     that clips right at the content edge, cutting off the top of a button's
     border/shadow the instant :hover's translateY(-1px) lifts it. padding-
     top reserves exactly that room; padding-bottom already did the same for
     the bottom box-shadow. */
  padding: 4px 0 2px;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: thin;
}

nav.tabs button {
  border: 1px solid var(--outline);
  background: var(--card-bg);
  color: var(--text);
  padding: 6px 10px;
  border-radius: 999px;
  font-family: var(--font-heading);
  font-size: 0.72rem;
  font-weight: 600;
  cursor: pointer;
  box-shadow: 0 2px 0 var(--outline);
  white-space: nowrap;
  flex: 0 0 auto;
  transition: transform 0.08s ease, box-shadow 0.08s ease, background 0.15s ease, color 0.15s ease;
}

nav.tabs button:hover {
  transform: translateY(-1px);
}

nav.tabs button:active {
  transform: translateY(1px);
  box-shadow: none;
}

/* A selected (blue-filled) tab keeps its rim in its own color rather than
   the shared dark --outline used by the unselected white tabs around it —
   the same "match the fill, not a generic dark line" rule as .landing-tile-
   highlight and the colored button.pill variants. */
nav.tabs button.active {
  background: var(--primary);
  color: #fff;
  border-color: var(--primary-dark);
  box-shadow: 0 2px 0 var(--primary-dark);
}

nav.tabs button.active:active {
  box-shadow: none;
}

/* Add Word / My Progress are the two tabs the monetization plan leans on —
   Add Word gates custom words behind a paid account already (see the
   premium-gate cards below), and My Progress is where the paid-only
   "Premium Insights" teaser lives. A real border (not a box-shadow ring)
   marks both — nav.tabs needs overflow-x:auto for its horizontal-scroll
   fallback, which forces its overflow-y to auto as a side effect (a CSS
   quirk: only one axis can stay "visible"), so anything drawn outside an
   element's own border box — a box-shadow ring included — risked getting
   clipped there. A border is part of the box itself and is never clipped
   this way; padding below is trimmed by the border's own width so the
   pill's overall size still matches its unhighlighted neighbors. */
/* Unselected, these now look exactly like the plain Home tab (white fill,
   dark outline rim) — only .active below still carries the accent-colored
   "selected" look, so the accent color reads as "you're here now" instead
   of permanently marking these two tabs as different from the rest. */
nav.tabs button.tab-btn-highlight.active {
  background: var(--accent);
  border-color: var(--accent-dark);
  color: #fff;
  box-shadow: 0 2px 0 var(--accent-dark);
}
nav.tabs button.tab-btn-highlight.active:active {
  box-shadow: none;
}

/* The Admin nav tab is a permanently mint-filled button — same look as the
   Login button — rather than the plain white/outline style every other tab
   uses, so it reads as its own elevated action instead of just another
   destination in the row. */
nav.tabs button[data-view="admincodes"] {
  background: var(--accent);
  border-color: var(--accent-dark);
  color: #fff;
  box-shadow: 0 2px 0 var(--accent-dark);
}
nav.tabs button[data-view="admincodes"]:active {
  box-shadow: none;
}

/* "Study" (Quiz/Spelling/Flashcards) and "Game" (Times Table/Typing Game)
   nav groups — a trigger button that reveals a small dropdown of the real
   destinations on hover (desktop) or tap (touch, since hover doesn't fire
   reliably there), rather than adding three more permanent buttons to an
   already-crowded nav row. The trigger deliberately isn't a .tab-btn (it has
   no data-view and shouldn't itself navigate anywhere) — see the tabGroups
   wiring in app.js for the open/close and has-active logic. */
.tab-group {
  position: relative;
  display: inline-flex;
}

.tab-group-trigger {
  display: inline-flex;
  align-items: center;
  gap: 3px;
}

.tab-caret {
  font-size: 0.65em;
  opacity: 0.7;
  transition: transform 0.15s ease;
}

.tab-group.open .tab-caret {
  transform: rotate(180deg);
}

/* Mirrors nav.tabs button.active — the trigger reads as "selected" whenever
   the currently-open view belongs to its group, even though the trigger
   itself never carries the ordinary .active class (that's reserved for an
   exact data-view match, which the trigger doesn't have). */
.tab-group-trigger.has-active {
  background: var(--accent);
  color: #fff;
  border-color: var(--accent-dark);
  box-shadow: 0 2px 0 var(--accent-dark);
}

/* Positioned with `position: fixed` and explicit top/bottom/left set by
   app.js's positionDropdown(), not by any CSS anchoring rule — nav.tabs
   needs overflow-x: auto for its horizontal-scroll fallback, which forces
   overflow-y to auto as an unavoidable side effect (see the box-shadow vs.
   border comment on nav.tabs button.tab-btn-highlight above for the same
   quirk elsewhere), and a dropdown left positioned relative to its trigger
   would just get clipped by that. app.js moves each dropdown out to <body>
   once, on first use, entirely sidestepping the issue. */
.tab-dropdown {
  /* Matches the site's own background instead of plain white, so the
     dropdown panel reads as part of the app's frame rather than a
     generic browser-menu box. */
  background: var(--bg);
  /* Matches the Login button's exact 1px border / 2px shadow-offset
     thickness instead of the bulkier 3px/3px default. */
  border: 1px solid var(--outline);
  border-radius: 16px;
  box-shadow: 0 2px 0 var(--outline);
  padding: 6px;
  display: none;
  flex-direction: column;
  gap: 4px;
  min-width: 165px;
  z-index: 2000;
}

.tab-dropdown.open {
  display: flex;
}

/* Dropdown items are moved out to <body> by app.js (see the comment on
   .tab-dropdown above), so they no longer match the `nav.tabs button`
   descendant selector that used to give them the pill look — this repeats
   that same look explicitly, plus a playful colored-hover state instead of
   a plain gray one, to match the rest of the app's bold-outline buttons. */
.tab-dropdown-item {
  width: 100%;
  justify-content: flex-start !important;
  text-align: left;
  font-family: var(--font-heading);
  font-weight: 600;
  font-size: 0.78rem;
  color: var(--text);
  background: #fff;
  border: 1px solid var(--outline);
  border-radius: 999px;
  box-shadow: 0 2px 0 var(--outline);
  padding: 7px 12px;
  cursor: pointer;
  transition: transform 0.08s ease, box-shadow 0.08s ease, background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}

.tab-dropdown-item:hover {
  transform: translateY(-1px);
  background: var(--accent);
  color: #fff;
  border-color: var(--accent-dark);
  box-shadow: 0 2px 0 var(--accent-dark);
}

.tab-dropdown-item:active {
  transform: translateY(1px);
  box-shadow: none;
}

.bottom-tab-group {
  position: relative;
}

.nav-badge {
  display: inline-block;
  margin-left: 3px;
  font-size: 0.85em;
  vertical-align: -1px;
}

.view {
  display: none;
}

.view.active {
  display: block;
  animation: fadeIn 0.2s ease;
}

@keyframes fadeIn {
  from { opacity: 0; transform: translateY(4px); }
  to { opacity: 1; transform: translateY(0); }
}

/* Landing / home screen (#view-landing) — every menu as one 3-column tile
   grid ("바둑판식"), ordered by importance reading left-to-right then
   top-to-bottom, instead of tucked behind a nav row + "More" dropdown. Each
   tile is a .tab-btn (see app.js's tabButtons), just styled like a bigger
   app-icon tile instead of a small pill, in the same bold-outline language
   as every other card/button. */
.landing-tiles {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 10px;
}

.landing-tile {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;
  gap: 6px;
  width: 100%;
  border: 1px solid var(--outline);
  border-radius: 18px;
  background: #fff;
  box-shadow: 0 2px 0 var(--outline);
  padding: 14px 6px 12px;
  font-family: var(--font-body);
  text-align: center;
  cursor: pointer;
  transition: transform 0.08s ease, box-shadow 0.08s ease;
}

/* Hovering a tile fills it with the same mint and rim as the Login button
   (--accent / --accent-dark), rather than just lifting it — the icon tile
   and text flip to white so they still read against the now-solid fill. */
.landing-tile:hover {
  transform: translateY(-1px);
  background: var(--accent);
  border-color: var(--accent-dark);
  box-shadow: 0 2px 0 var(--accent-dark);
}

.landing-tile:hover .landing-tile-icon {
  background: #fff;
  border-color: #fff;
}

.landing-tile:hover .landing-tile-title,
.landing-tile:hover .landing-tile-desc {
  color: #fff;
}

.landing-tile:active {
  transform: translateY(1px);
  box-shadow: none;
}

.landing-tile-icon {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border: 2px solid var(--outline);
  border-radius: 14px;
  background: var(--bg);
  font-size: 1.4rem;
  transition: background 0.08s ease, border-color 0.08s ease;
}

.landing-tile-body {
  flex: 1 1 auto;
  min-width: 0;
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
}

.landing-tile-title {
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: 0.8rem;
  line-height: 1.2;
  color: var(--text);
}

.landing-tile-desc {
  font-size: 0.66rem;
  line-height: 1.25;
  color: var(--muted);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* My Progress / Add Word — the two monetization-priority tiles — get the
   same accent treatment as their nav-bar pills, so they still read as the
   "important" ones even at the top of an already importance-ordered grid. */
.landing-tile-highlight {
  border-color: var(--accent);
  background: #f2fbf8;
  box-shadow: 0 2px 0 var(--accent-dark);
}

.landing-tile-highlight:active {
  box-shadow: none;
}

.landing-tile-highlight .landing-tile-icon {
  border-color: var(--accent);
  background: #dff5ee;
}

.landing-tile-badge {
  position: absolute;
  top: 8px;
  right: 8px;
  width: 20px;
  height: 20px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.72rem;
  background: #fff;
  border: 2px solid var(--outline);
  border-radius: 50%;
}

@media (max-width: 480px) {
  .landing-tiles {
    gap: 8px;
  }

  .landing-tile {
    padding: 10px 4px 9px;
    gap: 4px;
    border-radius: 14px;
  }

  .landing-tile-icon {
    width: 36px;
    height: 36px;
    font-size: 1.15rem;
    border-radius: 11px;
  }

  .landing-tile-title {
    font-size: 0.68rem;
  }

  /* Too little room left for a second line of body text once three tiles
     share a 390px-wide screen — the title alone still identifies the menu,
     same as an app icon's label. */
  .landing-tile-desc {
    display: none;
  }
}

.card {
  background: var(--card-bg);
  border: 3px solid var(--outline);
  border-radius: 20px;
  box-shadow: 0 4px 0 var(--outline);
  padding: 16px;
  margin-bottom: 14px;
}

.card h3 {
  margin: 0 0 10px;
  font-family: var(--font-heading);
  font-size: 1rem;
}

/* My Progress, Admin and Add Word specifically use the thinner "1px
   border / 2px shadow" proportions (matching the Login button) instead of
   the site's usual thicker 3px/4px card rim — everywhere else keeps the
   thicker look. */
#view-stats .card,
#view-admincodes .card,
#view-addword .card,
#view-myaccount .card,
#view-quiz .card,
#view-spelling .card,
#view-flashcards .card,
#view-wordlist .card {
  border-width: 1px;
  box-shadow: 0 2px 0 var(--outline);
}

/* Quiz/Spelling/Flashcards each now open with their own title so the card
   reads as a named section instead of jumping straight into controls — a
   full line-gap below (rather than .card h3's tighter 10px) so the title
   doesn't feel glued to the setting row underneath it. */
.view-section-title {
  margin: 0 0 20px;
  font-family: var(--font-heading);
  font-size: 1.1rem;
  font-weight: 700;
  color: var(--text);
}

/* Quiz/Spelling/Flashcards specifically get their section title tinted a
   dark mint (matching --accent-dark) instead of the plain neutral --text
   the other views' titles keep — these three are the app's main practice
   loops, so a shot of the brand color helps them read as "the main event"
   at a glance. */
#view-quiz .view-section-title,
#view-spelling .view-section-title,
#view-flashcards .view-section-title {
  color: var(--accent-dark);
}

.card-divider {
  border: none;
  border-top: 1px solid #eef0f6;
  margin: 16px 0;
}

/* A card locked behind a paid account: its own controls are dimmed and
   inert (pointer-events cascades to every descendant), and a transparent
   overlay sits on top to catch the click and show the upgrade prompt.
   position:relative lives on the specific cards (not gated behind the
   .premium-gate class) so the overlay's position:absolute always resolves
   against its own card — never escaping to some far-off positioned
   ancestor (which, when unlocked, swallowed clicks across the whole page,
   including the header's logout button). [hidden] is forced with
   !important so it always wins over the display:flex below, regardless of
   author-rule order — an unlocked card must never show this at all. */
#addword-extract-card,
#addword-manual-card,
#my-added-words-card,
#stats-insights-card {
  position: relative;
}

.premium-insights-list {
  margin: 0;
  padding-left: 1.2em;
  line-height: 1.9;
  font-size: 0.9rem;
}
.card.premium-gate > *:not(.premium-gate-overlay) {
  opacity: 0.4;
  pointer-events: none;
  user-select: none;
}
.premium-gate-overlay[hidden] {
  display: none !important;
}
.premium-gate-overlay {
  position: absolute;
  inset: 0;
  z-index: 5;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 12px;
  cursor: pointer;
  background: transparent;
  border-radius: var(--radius);
}
.premium-gate-label {
  display: flex;
  flex-direction: column;
  gap: 8px;
  background: var(--card-bg);
  border: 2px solid var(--accent);
  color: var(--text);
  padding: 18px 22px;
  border-radius: 14px;
  box-shadow: var(--shadow);
  max-width: min(320px, 90%);
}
.premium-gate-label-title {
  font-weight: 700;
  font-size: 1.2rem;
}
.premium-gate-label-desc {
  font-weight: 400;
  font-size: 0.95rem;
  line-height: 1.45;
  color: var(--muted);
}

/* My Progress's Premium Insights lock card and Add Word's premium lock
   banner match the Login button's thinner border/shadow proportions too
   (see #view-stats .card above). */
#view-stats .premium-gate-label,
#view-addword .premium-gate-label {
  border-width: 1px;
  box-shadow: 0 2px 0 var(--accent);
}

/* Fixed to the viewport rather than any one card — centered on the page and
   stays put while scrolling. Sits above ordinary content but well below the
   true modals (auth/upgrade overlays at z-index 1100+). Only the label
   itself is clickable — the transparent backdrop lets clicks on whatever's
   underneath (each card's own invisible overlay) still register. */
.premium-lock-banner {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px;
  pointer-events: none;
}
.premium-lock-banner[hidden] {
  display: none;
}
.premium-lock-banner .premium-gate-label {
  pointer-events: auto;
  cursor: pointer;
}

/* Add Word: Extract-words and Add-manually side by side on a wide enough
   screen, My Added Words full-width underneath; stacks to one column below
   the shared 480px mobile breakpoint (see that media query). */
.addword-top-row {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  margin-bottom: 12px;
}
.addword-top-row > .card {
  flex: 1 1 0;
  min-width: 0;
  margin-bottom: 0;
}

.section-title-with-info {
  display: flex;
  align-items: center;
  gap: 6px;
}

/* A "?" icon that reveals its description on hover/focus instead of it
   always taking up a line of its own — used to shrink the Extract Words
   section now that it shares a card with Add a word manually. */
.info-icon {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  flex: 0 0 auto;
  border-radius: 50%;
  background: #e6e9f4;
  color: var(--primary-dark);
  font-size: 0.7rem;
  font-weight: 700;
  cursor: help;
}

.info-icon .info-tooltip {
  position: absolute;
  top: 130%;
  right: 0;
  z-index: 5;
  width: 260px;
  max-width: min(260px, calc(100vw - 32px));
  background: #1f2430;
  color: #fff;
  font-size: 0.72rem;
  font-weight: 400;
  line-height: 1.4;
  padding: 8px 10px;
  border-radius: 8px;
  box-shadow: var(--shadow);
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.15s ease;
}

.info-icon:hover .info-tooltip,
.info-icon:focus .info-tooltip,
.info-icon:focus-within .info-tooltip {
  opacity: 1;
  visibility: visible;
}

.setting-row {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: center;
  margin-bottom: 12px;
}

.setting-row-nowrap {
  flex-wrap: nowrap;
}

.setting-row-nowrap select {
  min-width: 0;
  flex: 0 1 auto;
}

/* Spelling's Number-of-Questions row is the only thing in that row, so it
   reads better centered than pinned to the left edge like a multi-item
   settings row. */
.setting-row-center {
  justify-content: center;
}

#view-spelling .setting-row-center {
  margin-bottom: 22px;
}

.icon-btn {
  display: flex;
  align-items: center;
  gap: 5px;
  flex: 0 0 auto;
  white-space: nowrap;
}

.setting-row label {
  font-family: var(--font-heading);
  font-weight: 700;
  color: var(--text);
  font-size: 0.85rem;
}

/* Native <select>s used to be a plain thin-gray-border, system-font box —
   the one spot in the app still built out of default browser form-control
   styling instead of its own bold-outline cartoon look. `appearance: none`
   plus a hand-drawn caret puts it back in that same visual language: pill
   shape, dark rim, flat shadow, and the Fredoka heading font every button
   and label already use. */
select {
  padding: 6px 26px 6px 12px;
  border-radius: 999px;
  border: 1px solid var(--outline);
  background-color: #fff;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none'%3E%3Cpath d='M5 8l5 5 5-5' stroke='%232c2318' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 9px center;
  background-size: 11px;
  font-family: var(--font-heading);
  font-weight: 600;
  font-size: 0.85rem;
  color: var(--text);
  box-shadow: 0 2px 0 var(--outline);
  appearance: none;
  -webkit-appearance: none;
  -moz-appearance: none;
  cursor: pointer;
  transition: transform 0.08s ease, box-shadow 0.08s ease;
}

select:hover {
  transform: translateY(-1px);
}

select:active {
  transform: translateY(1px);
  box-shadow: none;
}

/* The Level dropdown on the "Single word" add form gets the same orange
   as the Save word button right below it, instead of the shared neutral
   outline color every other select uses — a visual pairing since they sit
   in the same form and Save word is the form's main action. */
#manual-level {
  box-shadow: 0 2px 0 var(--accent);
}

#manual-level:active {
  box-shadow: none;
}

/* Browsers apply :focus-visible to <select> on a plain mouse click, not
   just keyboard focus, which was leaving a blue outline ring behind every
   time a dropdown was opened. The pill shape and hover/active states
   already give plenty of visual feedback, so the ring is dropped. */
select:focus-visible {
  outline: none;
}

/* Newer Chrome versions can render a <select>'s open dropdown list using
   the "customizable select" popup, which borrows box-model properties
   (border-radius, background, shadow) from the select control itself
   instead of drawing a plain OS list. Our select is a fully rounded
   999px pill, meant for a single-line control — a taller popup with
   several rows inheriting that same radius renders as an oversized,
   mostly-blank rounded shape instead of a normal option list, which is
   what was showing up as a stray white block covering nearby content.
   @supports scopes this reset to only the browsers that actually have
   this popup (so it's a no-op everywhere else), and gives the popup its
   own sane, non-pill appearance: a small rounded rectangle with a
   visible border, sized to fit its rows instead of the control's shape. */
@supports selector(::picker(select)) {
  select {
    appearance: base-select;
  }

  select::picker(select) {
    appearance: base-select;
    border-radius: 12px;
    border: 1px solid var(--outline);
    background: #fff;
    box-shadow: 0 6px 16px rgba(44, 35, 24, 0.18);
    padding: 4px;
    margin-top: 6px;
  }

  select::picker-icon {
    display: none;
  }

  select option {
    padding: 6px 10px;
    border-radius: 8px;
  }

  select option:hover,
  select option:focus {
    background: #e4f3ee;
  }
}

/* Flashcards */
.flashcard-row {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
}

/* Back and Next both start as plain white/black pills; on hover Back
   suggests "go back" with a neutral gray fill, while Next picks up the
   Login button's own mint fill, hinting it moves you forward, without
   either button needing a permanent color of its own. */
#flash-prev:hover {
  background: #e7e7ec;
}

#flash-next:hover {
  background: var(--accent);
  color: #fff;
  border-color: var(--accent-dark);
}

/* Spelling's Back/Next start white/black like Flashcards' pair, but once
   Next actually becomes pressable (the answer's been checked), both flip
   color for the rest of that word — Next to the Login button's own mint as
   the obvious "go forward" action, Back to gray alongside it — rather than
   only hinting at it on hover. :has() lets Back react to its sibling
   Next's own state without any extra class from JS. */
#spelling-next:not(:disabled) {
  background: var(--accent);
  color: #fff;
  border-color: var(--accent-dark);
}

.flash-controls:has(#spelling-next:not(:disabled)) #spelling-back {
  background: #e7e7ec;
}

.check-icon-svg {
  width: 1em;
  height: 1em;
  flex: 0 0 auto;
  vertical-align: -0.15em;
  margin-right: 0.35em;
}

.flash-side-btn {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 5px;
}

#flashcard-stage {
  perspective: 1200px;
  max-width: 340px;
  flex: 1 1 auto;
  margin: 0;
}

.flashcard {
  position: relative;
  min-height: 170px;
  border-radius: var(--radius);
  cursor: pointer;
  transform-style: preserve-3d;
  transition: transform 0.5s;
}

.flashcard.flipped {
  transform: rotateY(180deg);
}

.flashcard-face {
  position: absolute;
  inset: 0;
  backface-visibility: hidden;
  border: 3px solid var(--outline);
  border-radius: 20px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 16px;
  text-align: center;
  box-shadow: 0 4px 0 var(--outline);
}

.flashcard-front {
  background: var(--bg);
  color: var(--accent-dark);
}

.flashcard-front .word {
  font-family: var(--font-heading);
  font-size: 1.5rem;
  /* The page only loads Fredoka's 500 and 700 weights (see the Google
     Fonts <link> in index.html) — asking for 600 without that weight
     actually loaded makes the browser substitute the nearest heavier
     match, which is 700, so it silently rendered just as bold as before.
     500 is the lightest weight actually available. */
  font-weight: 500;
}

/* When the meaning is shown on the front instead of the word, it's usually
   a full sentence rather than a single word — shrink it so longer text still
   fits comfortably. */
.flashcard.front-meaning .flashcard-front .word {
  font-size: 1.05rem;
  font-weight: 500;
}

.flashcard-back {
  background: #fff;
  transform: rotateY(180deg);
}

.flashcard-back .definition {
  font-size: 0.95rem;
  font-weight: 600;
  margin-bottom: 6px;
}

.flashcard-back .example {
  color: var(--muted);
  font-style: italic;
  font-size: 0.85rem;
}

.hint {
  text-align: center;
  color: var(--muted);
  font-size: 0.75rem;
  margin-top: 14px;
}

.flash-controls {
  display: flex;
  justify-content: center;
  gap: 8px;
  margin-top: 14px;
  flex-wrap: wrap;
}

/* The category/front-side/source row sat almost flush against the big
   flashcard below it — give Flashcards the same breathing room Quiz's
   setting row already got. */
#view-flashcards .setting-row {
  margin-bottom: 26px;
}

/* Every pill button gets the same "pressed" look: a solid shade-darker rim
   along the bottom edge (a flat box-shadow, not a blur) that the button
   sinks into on tap — the tactile, satisfying-to-press style familiar from
   Duolingo/Kahoot-style kids' apps, instead of a flat colored rectangle.
   Each color variant only needs to set --pill-shadow to its own "-dark"
   token; the shape/animation lives here once. */
/* Every pill now shares one dark outline (border + flat shadow) regardless
   of its fill color — that shared rim, not each variant's own darker shade,
   is what reads as "one consistent button system" instead of a pile of
   individually-colored rectangles. Only the fill changes between variants. */
button.pill {
  --pill-shadow: var(--outline);
  /* A thinner border than the shadow's offset (1px vs 2px below) is what
     makes the shadow read as its own raised strip rather than just more
     border — equal thicknesses made the whole rim blend into one flat
     outline with no visible "lift". */
  border: 1px solid var(--outline);
  border-radius: 999px;
  padding: 8px 17px;
  font-family: var(--font-heading);
  font-weight: 600;
  cursor: pointer;
  font-size: 0.85rem;
  transition: transform 0.08s ease, box-shadow 0.08s ease;
  box-shadow: 0 2px 0 var(--pill-shadow);
}

button.pill:active {
  transform: translateY(1px);
  box-shadow: none;
}

/* Only .neutral (white fill, dark text) keeps the shared --outline rim —
   every solid-colored fill instead gets a border + shadow pulled from its
   own "-dark" shade, so the rim always reads as part of that button's own
   color rather than a mismatched dark line sitting on top of it. */
button.pill.primary { background: var(--primary); color: #fff; border-color: var(--primary-dark); --pill-shadow: var(--primary-dark); }
button.pill.success { background: var(--success); color: #fff; border-color: var(--success-dark); --pill-shadow: var(--success-dark); }
button.pill.error { background: var(--error); color: #fff; border-color: var(--error-dark); --pill-shadow: var(--error-dark); }
button.pill.neutral { background: #fff; color: var(--text); }
button.pill.accent { background: var(--accent); color: #fff; border-color: var(--accent-dark); --pill-shadow: var(--accent-dark); }
button.pill.dark { background: #1f2430; color: #fff; border-color: #0d0f14; --pill-shadow: #0d0f14; }
button.pill.green { background: #2bb673; color: #fff; border-color: #1d8a56; --pill-shadow: #1d8a56; }

button.pill.small {
  border-width: 1px;
  padding: 6px 12px;
  font-size: 0.72rem;
  box-shadow: 0 2px 0 var(--pill-shadow);
}

button.pill.small:active {
  box-shadow: none;
}

button.pill:disabled {
  opacity: 0.5;
  cursor: default;
  transform: none;
}

.my-added-words-header {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 14px;
}

.level-bulk-select {
  padding: 5px 10px;
  font-size: 0.72rem;
  border-radius: 999px;
}

.level-bulk-select:disabled {
  opacity: 0.5;
  cursor: default;
}

/* "Change level" and "Word Management" turn the same mint as the Login
   button on hover, same as #custom-export-btn below, so all three read as
   one family of secondary actions on this row. */
.level-bulk-select:not(:disabled):hover {
  background-color: var(--accent);
  color: #fff;
}

#custom-export-btn:hover {
  background: var(--accent);
  color: #fff;
  border-color: var(--accent-dark);
  transform: translateY(-1px);
}

.my-added-words-header-actions {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
  /* Forces this row onto its own line under the title, rather than sharing
     the title's line whenever there happens to be room for both. */
  flex-basis: 100%;
}

.wordlist-item .cw-select {
  width: 18px;
  height: 18px;
  flex: 0 0 auto;
  cursor: pointer;
}

/* The "All" select-all/clear-all toggle — same visual weight as the
   pill.neutral.small buttons it sits beside, just built from a real
   checkbox+label so its own checked state reflects the grid's selection. */
.select-all-toggle {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  background: #e6e9f4;
  color: var(--text);
  border-radius: 999px;
  padding: 5px 11px;
  font-size: 0.72rem;
  font-weight: 600;
  cursor: pointer;
}

.select-all-toggle input[type="checkbox"] {
  width: 14px;
  height: 14px;
  cursor: pointer;
}

.speak-btn {
  border: none;
  background: #eef2fb;
  color: var(--primary-dark);
  border-radius: 999px;
  padding: 5px 10px;
  cursor: pointer;
  font-size: 0.75rem;
}

/* The flashcard's pronunciation button — the one weak spot called out in
   the reference app was that "hear it" was easy to miss and unclear whether
   it had actually fired, so this is deliberately the biggest, most obvious
   tappable thing on the card front, with its own always-visible label and a
   pulse while audio is actually playing (see speak()'s onstart/onend). It
   uses the warm accent color rather than the card's own blue — a same-family
   blue-on-blue button read as washed out against the card's blue gradient
   background; the accent orange actually pops against it. Extra gap below
   the button (12px, not 4px) also keeps the pulse's scale-up from growing
   down into the label underneath it. */
.speak-btn-hero-wrap {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  margin-bottom: 10px;
}

button.speak-btn-hero {
  width: 56px;
  height: 56px;
  border-radius: 50%;
  background: var(--accent);
  color: #fff;
  border: 1px solid var(--accent-dark);
  box-shadow: 0 2px 0 var(--accent-dark);
  font-size: 1.5rem;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  transition: transform 0.08s ease, box-shadow 0.08s ease;
}

button.speak-btn-hero:active {
  transform: translateY(2px);
  box-shadow: none;
}

button.speak-btn-hero.speak-btn-active {
  animation: speak-btn-pulse 0.6s ease-in-out infinite;
}

@keyframes speak-btn-pulse {
  0%, 100% {
    transform: scale(1);
    box-shadow: 0 2px 0 var(--accent-dark);
  }
  50% {
    transform: scale(1.05);
    box-shadow: 0 3px 0 var(--accent-dark);
  }
}

.speak-btn-hero-label {
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: 0.72rem;
  color: var(--accent-dark);
}

/* Quiz */
#quiz-question {
  font-size: 1rem;
  font-weight: 600;
  text-align: center;
  margin-bottom: 12px;
}

#quiz-question .quiz-target {
  color: var(--primary);
}

.options {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
}

/* Typing Game (falling words) */
.typegame-lives {
  font-size: 0.95rem;
  letter-spacing: 2px;
}

.typegame-stage {
  position: relative;
  height: 320px;
  border-radius: var(--radius);
  overflow: hidden;
  margin-bottom: 10px;
  background: linear-gradient(180deg, #dfe9ff 0%, #f4f7fb 100%);
  box-shadow: inset 0 0 0 2px #d9e2f7;
  transition: background 0.6s ease;
}

/* Five distinct scenes cycling with the stage counter — day (the default
   background above, stage-scene-0 needs no override), sunset, dusk, deep
   space, and underwater — so a stage-up reads as "somewhere new" rather
   than just a faster falling rate. CSS-only (no external image assets). */
.typegame-stage.stage-scene-1 {
  background: linear-gradient(180deg, #ffd9a8 0%, #ffb199 55%, #ff8a80 100%);
}
.typegame-stage.stage-scene-2 {
  background: linear-gradient(180deg, #232a52 0%, #333f7d 55%, #4c56a0 100%);
}
.typegame-stage.stage-scene-3 {
  background: linear-gradient(180deg, #0b0f2e 0%, #1c1450 55%, #341a6b 100%);
}
.typegame-stage.stage-scene-4 {
  background: linear-gradient(180deg, #9be8f2 0%, #4fb8d6 55%, #1f7fa8 100%);
}

/* A slow, low-opacity cloud/star drift — just enough movement to feel alive
   without competing for attention with the falling words. */
.typegame-sky {
  position: absolute;
  inset: 0;
  overflow: hidden;
}

.typegame-cloud {
  position: absolute;
  font-size: 1.8rem;
  opacity: 0.5;
  animation: typegame-drift 55s linear infinite;
}

.typegame-cloud-1 { top: 6%; left: -12%; }
.typegame-cloud-2 { top: 32%; left: -22%; font-size: 1.3rem; opacity: 0.35; animation-duration: 70s; animation-delay: -20s; }
.typegame-cloud-3 { top: 58%; left: -16%; font-size: 1.5rem; opacity: 0.3; animation-duration: 62s; animation-delay: -40s; }

@keyframes typegame-drift {
  from { transform: translateX(0); }
  to { transform: translateX(800px); }
}

.typegame-star {
  position: absolute;
  font-size: 1rem;
  opacity: 0.5;
  animation: typegame-twinkle 2.6s ease-in-out infinite;
}

.typegame-star-1 { top: 12%; right: 16%; }
.typegame-star-2 { top: 66%; right: 28%; font-size: 0.85rem; animation-delay: 1.2s; }

@keyframes typegame-twinkle {
  0%, 100% { opacity: 0.2; transform: scale(0.9); }
  50% { opacity: 0.7; transform: scale(1.15); }
}

/* Shared look for the round icon buttons scattered over the falling-word
   games (mute, pause, end) — gradient fill, soft tinted shadow, and a
   playful scale+spin on hover/press. Each button's own class still owns
   its position, size, and gradient color. */
/* Pause/Mute/Expand used to each get their own glowing gradient (orange,
   blue, green) with a soft color-matched blur shadow — a different visual
   language from the rest of the app's flat-shadow bold-outline buttons, and
   three different colors for three buttons that aren't actually different
   in importance. They now share one look (white circle, dark rim, flat
   shadow) like every other button in the app; End keeps its own red
   identity since it's a genuinely different, one-way action. */
.round-icon-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  border: 2px solid var(--outline);
  border-radius: 50%;
  cursor: pointer;
  background: #fff;
  color: var(--text);
  box-shadow: 0 3px 0 var(--outline);
  transition: transform 0.1s ease, box-shadow 0.1s ease;
}

.round-icon-btn:hover {
  transform: translateY(-1px);
}

.round-icon-btn:active {
  transform: translateY(2px);
  box-shadow: 0 1px 0 var(--outline);
}

.typegame-pause-btn,
.typegame-mute-btn,
.typegame-expand-btn {
  width: 32px;
  height: 32px;
  font-size: 1rem;
  z-index: 3;
  /* Matches the Login button's exact 1px border / 2px shadow-offset
     thickness rather than the bulkier default .round-icon-btn rim. */
  border-width: 1px;
  box-shadow: 0 2px 0 var(--outline);
}

.typegame-pause-btn:active,
.typegame-mute-btn:active,
.typegame-expand-btn:active {
  box-shadow: none;
}

.typegame-pause-btn {
  position: absolute;
  top: 8px;
  left: 8px;
  color: var(--accent-dark);
}

.typegame-pause-btn[hidden] {
  display: none;
}

.typegame-expand-btn {
  position: absolute;
  top: 8px;
  right: 8px;
  color: var(--accent-dark);
}

/* Mute now stacks directly under Expand on the right, instead of sitting in
   its own column under Pause on the left — one cluster of "view options"
   buttons instead of two separate floating pairs. */
.typegame-mute-btn {
  position: absolute;
  top: 48px;
  right: 8px;
}

/* A taller play area — see typegame-expand-btn's click handler — while
   keeping the score row and input/hint below it in normal document flow,
   so both stay visible and usable, just further down the page. */
.typegame-stage.typegame-stage-expanded {
  height: 600px;
}

.typegame-end-btn {
  position: absolute;
  bottom: 10px;
  right: 10px;
  z-index: 3;
  width: 36px;
  height: 36px;
  background: linear-gradient(145deg, var(--accent), var(--accent-dark));
  color: #fff;
  box-shadow: 0 3px 10px rgba(14, 156, 125, 0.55);
  font-size: 0.95rem;
  font-weight: 700;
}

.typegame-end-btn:hover {
  background: linear-gradient(145deg, #4fe0c0, var(--accent-dark));
}

.typegame-end-btn[hidden] {
  display: none;
}

.typegame-stage.typegame-shake {
  animation: typegame-shake 0.3s ease;
}

@keyframes typegame-shake {
  0%, 100% { transform: translateX(0); }
  25% { transform: translateX(-6px); }
  75% { transform: translateX(6px); }
}

.typegame-words {
  position: absolute;
  inset: 0;
}

.typegame-word {
  position: absolute;
  transform: translateX(-50%);
  background: #fff;
  border: 2px solid var(--accent);
  border-radius: 999px;
  padding: 5px 12px;
  font-weight: 700;
  font-size: 0.95rem;
  white-space: nowrap;
  box-shadow: var(--shadow);
}

.typegame-word.tw-lock {
  border-color: var(--accent-dark);
  background: #eaf9f3;
}

.typegame-word.tw-cleared {
  animation: typegame-word-pop 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
  pointer-events: none;
}

@keyframes typegame-word-pop {
  0% { transform: translateX(-50%) scale(1); opacity: 1; }
  35% { transform: translateX(-50%) scale(1.4); opacity: 1; }
  100% { transform: translateX(-50%) scale(0.2); opacity: 0; }
}

/* A short expanding/fading burst ring spawned at a cleared item's position —
   sells the "pop" alongside the word's own scale-and-vanish above. */
.typegame-pop-fx {
  position: absolute;
  width: 14px;
  height: 14px;
  margin-left: -7px;
  margin-top: -7px;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(255, 255, 255, 0.95) 0%, rgba(122, 189, 255, 0.65) 45%, rgba(122, 189, 255, 0) 72%);
  pointer-events: none;
  z-index: 1;
  animation: typegame-pop-burst 0.45s ease-out forwards;
}

@keyframes typegame-pop-burst {
  0% { transform: scale(0.4); opacity: 0.9; }
  100% { transform: scale(7); opacity: 0; }
}

.typegame-word .tw-typed {
  color: var(--success);
}

.typegame-word .tw-rest {
  color: var(--text);
}

.typegame-overlay {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  text-align: center;
  padding: 16px;
  background: rgba(20, 28, 55, 0.72);
  color: #fff;
}

.typegame-overlay[hidden] {
  display: none;
}

.typegame-overlay h3 {
  margin: 0;
  font-size: 1.2rem;
  font-family: var(--font-heading);
  font-weight: 700;
}

/* The Game Over mascot — a little Australian-animal icon above the title,
   matching the "friendly mascot + message" pattern the spelling report's
   celebration-card already uses, so the screen reads as a fun beat rather
   than a flat stop-sign moment. */
.game-over-mascot {
  display: block;
  font-size: 2.6rem;
  line-height: 1;
  animation: goal-banner-bounce 1.4s ease-in-out infinite;
}

/* Just the Paused overlays get a mint-tinted heading (icon included, since
   it's plain text glued to the word, not a separate colorable element) —
   Start/Game Over keep the plain white heading. */
#typegame-pause-overlay h3,
#timestable-pause-overlay h3 {
  color: #7cf2d6;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
}

.pause-icon-svg {
  flex: 0 0 auto;
  vertical-align: -2px;
}

.typegame-overlay p {
  margin: 0;
  font-size: 0.85rem;
  color: #dbe3f7;
  max-width: 280px;
}

/* Final/Best score are the numbers someone actually cares about on this
   screen, so they get their own bold, bigger, Fredoka treatment instead of
   blending into the same small pale-blue paragraph style as the rest of
   the overlay's incidental copy. */
#typegame-final-score,
#timestable-final-score {
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: 1.15rem;
  color: #fff;
}

#typegame-high-score,
#timestable-high-score {
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: 0.95rem;
  color: #7cf2d6;
}

/* Times Table start screen: a two-line "how to answer" example, built by JS
   (not the generic data-i18n text swap) so the example numbers can be bold
   and colored. Both lines are centered, per how the rest of this overlay
   already lays out. */
.timestable-instructions {
  max-width: 300px;
  line-height: 1.6;
}
.timestable-instructions .ti-line {
  display: block;
}
.timestable-instructions .ti-highlight {
  color: var(--accent);
  font-weight: 700;
}

/* The Number-of-Questions-style stepper reads fine on a light card, but its
   label uses a muted grey meant for that background — override just the
   label color here so it's legible against the Times Table start screen's
   dark overlay. */
.typegame-overlay .goal-stepper-label {
  color: #dbe3f7;
}

.timestable-limit-msg {
  margin: 0;
  font-size: 0.8rem;
  color: #a8f0dc;
  max-width: 280px;
}

.typegame-encourage-msg {
  margin: 0;
  font-size: 0.85rem;
  color: #dbe3f7;
  max-width: 280px;
}

/* Stage indicator, next to the score tag in the HUD row */
.stage-tag {
  font-weight: 700;
  color: var(--accent, var(--primary-dark));
  font-size: 0.85rem;
}

.stage-tag:empty {
  display: none;
}

/* A brief, non-blocking celebration banner shown on a stage-up: sits above
   the falling items but below the mute/pause buttons, ignores clicks so it
   never interrupts typing, and is toggled purely via the [hidden] attribute
   plus this pop-in/out keyframe (JS sets a timeout to re-hide it). */
.typegame-stage-banner {
  position: absolute;
  top: 14%;
  left: 50%;
  z-index: 4;
  transform: translateX(-50%);
  pointer-events: none;
  background: rgba(30, 40, 75, 0.85);
  color: #fff;
  font-weight: 700;
  font-size: 0.95rem;
  text-align: center;
  padding: 10px 18px;
  border-radius: 999px;
  box-shadow: var(--shadow);
  white-space: nowrap;
  animation: typegame-stage-banner-pop 1.8s ease forwards;
}

.typegame-stage-banner[hidden] {
  display: none;
}

@keyframes typegame-stage-banner-pop {
  0% { opacity: 0; transform: translateX(-50%) scale(0.85) translateY(6px); }
  15% { opacity: 1; transform: translateX(-50%) scale(1.05) translateY(0); }
  25% { transform: translateX(-50%) scale(1) translateY(0); }
  80% { opacity: 1; }
  100% { opacity: 0; transform: translateX(-50%) scale(1) translateY(-8px); }
}

.typegame-input {
  width: 100%;
  padding: 10px 14px;
  border-radius: 999px;
  /* Same site-background border treatment as #spelling-input — thick
     enough (4px) to actually read as a border against the page's own
     near-identical background color, instead of all but disappearing. */
  border: 4px solid var(--bg);
  font-size: 1rem;
  text-align: center;
}

.typegame-input:disabled {
  background: #f0f2f8;
}

.typegame-input:focus {
  outline: none;
  border-color: var(--accent-dark);
}

.typegame-input.typegame-input-error {
  border-color: var(--error);
}

.typegame-typo-msg {
  text-align: center;
  color: var(--error);
  font-weight: 700;
  font-size: 0.85rem;
  margin: 0 0 6px;
}

.typegame-typo-msg[hidden] {
  display: none;
}

/* The generic .hint style (small, muted grey) reads fine as a footnote
   elsewhere, but the falling-game input instructions are functional, not
   decorative — bump contrast and size so it's actually legible under the
   input box. */
#view-typegame .hint,
#view-timestable .hint {
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--text);
  background: #eef2fb;
  border-radius: 10px;
  padding: 8px 12px;
  line-height: 1.4;
  text-align: left;
}

@media (max-width: 480px) {
  /* Stacked (column) at this width, so the row's cross-axis is now
     horizontal — align-items: flex-start (inherited from the desktop
     side-by-side layout) left each card shrink-to-fit its own content
     instead of matching the other's width. Stretching them back to the
     row's full width keeps both boxes the same width once they're
     stacked. */
  .addword-top-row {
    flex-direction: column;
    align-items: stretch;
  }
  .options { grid-template-columns: 1fr; }

  /* Quiz's Category row and Number of Questions row each wrap onto their
     own line at this width — centering them (instead of the default
     left-packed flex-start) reads as tidy rather than off-balance. */
  #view-quiz .setting-row {
    justify-content: center;
  }

  .flash-side-btn {
    width: 46px;
    height: 46px;
    padding: 0;
    border-radius: 50%;
    justify-content: center;
  }

  .flash-side-btn .btn-label {
    display: none;
  }

  .flash-side-btn .btn-arrow {
    font-size: 1.5rem;
    line-height: 1;
  }

  #flashcard-stage {
    max-width: none;
  }

  header {
    padding-left: 92px;
    padding-right: 92px;
  }

  header h1 {
    font-size: 0.98rem;
  }

  .auth-toggle,
  .lang-toggle {
    max-width: 76px;
    font-size: 0.66rem;
    padding: 5px 8px;
  }

  /* Below 480px the top tab row is replaced by the fixed .bottom-tabs bar
     (see its own rules further down) — a mobile-app pattern that reads much
     less "2000s webpage" than a wrapping row of pill buttons. */
  .tabs-bar {
    margin: 0;
  }

  nav.tabs {
    display: none;
  }
}

/* Mobile-only bottom tab bar — see index.html's .bottom-tabs markup. Hidden
   entirely above the 480px breakpoint, where the top nav.tabs row is used
   instead. */
.bottom-tabs {
  display: none;
}

@media (max-width: 480px) {
  body {
    /* Reserves room at the very bottom of the page so the fixed bar never
       overlaps the footer text above it. */
    padding-bottom: 74px;
  }

  .bottom-tabs {
    display: flex;
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 40;
    background: var(--card-bg);
    border-top: 3px solid var(--outline);
    padding: 6px 4px calc(6px + env(safe-area-inset-bottom));
    gap: 2px;
  }

  .bottom-tab-btn {
    flex: 1 1 0;
    min-width: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 2px;
    border: none;
    background: none;
    color: var(--muted);
    font-family: var(--font-heading);
    font-weight: 600;
    font-size: 0.62rem;
    padding: 5px 2px;
    border-radius: 12px;
    cursor: pointer;
    position: relative;
    transition: color 0.15s ease, background 0.15s ease;
  }

  /* The Study/Game trigger's wrapper is the actual flex child of .bottom-tabs
     now (so it can also anchor the upward dropdown) — it takes over the
     equal-width sizing .bottom-tab-btn normally gets directly, and its
     trigger button fills the wrapper. */
  .bottom-tabs .tab-group {
    flex: 1 1 0;
    min-width: 0;
    display: flex;
  }

  .bottom-tabs .tab-group .tab-group-trigger {
    flex: 1 1 auto;
    width: 100%;
  }

  .tab-dropdown-up .tab-dropdown-item {
    font-size: 0.68rem;
  }

  .bottom-tab-icon {
    font-size: 1.25rem;
    line-height: 1;
  }

  .bottom-tab-label {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 100%;
  }

  .bottom-tab-btn.active,
  .bottom-tab-btn.has-active {
    color: var(--primary-dark);
    background: #eaf1ff;
  }

  /* The two monetization tabs (My Progress / Add Word) keep their badge
     visible in the bottom bar too, just repositioned to sit above the icon
     rather than inline after the label — there isn't room for both on this
     narrow a button. */
  .bottom-tab-btn-highlight.active,
  .bottom-tab-btn-highlight.has-active {
    color: var(--accent-dark);
    background: #dff5ee;
  }

  .bottom-tab-badge {
    position: absolute;
    top: 0;
    right: 18%;
    width: 14px;
    height: 14px;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 0.55rem;
    line-height: 1;
    background: #fff;
    border: 1.5px solid var(--outline);
    border-radius: 50%;
  }

  .icon-btn .btn-label {
    display: none;
  }

  .icon-btn {
    padding: 8px 10px;
    font-size: 1rem;
  }

  .setting-row-nowrap label {
    white-space: nowrap;
  }

  /* The flashcards row packs a category select, a front-side select, and the
     source select into one line — drop the redundant "Category" label (the
     selected option already says "Vocabulary") to free up space. Equal
     flex (1 1 0) used to hand the source select ("🎲 Level words") the same
     width as its much-shorter siblings, which is what forced its text to
     wrap onto two lines — appearance:none lets the select's own text wrap
     like normal inline content once its box gets that tight. Sizing each
     select to its own content instead (flex: 0 1 auto), then letting only
     the source select grow into whatever room is left, keeps every
     select's label on one line, with nowrap/ellipsis as a last-resort
     safety net if a translation still doesn't fit. */
  #view-flashcards .setting-row-nowrap label {
    display: none;
  }

  #view-flashcards .setting-row-nowrap select {
    flex: 0 1 auto;
    min-width: 0;
    padding: 6px 18px 6px 8px;
    background-position: right 5px center;
    background-size: 9px;
    font-size: 0.72rem;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  #view-flashcards .setting-row-nowrap #flash-source {
    flex: 1 1 auto;
  }

  /* Times Table/Typing Game's top row (score, stage, speed stepper, hearts)
     is another setting-row-nowrap that's too tight at this width — the
     score/stage pills were letting flexbox shrink them below their text's
     own width, which (like the flashcards row above) wrapped the text
     inside instead of just staying on one line. Trim their padding/font
     and pin them to nowrap so the whole row fits without any one piece
     breaking onto a second line. */
  #view-typegame .setting-row-nowrap .score-tag,
  #view-timestable .setting-row-nowrap .score-tag {
    padding: 5px 10px;
    font-size: 0.8rem;
    white-space: nowrap;
    flex: 0 0 auto;
  }

  #view-typegame .setting-row-nowrap .stage-tag,
  #view-timestable .setting-row-nowrap .stage-tag {
    font-size: 0.75rem;
    white-space: nowrap;
    flex: 0 0 auto;
  }

  #view-typegame .setting-row-nowrap .typegame-lives,
  #view-timestable .setting-row-nowrap .typegame-lives {
    font-size: 0.8rem;
    letter-spacing: 1px;
    white-space: nowrap;
  }

  .typegame-stage {
    height: 260px;
  }

  .typegame-stage.typegame-stage-expanded {
    height: 460px;
  }

  .typegame-word {
    font-size: 0.85rem;
    padding: 4px 9px;
  }

  .game-speed-control .goal-step-btn {
    width: 24px;
    height: 24px;
    font-size: 0.85rem;
  }

  .game-speed-control .goal-stepper-value {
    font-size: 0.8rem;
    min-width: 1.2em;
  }

  .game-speed-label {
    display: none;
  }
}

.option-btn {
  border: 1px solid var(--outline);
  background: #fff;
  border-radius: 14px;
  padding: 10px;
  font-size: 0.88rem;
  cursor: pointer;
  text-align: left;
  box-shadow: 0 2px 0 var(--outline);
  transition: border-color 0.15s ease, background 0.15s ease, transform 0.08s ease, box-shadow 0.08s ease;
}

.option-btn:hover {
  border-color: var(--primary);
}

.option-btn:active {
  transform: translateY(1px);
  box-shadow: none;
}

.option-btn.correct {
  background: #e4f8ef;
  border-color: var(--success);
  box-shadow: 0 2px 0 var(--success-dark);
}

.option-btn.incorrect {
  background: #fdeaea;
  border-color: var(--error);
  box-shadow: 0 2px 0 var(--error-dark);
}

.option-btn:disabled {
  cursor: default;
}

/* Keyboard play: shows which option arrow keys currently point at, before
   Enter locks in an answer. Suppressed once the question is answered, since
   .correct/.incorrect already carry their own color by then. */
.option-btn-kbd-focus {
  outline: 3px solid var(--primary);
  outline-offset: 2px;
}

.option-btn:disabled.option-btn-kbd-focus {
  outline: none;
}

.quiz-footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-top: 12px;
  flex-wrap: wrap;
  gap: 8px;
}

.progress-bar {
  height: 6px;
  background: #e6e9f4;
  border-radius: 999px;
  overflow: hidden;
  margin-bottom: 12px;
}

.progress-bar-fill {
  height: 100%;
  background: var(--primary);
  transition: width 0.25s ease;
}

/* Milestone tick marks over the quiz's real per-question progress bar — a
   thin vertical line every ~20% so progress reads in visible steps rather
   than as one smooth, hard-to-judge fill. CSS-only (a repeating gradient),
   so it needs no extra markup or JS bookkeeping of question counts. */
#view-quiz .progress-bar {
  position: relative;
  background-image: repeating-linear-gradient(
    to right,
    transparent 0,
    transparent calc(20% - 2px),
    rgba(44, 35, 24, 0.35) calc(20% - 2px),
    rgba(44, 35, 24, 0.35) 20%
  );
}

/* A small scale-bounce played on whatever just got marked correct — the
   right quiz option, a correctly-spelled word, or the flashcard "I know
   this" button — reusing the same remove/reflow/re-add restart trick as
   .score-tag-pulse (see pulseScoreTag()). */
.option-btn-bounce {
  animation: option-bounce 0.35s ease;
}

@keyframes option-bounce {
  0% {
    transform: scale(1);
  }
  45% {
    transform: scale(1.08);
  }
  100% {
    transform: scale(1);
  }
}

/* Was plain bold text easy to miss next to the round's other controls — now
   a real chip, reusing the app's own card language (white surface, a
   flat-color rim rather than a diffuse shadow) instead of inventing a new,
   unrelated floating-pill style. A trophy leads it so it reads as
   "progress" at a glance, not just a random number. */
.score-tag {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: 1rem;
  color: var(--accent-dark);
  background: #fff;
  border: 1px solid #c8f0e2;
  padding: 6px 16px;
  border-radius: 999px;
  box-shadow: 0 2px 0 #c8f0e2;
}

.score-tag::before {
  content: "🏆";
  font-size: 0.95em;
}

.score-tag-pulse {
  animation: score-tag-pulse-anim 0.4s ease;
}

@keyframes score-tag-pulse-anim {
  0% { transform: scale(1); }
  35% { transform: scale(1.18); }
  100% { transform: scale(1); }
}

/* Spelling */
#spelling-word-audio {
  display: flex;
  justify-content: center;
  gap: 8px;
  flex-wrap: wrap;
}

/* Extra breathing room above the Hear the word/Check Answer row, so it
   doesn't sit right under the Number of Questions row above it. */
.spelling-action-row {
  margin-top: 6px;
}

#spelling-input {
  width: 100%;
  padding: 10px;
  font-size: 1.05rem;
  border-radius: 12px;
  /* Matches the site's own background instead of a generic gray-blue —
     see the same treatment on .typegame-input, .wordlist-search and the
     Add Word form fields. Thick (4px) so it actually reads as a border
     against the page's own near-identical background, instead of
     disappearing. */
  border: 4px solid var(--bg);
  text-align: center;
  /* Extra gap above (below the Hear the word row) beyond the plain 10px
     the input used to sit at. */
  margin: 20px 0 10px;
}

#spelling-input:focus {
  outline: none;
  border-color: var(--accent-dark);
}

#spelling-input.correct { border-color: var(--success); background: #e4f8ef; }
#spelling-input.incorrect { border-color: var(--error); background: #fdeaea; }

/* A scrambled tray of the current word's own letters (plus a decoy or two)
   under the input — a lighter hint than reading the word's spelling aloud
   letter-by-letter. Tapping a tile appends it to the input; a tile fades
   once tapped so it's clear which ones have already been used. */
.spelling-hint-tray {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 6px;
  margin: 0 0 10px;
}

.spelling-hint-tile {
  width: 34px;
  height: 34px;
  /* Matches the site's own background instead of the previous primary-blue
     rim, so the tray reads as part of the page's own frame — same
     treatment as #spelling-input's border above, at 3px (rather than
     4px) since these tiles are much smaller. */
  border: 3px solid var(--bg);
  border-radius: 10px;
  background: #fff;
  color: var(--accent-dark);
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: 0.95rem;
  text-transform: uppercase;
  cursor: pointer;
  box-shadow: 0 3px 0 var(--bg);
  transition: transform 0.08s ease, box-shadow 0.08s ease, background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}

.spelling-hint-tile:hover {
  transform: translateY(-1px);
}

.spelling-hint-tile:active {
  transform: translateY(2px);
  box-shadow: 0 1px 0 var(--bg);
}

/* A tapped tile flips to the Login button's own mint fill and gives a
   little wobble, so using a hint reads as a small win in the moment. Its
   border also switches to the Login button's exact 1px/2px thickness
   instead of the base tile's thicker 3px background-matching rim. It stays
   clickable (cursor: pointer, not disabled) — tapping it again undoes that
   one letter and turns the tile back off, in case of a mis-tap. */
.spelling-hint-tile-used {
  background: var(--accent);
  border-width: 1px;
  border-color: var(--accent-dark);
  color: #fff;
  box-shadow: 0 2px 0 var(--accent-dark);
  cursor: pointer;
  animation: spelling-hint-used-wobble 0.45s ease forwards;
}

@keyframes spelling-hint-used-wobble {
  0% {
    transform: scale(1) rotate(0deg);
  }
  35% {
    transform: scale(1.22) rotate(-10deg);
  }
  65% {
    transform: scale(0.94) rotate(7deg);
  }
  100% {
    transform: scale(1) rotate(0deg) translateY(2px);
  }
}

.spelling-tip {
  color: var(--muted);
  text-align: center;
  font-size: 0.85rem;
  min-height: 18px;
}

/* Calls out the "Incomplete words first" auto-selection message so it
   doesn't blend in with the other, more routine status text above the
   grid. */
.spelling-tip.status-highlight {
  color: var(--accent);
  font-weight: 700;
}

/* Shared between the before-start screen and the after-start practice
   panel so the card reads as one consistent "stage" across that
   transition, instead of a bordered box appearing only once you've
   started. */
.spelling-stage-box {
  border: 2px solid var(--outline);
  border-radius: var(--radius);
  background: #fbfffd;
  margin: 6px 0 14px;
  padding: 18px 14px;
}

#spelling-start-screen {
  text-align: center;
  padding: 30px 10px;
}

.spelling-wrong-word {
  color: var(--error);
  font-weight: 700;
  font-size: 1.25rem;
}

.spelling-wrong-prompt {
  color: var(--error);
  font-size: 0.82rem;
  margin-top: 2px;
}

.spelling-meaning-line {
  color: var(--text);
  font-size: 0.85rem;
  margin-top: 6px;
}

.spelling-correct-line {
  color: var(--success);
  font-weight: 700;
  font-size: 0.95rem;
}

/* Word list */
.wordlist-filters {
  display: flex;
  gap: 8px;
  align-items: center;
  margin-bottom: 8px;
}

.wordlist-search {
  flex: 1 1 auto;
  min-width: 0;
  padding: 6px 12px;
  border-radius: 999px;
  border: 3px solid var(--bg);
  font-size: 0.9rem;
}

.wordlist-search:focus {
  outline: none;
  border-color: var(--accent-dark);
}

.admin-panel-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin: 10px 0 16px;
}

.admin-panel-search-inline {
  flex: 1 1 0;
  /* About a third of the row on a normal-width screen — grows a little on
     very narrow ones instead of shrinking to nothing. */
  min-width: 110px;
  max-width: 33%;
}

.admin-panel-sort-group {
  display: flex;
  align-items: center;
  gap: 8px;
  /* Pushes this group (and only this group) to the row's right edge,
     regardless of how much room the search box / generate button to its
     left is taking up. */
  margin-left: auto;
}

.level-filter-row {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-start;
  gap: 8px;
  margin: 4px 0 14px;
}

/* Holds the combined sort/level filter dropdown on the left and a half-width
   search box pinned to the right, on the row where the old level-filter
   buttons used to sit. */
.custom-controls-row {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 4px 0 14px;
}

.custom-search-half {
  flex: 0 0 50%;
  width: 50%;
  margin-left: auto;
}

.filter-dropdown {
  position: relative;
}

/* Styled to read as a dropdown trigger (select-like border/background) with
   a chevron that flips to show open/closed state, rather than a plain pill
   button that gives no hint it opens something. */
.filter-dropdown-toggle {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  background: #fff;
  color: var(--text);
  border: 3px solid var(--bg);
  border-radius: 999px;
  padding: 8px 12px;
  font-size: 0.8rem;
  font-weight: 600;
  cursor: pointer;
}

.filter-dropdown-toggle:hover,
.filter-dropdown-toggle.open {
  border-color: var(--accent-dark);
}

.filter-dropdown-chevron {
  font-size: 0.7rem;
  color: var(--muted);
  transition: transform 0.15s ease;
}

.filter-dropdown-toggle.open .filter-dropdown-chevron {
  transform: rotate(180deg);
}

.filter-dropdown-panel {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  z-index: 6;
  min-width: 220px;
  background: var(--card-bg);
  border-radius: 12px;
  box-shadow: var(--shadow);
  padding: 10px 12px;
  border: 1px solid #e6e9f4;
}

.filter-dropdown-panel[hidden] {
  display: none;
}

.filter-dropdown-group {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.filter-dropdown-group label {
  display: flex;
  align-items: center;
  gap: 7px;
  font-size: 0.82rem;
  cursor: pointer;
}

.filter-dropdown-group input[type="checkbox"] {
  width: 15px;
  height: 15px;
  flex: 0 0 auto;
  cursor: pointer;
}

.filter-dropdown-divider {
  border: none;
  border-top: 1px solid #eef0f6;
  margin: 8px 0;
}

@media (max-width: 480px) {
  .custom-search-half {
    max-width: none;
    /* Reclaims the row to itself once Sort & Filter/Change level have
       wrapped above it, instead of staying pinned to a 50%-width column
       with empty space beside it. */
    flex-basis: 100%;
    width: 100%;
    margin-left: 0;
  }

  .custom-controls-row {
    flex-wrap: wrap;
  }

  /* My added words' Select All/Delete/Word Management/Export row wraps at
     this width with each control keeping its own natural (very different)
     width, which reads as a messy, unaligned jumble rather than a toolbar.
     A fixed two-column grid gives every control the same cell width so
     they line up cleanly instead. */
  .my-added-words-header-actions {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 6px;
  }

  .my-added-words-header-actions > * {
    width: 100%;
    box-sizing: border-box;
    justify-content: center;
    text-align: center;
    margin: 0;
  }
}

@media (max-width: 480px) {
  .admin-panel-search-inline {
    max-width: none;
  }
}

.wordlist-level-select {
  flex: 0 1 auto;
  min-width: 0;
  border-radius: 999px;
  padding: 8px 10px;
  font-size: 0.8rem;
}

.goal-stepper {
  display: flex;
  align-items: center;
  gap: 6px;
  flex: 0 1 auto;
}

/* Centers the label+stepper in the Quiz setting row, in the space left after
   the category select and the right-pinned New Quiz button. */
#quiz-goal-stepper {
  margin: 0 auto;
}

/* Extra breathing room in the Quiz tab: roughly a line's gap on either side
   of the progress bar, so the setting row / progress bar / question don't
   feel cramped together. */
#view-quiz .setting-row {
  margin-bottom: 30px;
}

#view-quiz .progress-bar {
  margin-bottom: 30px;
}

#view-quiz #quiz-restart {
  padding: 6px 12px;
  font-size: 0.72rem;
  font-weight: 600;
  line-height: 1;
}

/* The New Quiz button's icon used to be a plain 🔄 emoji glyph — its own
   fixed, multi-color rendering that never matched the button's own accent
   fill and read as small/muddy on some platforms. A currentColor SVG inside
   a small circular badge instead always matches the button's text color and
   stays crisp at any size. The badge is sized close to the text's own
   line-height (rather than a fixed, noticeably larger circle) so the button
   doesn't grow taller than its neighbors in the same setting row. */
.new-quiz-btn {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  line-height: 1;
}

.new-quiz-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 15px;
  height: 15px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.3);
  flex: 0 0 auto;
}

.new-quiz-icon svg {
  width: 9px;
  height: 9px;
  color: #fff;
}

.new-quiz-btn:active .new-quiz-icon svg {
  transform: rotate(-40deg);
  transition: transform 0.2s ease;
}

/* The falling-game speed control, sitting between the stage tag and the
   lives display in the score row. Auto margins on both sides center it in
   whatever space is left after the lives display's own margin-left:auto
   claims the right edge — a live control, unlike the pre-game max-table
   stepper it otherwise matches visually. */
.game-speed-control {
  display: flex;
  align-items: center;
  gap: 6px;
  flex: 0 0 auto;
  margin: 0 auto;
}

.game-speed-label {
  font-family: var(--font-heading);
  font-weight: 700;
  color: var(--text);
  font-size: 0.75rem;
  white-space: nowrap;
}


.goal-stepper-label {
  font-family: var(--font-heading);
  font-weight: 700;
  color: var(--text);
  font-size: 0.85rem;
  white-space: nowrap;
}

/* Matches the plain nav-button look (white fill, dark outline, dark text)
   instead of a filled blue circle, so it reads as "just another menu
   control" rather than a separate, differently-themed widget. */
.goal-step-btn {
  border: 1px solid var(--outline);
  border-radius: 999px;
  width: 30px;
  height: 30px;
  flex: 0 0 auto;
  background: #fff;
  color: var(--text);
  font-size: 1.05rem;
  font-weight: 700;
  line-height: 1;
  cursor: pointer;
  box-shadow: 0 2px 0 var(--outline);
  transition: transform 0.1s ease, box-shadow 0.1s ease;
}

.goal-step-btn:active {
  transform: translateY(1px) scale(0.96);
  box-shadow: none;
}

.goal-step-btn:disabled {
  opacity: 0.4;
  cursor: default;
  transform: none;
  box-shadow: 0 2px 0 var(--outline);
}

.goal-stepper-value {
  min-width: 1.6em;
  text-align: center;
  font-weight: 700;
  font-size: 0.9rem;
}

/* The "Number of Questions" steppers (Quiz/Spelling) get an extra playful
   treatment — a little target chip around the number, rather than the plain
   bare digit the other steppers (game speed, times-table ceiling) still use;
   those didn't come up as looking dull, so they keep the plainer number and
   just inherit the friendlier step buttons above. */
.goal-stepper-value-wrap {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  background: #e6f9f2;
  border: 2px solid var(--accent);
  border-radius: 999px;
  padding: 3px 10px 3px 6px;
}

.goal-stepper-icon {
  font-size: 0.95rem;
  line-height: 1;
}

.goal-stepper-fun .goal-stepper-value {
  color: var(--accent-dark);
  font-size: 1rem;
}

/* The goal-reached banner used to be a plain green box of bold text — this
   gives it a little mascot (a bouncing party emoji, since it needs no new
   artwork and still reads as an original, generic celebration rather than
   any specific character) plus a confetti-pop entrance, so hitting a goal
   feels like a small event rather than a status update. */
.goal-banner {
  background: linear-gradient(135deg, #eaf9f3, #e4f8ef);
  border: 3px solid var(--success-dark);
  border-radius: 20px;
  padding: 16px 14px 14px;
  margin-bottom: 14px;
  text-align: center;
  box-shadow: 0 4px 0 var(--success-dark);
  animation: goal-banner-pop 0.35s cubic-bezier(0.34, 1.56, 0.64, 1);
}

.goal-banner::before {
  content: "🎉";
  display: block;
  font-size: 2.1rem;
  line-height: 1;
  margin-bottom: 4px;
  animation: goal-banner-bounce 1s ease-in-out infinite;
}

@keyframes goal-banner-pop {
  0% {
    transform: scale(0.85);
    opacity: 0;
  }
  100% {
    transform: scale(1);
    opacity: 1;
  }
}

@keyframes goal-banner-bounce {
  0%, 100% {
    transform: translateY(0) rotate(0deg);
  }
  50% {
    transform: translateY(-6px) rotate(-8deg);
  }
}

.goal-banner[hidden] {
  display: none;
}

.goal-message {
  margin: 0 0 10px;
  font-weight: 700;
  color: #1c7a55;
  font-size: 1rem;
}

.goal-actions {
  display: flex;
  gap: 8px;
  justify-content: center;
  flex-wrap: wrap;
}

/* A small, reusable "good news" card — a friendly mascot plus a short line —
   for empty/success states that used to be a single flat sentence (e.g. the
   spelling report's "no mistakes today"). Anywhere else in the app that
   shows a similar one-line congratulations can reuse this instead of adding
   another plain <p class="muted">. */
.celebration-card {
  text-align: center;
  background: linear-gradient(135deg, #eaf9f3, #dff5ee);
  border: 3px solid var(--accent-dark);
  border-radius: 18px;
  padding: 16px 14px;
  margin: 10px 0;
}

.celebration-card-icon {
  display: block;
  font-size: 2.4rem;
  line-height: 1;
  margin-bottom: 6px;
  animation: goal-banner-bounce 1.4s ease-in-out infinite;
}

.celebration-card-text {
  margin: 0;
  font-weight: 700;
  color: var(--accent-dark);
  font-size: 0.95rem;
}

.wordlist-count {
  margin: 0 0 8px;
  font-size: 0.75rem;
}

#custom-words-count {
  font-size: 0.9rem;
  font-weight: 700;
  color: var(--text);
}

.wordlist-grid {
  display: grid;
  gap: 8px;
  max-height: 46vh;
  overflow-y: auto;
  padding-right: 2px;
}

.wordlist-item {
  background: #fff;
  border: 1px solid var(--outline);
  border-radius: 14px;
  padding: 9px 12px;
  box-shadow: 0 2px 0 var(--outline);
  display: flex;
  justify-content: space-between;
  /* flex-start rather than center: a row whose definition wraps onto a
     second line should keep its text (and the buttons beside it) pinned to
     the top-left instead of drifting to the row's vertical centre, which is
     what made rows with longer text look out of line with short ones. */
  align-items: flex-start;
  gap: 8px;
}

.wordlist-item-main {
  /* The real bug behind "some rows look centered, some left-aligned": this
     block sat between the checkbox and the badges with no explicit width,
     so justify-content: space-between on .wordlist-item centered it in
     whatever room was left over whenever its own content was narrower than
     that space, instead of pinning it flush to the checkbox. flex: 1 makes
     it claim all the remaining width every time, so its own text — which
     is left-aligned by default — always starts in the same place.
     min-width: 0 lets it actually shrink below its content's natural width
     so long words/definitions wrap instead of overflowing the row. */
  flex: 1;
  min-width: 0;
}

.wordlist-item .w {
  font-weight: 700;
  font-size: 0.92rem;
}

.wordlist-item .d {
  color: var(--muted);
  font-size: 0.8rem;
  /* Some fetched definitions run to 4+ lines while most are 1 — clamping
     keeps row heights from swinging wildly and looking jagged next to each
     other; the full text is still there in the title tooltip and via Edit. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.admin-pending-badge {
  color: #0a7d63;
  font-weight: 700;
}

.wordlist-item .admin-code-text,
.admin-code-text {
  font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
  font-size: 1rem;
  letter-spacing: 0.5px;
}

#upgrade-request-fulfilled-code {
  font-weight: 700;
  background: var(--bg);
  border-radius: 8px;
  padding: 8px 12px;
  text-align: center;
}

.wordlist-item .mastery {
  font-size: 0.72rem;
  padding: 3px 9px;
  border-radius: 999px;
  background: #eef2fb;
  color: var(--primary-dark);
  white-space: nowrap;
}

.wordlist-item .mastery.high { background: #e4f8ef; color: var(--success); }
.wordlist-item .mastery.low { background: #fdeaea; color: var(--error); }

/* Stats */
.stats-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
  gap: 8px;
  margin-bottom: 12px;
}

.stat-box {
  background: #fff;
  border: 1px solid var(--outline);
  border-radius: 14px;
  padding: 10px;
  text-align: center;
  box-shadow: 0 2px 0 var(--outline);
}

.stat-box .num {
  font-family: var(--font-heading);
  font-size: 1.3rem;
  font-weight: 700;
  color: var(--accent-dark);
}

.stat-box .lbl {
  color: var(--muted);
  font-size: 0.75rem;
}

.reset-btn {
  display: block;
  margin: 0 auto;
  background: none;
  border: 1px solid var(--error);
  color: var(--error);
  border-radius: 999px;
  padding: 7px 16px;
  cursor: pointer;
  font-size: 0.85rem;
}

footer {
  text-align: center;
  color: var(--muted);
  font-size: 0.7rem;
  padding: 14px 16px;
}

/* Level overlay */
.level-overlay {
  position: fixed;
  inset: 0;
  background: rgba(20, 28, 55, 0.55);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
  padding: 16px;
}

.level-overlay[hidden] {
  display: none;
}

/* Account overlay (log in / sign up) */
.auth-overlay {
  position: fixed;
  inset: 0;
  background: rgba(20, 28, 55, 0.55);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1100;
  padding: 16px;
}

.auth-overlay[hidden] {
  display: none;
}

/* Kid-friendly stand-in for window.confirm() — see #kid-confirm-overlay's
   comment in index.html for why a real confirm() doesn't work here. Sits
   above the auth/upgrade overlays since it's usually what leads into them. */
.kid-modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(20, 28, 55, 0.6);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1200;
  padding: 16px;
}

.kid-modal-overlay[hidden] {
  display: none;
}

.kid-modal-card {
  background: #fff;
  border: 4px solid var(--accent);
  border-radius: 28px;
  padding: 26px 22px;
  max-width: 340px;
  width: 100%;
  text-align: center;
  box-shadow: 0 20px 50px rgba(0, 0, 0, 0.3);
  animation: kid-modal-pop 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
}

@keyframes kid-modal-pop {
  0% { transform: scale(0.55) rotate(-5deg); opacity: 0; }
  65% { transform: scale(1.06) rotate(2deg); opacity: 1; }
  100% { transform: scale(1) rotate(0deg); }
}

.kid-modal-icon {
  font-size: 2.6rem;
  line-height: 1;
  animation: kid-modal-bounce 1.3s ease-in-out infinite;
}

@keyframes kid-modal-bounce {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-8px); }
}

.kid-modal-brand {
  font-family: "Fredoka", "Gaegu", "Segoe UI", sans-serif;
  font-weight: 700;
  color: var(--primary-dark);
  font-size: 1rem;
  margin: 8px 0 14px;
}

.kid-modal-message {
  /* No Gaegu here — its Hangul glyphs are drawn much wider-set than the
     Latin fallback, which read as excessively loose tracking on a full
     sentence. Fredoka still covers the Latin case; Korean falls through to
     the system UI font, which is tighter and more legible at paragraph
     length. */
  font-family: "Fredoka", "Segoe UI", sans-serif;
  font-weight: 500;
  font-size: 1.3rem;
  line-height: 1.5;
  letter-spacing: -0.02em;
  color: var(--text);
  margin: 0 0 20px;
}

.kid-modal-actions {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

button.kid-modal-btn {
  font-family: "Fredoka", "Segoe UI", sans-serif;
  font-size: 1.25rem;
  letter-spacing: -0.02em;
  padding: 12px;
  width: 100%;
}

/* English labels ("Sign me up!", "Not now") read as too cramped at the
   Korean-tuned -0.02em tracking; only Korean needs it pulled in. */
html[lang="en"] button.kid-modal-btn {
  letter-spacing: 0.02em;
}

.auth-card {
  background: #fff;
  border: 3px solid var(--outline);
  border-radius: var(--radius);
  padding: 20px 18px;
  max-width: 320px;
  width: 100%;
  box-shadow: 0 20px 50px rgba(0, 0, 0, 0.25);
}

.auth-card h2 {
  margin: 0 0 14px;
  font-size: 1.1rem;
  text-align: center;
}

.auth-card .add-mode-toggle {
  margin-bottom: 6px;
}

.auth-form label {
  display: block;
  font-size: 0.8rem;
  font-weight: 600;
  margin: 10px 0 4px;
}

.auth-form input {
  width: 100%;
  padding: 8px 10px;
  border: 2px solid #e0e5f2;
  border-radius: 10px;
  font-size: 0.9rem;
}

.auth-form input:focus {
  outline: none;
  border-color: var(--accent-dark);
}

/* Only for forms sitting in a full-width card (like My Account), not the
   narrow ~320px login/signup overlay where 100% width is already right. */
.auth-form-input-narrow {
  max-width: 33%;
  min-width: 160px;
}

@media (max-width: 480px) {
  .auth-form-input-narrow {
    max-width: none;
  }
}

.auth-hint {
  color: var(--muted);
  font-size: 0.72rem;
  margin: 3px 0 0;
}

.auth-error {
  color: var(--error);
  font-size: 0.8rem;
  margin: 10px 0 0;
}

.auth-error[hidden] {
  display: none;
}

.auth-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  margin-top: 16px;
}

.level-overlay-card {
  position: relative;
  background: #fff;
  border: 1px solid var(--outline);
  border-radius: var(--radius);
  padding: 20px 18px;
  max-width: 380px;
  width: 100%;
  text-align: center;
  box-shadow: 0 20px 50px rgba(0, 0, 0, 0.25);
}

/* Only shown when the level-select overlay was opened from the level badge
   (i.e. a level is already set) — the very first, mandatory pick on a
   fresh visit has no way out of it, so that state hides this button
   entirely rather than offering a close that would leave no level chosen. */
.level-overlay-close {
  position: absolute;
  top: 10px;
  right: 10px;
  width: 28px;
  height: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--outline);
  border-radius: 999px;
  background: #fff;
  color: var(--text);
  font-size: 0.85rem;
  line-height: 1;
  cursor: pointer;
  box-shadow: 0 2px 0 var(--outline);
}

.level-overlay-close:active {
  transform: translateY(1px);
  box-shadow: none;
}

.level-overlay-close[hidden] {
  display: none;
}

.level-overlay-card h2 {
  margin: 0 0 6px;
  font-family: var(--font-heading);
  font-size: 1.15rem;
}

.level-overlay-card p {
  color: var(--muted);
  margin: 0 0 14px;
  font-size: 0.85rem;
}

.level-choices {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.level-choice {
  border: 1px solid var(--outline);
  background: #fff;
  border-radius: 14px;
  padding: 10px 12px;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  box-shadow: 0 2px 0 var(--outline);
  transition: background 0.15s ease, border-color 0.15s ease, transform 0.1s ease, box-shadow 0.1s ease, color 0.15s ease;
}

/* Same mint fill/rim as the Login button on hover, not just a border tint. */
.level-choice:hover {
  background: var(--accent);
  border-color: var(--accent-dark);
  box-shadow: 0 2px 0 var(--accent-dark);
  transform: translateY(-1px);
}

.level-choice:hover .lv-title,
.level-choice:hover .lv-desc {
  color: #fff;
}

.level-choice .lv-title {
  font-weight: 700;
  font-size: 0.95rem;
}

.level-choice .lv-desc {
  color: var(--muted);
  font-size: 0.78rem;
}

/* Bottom-sheet treatment for the app's true modals (log in/sign up + the
   upgrade prompt, which both reuse .auth-overlay/.auth-card; the kid-friendly
   confirm; and the level picker) on phones — a sheet anchored to the bottom
   edge is easier to reach one-handed and feels native, versus a small card
   floating in the middle of a cramped screen. Desktop keeps the original
   centered-card layout untouched. */
@media (max-width: 480px) {
  .auth-overlay,
  .kid-modal-overlay,
  .level-overlay {
    align-items: flex-end;
    padding: 0;
  }

  .auth-overlay .auth-card,
  .kid-modal-overlay .kid-modal-card,
  .level-overlay .level-overlay-card {
    max-width: none;
    width: 100%;
    border-radius: 22px 22px 0 0;
    border-width: 3px 0 0;
    padding-bottom: max(20px, env(safe-area-inset-bottom));
    max-height: 85vh;
    overflow-y: auto;
    animation: sheet-slide-up 0.28s cubic-bezier(0.2, 0.9, 0.3, 1);
  }

  /* The kid-modal's playful pop-in doesn't fit a sheet sliding up from the
     edge, so it's swapped for the same slide-up as the other two on mobile. */
  .kid-modal-overlay .kid-modal-card {
    border: none;
    border-top: 4px solid var(--accent);
  }
}

@keyframes sheet-slide-up {
  from {
    transform: translateY(100%);
  }
  to {
    transform: translateY(0);
  }
}

.stat-bar {
  margin-top: 6px;
  display: inline-flex;
  align-items: center;
  gap: 8px;
}

.stat-chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  border: 1px solid var(--accent-dark);
  background: var(--accent);
  color: #fff;
  font-family: var(--font-heading);
  font-weight: 700;
  padding: 4px 11px;
  font-size: 0.78rem;
  border-radius: 999px;
  box-shadow: 0 2px 0 var(--accent-dark);
}

.stat-chip-icon {
  font-size: 0.95em;
}

.stat-chip.stat-chip-pulse {
  animation: stat-chip-pop 0.4s ease;
}

@keyframes stat-chip-pop {
  0% {
    transform: scale(1);
  }
  40% {
    transform: scale(1.25);
  }
  100% {
    transform: scale(1);
  }
}

.level-badge {
  margin-top: 0;
  border: 1px solid var(--accent-dark);
  background: var(--card-bg);
  color: var(--accent-dark);
  font-family: var(--font-heading);
  font-weight: 700;
  padding: 4px 13px;
  font-size: 0.78rem;
  border-radius: 999px;
  cursor: pointer;
  box-shadow: 0 2px 0 var(--accent-dark);
}

.level-badge:active {
  transform: translateY(1px);
  box-shadow: none;
}

/* Speakable lines (click to hear via speech synthesis) */
.speakable-line {
  cursor: pointer;
  position: relative;
}

.speakable-line:hover {
  text-decoration: underline dotted;
}

.speakable-line::after {
  content: " 🔊";
  opacity: 0.6;
  font-size: 0.8em;
}

/* Add Word form */
.form-row {
  margin-bottom: 10px;
  display: flex;
  flex-direction: column;
  gap: 4px;
  text-align: left;
}

.form-row label {
  font-weight: 600;
  color: var(--muted);
  font-size: 0.82rem;
}

.form-row input,
.form-row select,
.form-row textarea {
  padding: 6px 8px;
  border-radius: 10px;
  border: 3px solid var(--bg);
  font-size: 0.9rem;
  width: 100%;
}

.form-row input:focus,
.form-row select:focus,
.form-row textarea:focus {
  outline: none;
  border-color: var(--accent-dark);
}

.form-row textarea {
  font-family: inherit;
  resize: vertical;
  min-height: 90px;
}

.add-mode-toggle {
  display: flex;
  gap: 6px;
  margin-bottom: 14px;
}

.add-mode-toggle button {
  font-size: 0.8rem;
  padding: 7px 14px;
}

.form-actions {
  display: flex;
  gap: 8px;
}

.muted {
  color: var(--muted);
  font-size: 0.82rem;
}

.file-picker-row {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 12px 0;
  flex-wrap: wrap;
}

.file-picker-name {
  color: var(--muted);
  font-size: 0.82rem;
  overflow-wrap: anywhere;
}


.ocr-progress {
  margin-top: 14px;
}

.ocr-review {
  margin-top: 18px;
}

.ocr-review-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  flex-wrap: wrap;
}

.ocr-review-header p {
  margin: 0;
  flex: 1 1 auto;
}

.ocr-review-header button {
  flex: 0 0 auto;
  font-size: 0.78rem;
  padding: 6px 12px;
}

.candidate-chip {
  cursor: pointer;
  user-select: none;
}

.candidate-chip.selected {
  border-color: var(--primary);
  background: #eef2fb;
}

.wordlist-item .edit-btn,
.wordlist-item .delete-btn,
.wordlist-item .retry-btn {
  border: none;
  background: #eef2fb;
  color: var(--primary-dark);
  border-radius: 999px;
  padding: 4px 10px;
  font-size: 0.8rem;
  cursor: pointer;
  white-space: nowrap;
}

.wordlist-item .delete-btn {
  background: #fdeaea;
  color: var(--error);
}

.wordlist-item .retry-btn {
  background: #e2f7ef;
  color: #0f8a6e;
}

.wordlist-item .retry-btn:disabled,
.failed-words-banner button:disabled {
  opacity: 0.6;
  cursor: default;
}

.failed-words-banner {
  background: #eaf9f3;
  border: 1px solid #a8ddc9;
  border-radius: 12px;
  padding: 10px 12px;
  margin-bottom: 10px;
}

.failed-words-banner[hidden] {
  display: none;
}

.failed-words-banner p {
  color: #0d7a61;
}

.failed-actions {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
}
