:root {
  --bg: #eef2f9;
  --bg-grad-1: #f5f7fb;
  --bg-grad-2: #e6edf8;
  --surface: #ffffff;
  --surface-alt: #f1f5fc;
  --border: #dce5f2;
  --text: #172033;
  --muted: #667085;
  --primary: #2f6fed;
  --primary-dark: #214fbb;
  --success: #1f9d6a;
  --warning: #e59a25;
  --danger: #d94a4a;
  --purple: #8b5cf6;
  --cyan: #0ea5a3;
  --radius: 16px;
  --shadow: 0 6px 22px rgba(23, 32, 51, 0.08);
  --shadow-lg: 0 12px 40px rgba(23, 32, 51, 0.16);
  --safe-top: env(safe-area-inset-top, 0px);
  --safe-bottom: env(safe-area-inset-bottom, 0px);
  --safe-left: env(safe-area-inset-left, 0px);
  --safe-right: env(safe-area-inset-right, 0px);

  /* Derived surfaces — every one of these is re-declared for the dark scheme
     below, so a new rule should use a variable rather than a literal colour. */
  --divider: #eef2f8;
  --track: #e7edf6;
  --outline: #cfd9ea;
  --placeholder: #9aa6b8;
  --text-soft: #2c3a54;
  --on-primary: #ffffff;
  --primary-soft: #e7eefc;
  --success-soft: #e6f6ee;
  --success-ink: #0f7a4f;
  --danger-soft: #fdecec;
  --danger-border: #f3b1b1;
  --toast-bg: #172033;
  --toast-ink: #ffffff;

  /* Activity calendar: an empty day plus four levels of "busier". */
  --heat-0: #e7edf6;
  --heat-1: #c6e8d6;
  --heat-2: #8ed6ac;
  --heat-3: #4bb37d;
  --heat-4: #1f7a52;

  /* Rank map leagues: a material ladder (wood → bronze → steel → silver → gold),
     matching the rank-badge frames. Two tones each for the emblem, ring and
     background tint; `-ink` is the readable label colour. Steel is blued (darker,
     bluer) so it reads apart from the lighter Silver above it. */
  --lg-wood-1: #a97f45; --lg-wood-2: #6f4f24; --lg-wood-ink: #6b4c22;
  --lg-bronze-1: #c06a33; --lg-bronze-2: #8a441a; --lg-bronze-ink: #8a441a;
  --lg-steel-1: #5d7593; --lg-steel-2: #3c4f6e; --lg-steel-ink: #45597b;
  --lg-silver-1: #a4afbe; --lg-silver-2: #737f8e; --lg-silver-ink: #5f6b7a;
  --lg-gold-1: #d9a838; --lg-gold-2: #9a6f1e; --lg-gold-ink: #8a6414;
}

/* Dark scheme. Applied by data-theme on <html> (see js/theme.js), which also
   honours the OS preference until the user picks a side explicitly. */
:root[data-theme="dark"] {
  --bg: #10141c;
  --bg-grad-1: #131924;
  --bg-grad-2: #0d1117;
  --surface: #1a2130;
  --surface-alt: #212a3b;
  --border: #2c3648;
  --text: #e6ecf6;
  --muted: #93a1b8;
  --primary: #5b8dfb;
  --primary-dark: #3f6fd8;
  --success: #34c48a;
  --warning: #e8ad4a;
  --danger: #f06a6a;
  --purple: #a78bfa;
  --cyan: #2bc4bd;
  --shadow: 0 6px 22px rgba(0, 0, 0, 0.45);
  --shadow-lg: 0 12px 40px rgba(0, 0, 0, 0.6);

  --divider: #29334a;
  --track: #2a3448;
  --outline: #3b4760;
  --placeholder: #6b7a93;
  --text-soft: #c3cddf;
  --on-primary: #0d1117;
  --primary-soft: #1e2f52;
  --success-soft: #16352a;
  --success-ink: #6ee7b7;
  --danger-soft: #3a1f22;
  --danger-border: #6b3336;
  --toast-bg: #e6ecf6;
  --toast-ink: #10141c;

  --heat-0: #273147;
  --heat-1: #14432e;
  --heat-2: #1d6b48;
  --heat-3: #2f9f6d;
  --heat-4: #4fd39a;

  --lg-wood-1: #c19a5e; --lg-wood-2: #7c5a2e; --lg-wood-ink: #d8b483;
  --lg-bronze-1: #d07d40; --lg-bronze-2: #8f4c1f; --lg-bronze-ink: #eaa871;
  --lg-steel-1: #7089ab; --lg-steel-2: #445b7c; --lg-steel-ink: #a9bcd6;
  --lg-silver-1: #b7c1cf; --lg-silver-2: #7f8b99; --lg-silver-ink: #cdd6e2;
  --lg-gold-1: #e6b74a; --lg-gold-2: #a97e26; --lg-gold-ink: #efce80;
}

/* The dropdown arrow is baked into a data: URI, so it needs its own dark copy. */
:root[data-theme="dark"] select {
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'><path fill='none' stroke='%2393a1b8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' d='M1 1.5l5 5 5-5'/></svg>");
}

/* Rule diagrams are SVG files drawn in dark ink on transparency — they cannot
   read a CSS variable through an <img>. Rather than ship two copies of every
   diagram, dark mode puts them on a light panel, like a figure on paper. */
:root[data-theme="dark"] .rule-diagram {
  background: #f4f7fc;
  border-radius: 12px;
}

* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }

/* Images are art, not saveable assets: no long-press "Save/Copy image" callout
   (which also let a curious user lift every rank badge), no drag-off, no
   right-click menu, no accidental text-style selection. */
img {
  -webkit-touch-callout: none;
  -webkit-user-drag: none;
  user-select: none;
  -webkit-user-select: none;
}

html {
  /* The canvas colour: this is what shows behind the notch, under the home bar
     and when the page is over-scrolled. A gradient alone does not paint there. */
  background-color: var(--bg);
  color-scheme: light;
}
:root[data-theme="dark"] { color-scheme: dark; }

html, body {
  margin: 0;
  padding: 0;
  min-height: 100%;
  background: linear-gradient(160deg, var(--bg-grad-1), var(--bg-grad-2));
  background-attachment: fixed;
  color: var(--text);
  font-family: -apple-system, "SF Pro Text", "Segoe UI Variable", "Segoe UI",
    system-ui, Roboto, Arial, sans-serif;
  font-size: 16px;
  -webkit-font-smoothing: antialiased;
}

body { overscroll-behavior-y: none; }

#app {
  max-width: 1120px;
  margin: 0 auto;
  padding: calc(12px + var(--safe-top)) calc(14px + var(--safe-right))
    calc(20px + var(--safe-bottom)) calc(14px + var(--safe-left));
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  gap: 14px;
}

/* ---------- generic ---------- */
.hidden { display: none !important; }
.muted { color: var(--muted); }
.center { text-align: center; }
.row { display: flex; align-items: center; gap: 10px; }
.row.wrap { flex-wrap: wrap; }
.spacer { flex: 1; }
h1, h2, h3 { margin: 0; }

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 16px;
}

button {
  font-family: inherit;
  font-size: 16px;
  font-weight: 650;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 12px 16px;
  cursor: pointer;
  transition: transform 0.06s ease, background 0.15s ease, border-color 0.15s ease;
  min-height: 48px;
  user-select: none;
}
button:active { transform: scale(0.97); }
button:disabled { opacity: 0.5; cursor: default; }

.btn-primary {
  background: var(--primary);
  border-color: var(--primary);
  color: #fff;
}
.btn-primary:active { background: var(--primary-dark); }
.btn-danger { background: var(--danger-soft); border-color: var(--danger-border); color: var(--danger); }
.btn-ghost { background: transparent; border-color: transparent; color: var(--muted); }
.btn-block { width: 100%; }

input, select {
  font-family: inherit;
  font-size: 16px;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 12px 14px;
  width: 100%;
  min-height: 48px;
}
select {
  appearance: none;
  -webkit-appearance: none;
  -moz-appearance: none;
  padding-right: 40px;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'><path fill='none' stroke='%23667085' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' d='M1 1.5l5 5 5-5'/></svg>");
  background-repeat: no-repeat;
  background-position: right 14px center;
  background-size: 12px 8px;
  cursor: pointer;
  text-overflow: ellipsis;
}
input[type="number"] { -moz-appearance: textfield; }
input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
input:focus, select:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px rgba(47, 111, 237, 0.12); }
input::placeholder { color: var(--placeholder); }
label.field { display: flex; flex-direction: column; gap: 6px; font-size: 13px; color: var(--muted); font-weight: 600; }

/* ---------- header ---------- */
.appbar {
  display: flex;
  flex-direction: column;
  gap: 8px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 10px 14px;
  position: sticky;
  top: calc(6px + var(--safe-top));
  z-index: 20;
}
.appbar-row { display: flex; align-items: center; gap: 12px; min-width: 0; }
.appbar-row.bottom { gap: 14px; }
.appbar-actions { display: flex; gap: 8px; margin-left: auto; flex: none; }
.rank-badge {
  width: 60px; height: 60px;
  border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  color: #fff; font-weight: 900; font-size: 22px;
  overflow: hidden; flex: none;
  box-shadow: inset 0 0 0 2px rgba(255,255,255,0.4);
}
/* Once the art has loaded the disc, its highlight ring and the round mask all
   come off (the colour itself is cleared inline, where it was set): the badge is
   painted with its own metal rim, and a white ring drawn over that only muddies
   it. The mask has to go with them — a badge is only round in the middle, and a
   circle the width of the frame cuts the wingtips and laurels off the ones that
   spread past it. */
.rank-badge.has-art { box-shadow: none; border-radius: 0; }
.rank-badge img { width: 100%; height: 100%; object-fit: contain; }
.appbar-info { flex: 1; min-width: 0; }
.appbar-rank { font-weight: 800; font-size: 15px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Clipped like the rank above it. Allowed to wrap, "Level 2 · 33 / 100 XP" took
   a second line on a narrow phone and made the whole bar taller. */
.appbar-sub {
  font-size: 12px; color: var(--muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.xp-bar { height: 8px; background: var(--track); border-radius: 5px; overflow: hidden; flex: 1; min-width: 0; }
.xp-bar > span { display: block; height: 100%; background: var(--primary); border-radius: 5px; transition: width 0.4s ease; }
.appbar-metrics { display: flex; gap: 14px; flex: none; align-items: center; }
.metric b, .metric span { line-height: 1.15; }
.metric { text-align: center; }
.metric b { display: block; font-size: 16px; font-weight: 800; }
.metric span { font-size: 11px; color: var(--muted); }
.iconbtn {
  width: 44px; height: 44px; min-height: 44px; padding: 0;
  display: flex; align-items: center; justify-content: center;
  font-size: 20px; border-radius: 12px; flex: none;
}

/* ---------- topbar (back + title) ---------- */
.topbar { display: flex; align-items: center; gap: 10px; }
.topbar h2 { font-size: 20px; font-weight: 800; }
.back { font-size: 15px; padding: 10px 14px; }

/* ---------- login ---------- */
.login-wrap {
  flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 18px; text-align: center;
}
.login-card { width: 100%; max-width: 380px; display: flex; flex-direction: column; gap: 14px; }
/* The app icon itself, not an emoji — same artwork as the favicon and the
   home-screen icon, so the sign-in screen and the installed app match. */
.login-logo { width: 96px; height: 96px; border-radius: 22px; box-shadow: var(--shadow); }
.login-title { font-size: 24px; font-weight: 800; }
.error-text { color: var(--danger); font-weight: 700; font-size: 14px; min-height: 18px; }
/* Build identifier: deliberately quiet — findable, never competing for attention. */
.login-version { font-size: 12px; color: var(--muted); opacity: .7; letter-spacing: .02em; }

/* ---------- menu ---------- */
.menu-title { font-size: 26px; font-weight: 850; }
.menu-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 12px;
}
.tile {
  border-radius: var(--radius);
  padding: 18px 16px;
  min-height: 104px;
  display: flex; flex-direction: column; justify-content: space-between;
  color: #fff; cursor: pointer; border: none; text-align: left;
  box-shadow: var(--shadow);
}
.tile:active { transform: scale(0.98); }
.tile .tile-icon { font-size: 26px; }
.tile .tile-name { font-size: 17px; font-weight: 800; margin-top: 8px; }
.tile .tile-desc { font-size: 12px; opacity: 0.9; font-weight: 500; }
.tile.verbs { background: linear-gradient(135deg, #2f6fed, #214fbb); }
.tile.study { background: linear-gradient(135deg, #0ea5a3, #0b7e7c); }
.tile.input { background: linear-gradient(135deg, #1f9d6a, #14784f); }
.tile.choice { background: linear-gradient(135deg, #8b5cf6, #6d3ed8); }
.tile.matching { background: linear-gradient(135deg, #f0934d, #e2722b); }
.tile.challenge { background: linear-gradient(135deg, #ec5d8b, #c43f6c); }
.tile.stats { background: linear-gradient(135deg, #e59a25, #b87514); }
.tile.admin { background: linear-gradient(135deg, #64748b, #475569); }
.tile.activity { background: linear-gradient(135deg, #3b8fa8, #276b80); }

/* ---------- settings panel ---------- */
.settings-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.settings-grid .full { grid-column: 1 / -1; }

/* ---------- timer ---------- */
.timer { margin: 4px 0; }
.timer-label { font-weight: 800; font-size: 15px; margin-bottom: 4px; }
.timer-track { height: 10px; background: var(--track); border-radius: 5px; overflow: hidden; }
.timer-track > span { display: block; height: 100%; border-radius: 5px; width: 100%; }

/* ---------- question / play ---------- */
.question-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 20px 16px;
  text-align: center;
}
.hint-label { font-size: 13px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); font-weight: 700; }
.hint-text { font-size: 28px; font-weight: 850; margin: 6px 0 14px; word-break: break-word; }
.progress-pill { display: inline-block; font-size: 12px; font-weight: 700; color: var(--muted); background: var(--surface-alt); border-radius: 999px; padding: 4px 12px; }

.answer-inputs { display: flex; flex-direction: column; gap: 12px; margin-top: 8px; }

/* choice columns */
.choice-cols { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-top: 10px; }
.choice-col-title { font-size: 14px; font-weight: 800; text-align: center; margin-bottom: 8px; color: var(--muted); }
.choice-opt {
  width: 100%; margin-bottom: 10px; text-align: center; font-size: 16px;
  min-height: 56px; border-radius: 14px; white-space: normal; word-break: break-word;
}
.choice-opt.selected { background: var(--primary-soft); border: 2px solid var(--primary); }
.choice-opt.correct { background: var(--success-soft); border: 2px solid var(--success); color: var(--success-ink); }
.choice-opt.wrong { background: var(--danger-soft); border: 2px solid var(--danger); color: var(--danger); }

/* matching board */
.match-board { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
.match-col-title { font-size: 12px; font-weight: 800; text-align: center; color: var(--muted); }
.match-card {
  width: 100%; margin-top: 8px; min-height: 56px; padding: 10px 6px;
  font-size: 15px; border-radius: 12px; line-height: 1.2; white-space: normal; word-break: break-word;
}
.match-card .tr { display: block; font-size: 11px; color: var(--muted); font-weight: 500; margin-top: 2px; }
.match-card.selected { background: var(--primary-soft); border: 2px solid var(--primary); }
.match-card.matched { background: var(--success-soft); border: 2px solid var(--success); opacity: 0.65; }
.match-card.flash-wrong { background: var(--danger-soft); border: 2px solid var(--danger); }

/* feedback */
.feedback { border-radius: 14px; padding: 14px; margin-top: 12px; border-left: 5px solid var(--primary); background: var(--surface); border: 1px solid var(--border); }
.feedback.correct { border-left-color: var(--success); }
.feedback.wrong { border-left-color: var(--danger); }
.feedback.warning { border-left-color: var(--warning); }
.feedback h3 { font-size: 17px; margin-bottom: 6px; }
.feedback .forms { font-size: 16px; font-weight: 700; }
.feedback ul { margin: 8px 0 0; padding-left: 18px; }
.feedback li { margin: 3px 0; }

/* study / verb details */
.verb-detail-title { font-size: 24px; font-weight: 850; text-align: center; margin-bottom: 12px; }
.form-table { width: 100%; border-collapse: collapse; }
.form-table td { padding: 10px 8px; border-bottom: 1px solid var(--divider); }
.form-table .lbl { color: var(--muted); font-size: 13px; white-space: nowrap; }
.form-table .val { font-size: 18px; font-weight: 750; }
.form-table .tr { color: var(--muted); font-size: 14px; text-align: right; }
.detail-block { margin-top: 14px; font-size: 15px; }
.detail-block b { color: var(--muted); font-weight: 700; }
.study-nav { display: flex; gap: 12px; margin-top: 14px; }
.study-nav button { flex: 1; }

/* verbs list */
.search-box { margin-bottom: 12px; }
.verb-row {
  display: flex; align-items: center; gap: 10px; padding: 12px 14px;
  border-bottom: 1px solid var(--divider); cursor: pointer;
}
.verb-row:active { background: var(--surface-alt); }
.verb-row .forms { flex: 1; min-width: 0; }
.verb-row .forms b { font-size: 16px; }
.verb-row .forms span { color: var(--muted); font-size: 13px; }
.verb-row .acc { font-size: 13px; font-weight: 800; white-space: nowrap; }
.verb-list-card { padding: 4px 0; max-height: 60vh; overflow-y: auto; }

/* stats table */
.stat-summary { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 12px; }
.stat-box { background: var(--surface-alt); border-radius: 12px; padding: 12px; text-align: center; }
.stat-box b { display: block; font-size: 22px; font-weight: 850; }
.stat-box span { font-size: 12px; color: var(--muted); }
.table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }
table.data { width: 100%; border-collapse: collapse; font-size: 14px; }
table.data th, table.data td { padding: 8px 10px; text-align: left; white-space: nowrap; }
table.data thead th { background: var(--surface-alt); font-weight: 700; font-size: 12px; color: var(--muted); position: sticky; top: 0; }
table.data tbody tr { border-bottom: 1px solid var(--divider); }

/* admin */
.user-row { display: flex; align-items: center; gap: 10px; padding: 12px 0; border-bottom: 1px solid var(--divider); }
.user-row .uname { font-weight: 800; flex: 1; }
.tag { font-size: 11px; font-weight: 800; padding: 2px 8px; border-radius: 999px; background: var(--surface-alt); color: var(--muted); }
.tag.admin { background: var(--primary-soft); color: var(--primary); }

/* activity monitor */
.tag.activity-login { background: var(--success-soft); color: var(--success); }
.tag.activity-logout { background: var(--surface-alt); color: var(--muted); }
.tag.activity-login_failed { background: var(--danger-soft); color: var(--danger); }
.tag.activity-game_start { background: var(--primary-soft); color: var(--primary); }
table.data td .tag { white-space: nowrap; }

/* stage break / result */
.stage-break, .result-screen { text-align: center; display: flex; flex-direction: column; gap: 16px; align-items: center; padding: 18px; }
.big-emoji { font-size: 56px; }
.result-art { font-family: "SF Mono", Menlo, Consolas, monospace; font-size: 12px; line-height: 1.25; white-space: pre; text-align: left; background: var(--surface-alt); border-radius: 12px; padding: 12px; overflow-x: auto; }
.result-stage-line { display: flex; justify-content: space-between; gap: 10px; padding: 6px 0; border-bottom: 1px solid var(--divider); font-size: 14px; }

.toast {
  position: fixed; left: 50%; bottom: calc(24px + var(--safe-bottom)); transform: translateX(-50%);
  background: var(--toast-bg); color: var(--toast-ink); padding: 12px 20px; border-radius: 999px; font-weight: 700;
  box-shadow: var(--shadow-lg); z-index: 100; font-size: 14px; max-width: 90vw; text-align: center;
  opacity: 0; transition: opacity 0.2s ease, transform 0.2s ease; pointer-events: none;
}
.toast.show { opacity: 1; transform: translateX(-50%) translateY(-4px); }

.xp-float {
  position: fixed; left: 50%; top: 30%; transform: translateX(-50%);
  font-size: 32px; font-weight: 900; color: var(--success); z-index: 90; pointer-events: none;
  animation: floatUp 1s ease forwards;
}
@keyframes floatUp { from { opacity: 0; transform: transl(-50%, 10px) scale(0.8);} 30% {opacity:1;} to { opacity: 0; transform: translateX(-50%) translateY(-50px) scale(1.1); } }

/* ---------- tablet / desktop ---------- */
@media (max-width: 460px) {
  .appbar { padding: 9px 11px; gap: 7px; }
  .appbar-row { gap: 9px; }
  /* A fourth action (the voice source) landed here, and on a phone the row is
     paid for out of the rank name beside it. Tighter buttons buy most of it
     back and stay above the 44px touch target once the tap padding around a
     40px box is counted. */
  .appbar-actions { gap: 3px; }
  .iconbtn { width: 38px; height: 38px; min-height: 38px; font-size: 17px; }
  .rank-badge { width: 52px; height: 52px; font-size: 19px; }
  .appbar-rank { font-size: 14px; }
  /* "Level 2 — 33 / 100 XP" is directly underneath, so the level on this line is
     a repeat. It is what pays for the fourth button on a phone. */
  .appbar-rank-lv { display: none; }
  .appbar-metrics { gap: 10px; }
  .metric b { font-size: 14px; }
  .metric span { font-size: 10px; }
  /* A year of squares never fits a phone; they stay tappable and scroll. */
  .heatmap-card { --heat-cell: 11px; --heat-gap: 3px; }
  .heat-stat b { font-size: 16px; }
}

@media (min-width: 720px) {
  #app { padding-left: 24px; padding-right: 24px; gap: 18px; }
  .menu-grid { grid-template-columns: repeat(3, 1fr); gap: 16px; }
  .tile { min-height: 130px; }
  .hint-text { font-size: 34px; }
  .menu-title { font-size: 32px; }
  .settings-grid { grid-template-columns: repeat(3, 1fr); }
}
@media (min-width: 1000px) {
  .menu-grid { grid-template-columns: repeat(4, 1fr); }
  .play-2col { display: grid; grid-template-columns: 1.3fr 1fr; gap: 18px; align-items: start; }
}

/* ---------- grammar ---------- */
.menu-title.small { font-size: 18px; margin-top: 4px; }
.tile.grammar { background: linear-gradient(135deg, #7c5cf0, #5b3fc4); }
.tile.mixed   { background: linear-gradient(135deg, #d9534a, #b03a32); }

/* rule list */
.rule-list { padding: 4px 0; }
.rule-row {
  display: flex; align-items: center; gap: 12px; width: 100%;
  padding: 12px 14px; background: none; border: none; border-bottom: 1px solid var(--divider);
  text-align: left; font: inherit; color: inherit; cursor: pointer;
}
.rule-row:last-child { border-bottom: none; }
.rule-row:active { background: var(--surface-alt); }
.rule-icon { font-size: 22px; flex: none; }
.rule-icon.big { font-size: 34px; }
.rule-text { flex: 1; display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.rule-sub { font-size: 12px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rule-meta { flex: none; display: flex; flex-direction: column; align-items: flex-end; gap: 2px; font-size: 12px; }
.rule-meta .acc { font-weight: 800; font-size: 14px; }

/* rule page */
/* The head is a grid rather than a wrapping row, so the language toggle sits
   where the width of the screen puts it and never where the width of its own
   label does: on a phone it has a row of its own under the title, and beside the
   title from 560px up. It was a wrapping flex row, which meant "🇷🇺 По-русски"
   being a few pixels wider than "🇬🇧 In English" was enough to tip it onto a
   second line — so the button jumped every time it was pressed. The reserved
   min-width keeps the two labels the same size for the same reason. */
.rule-head {
  display: grid; align-items: center; gap: 8px 12px;
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-areas: "icon title" "lang lang";
}
.rule-head .rule-icon { grid-area: icon; }
.rule-head > div { grid-area: title; min-width: 0; }
.rule-head h2 { font-size: 20px; }
.rule-summary { margin-top: 12px; font-size: 15px; line-height: 1.5; }
.rule-summary .rule-listen { float: right; margin: 0 0 4px 8px; }
.lang-toggle {
  grid-area: lang; justify-self: end; font-size: 13px; font-weight: 700; padding: 8px 12px;
  border-radius: 999px; background: var(--surface-alt); border: 1px solid var(--border);
  min-width: 132px; text-align: center; white-space: nowrap;
}
@media (min-width: 560px) {
  .rule-head { grid-template-columns: auto minmax(0, 1fr) auto; grid-template-areas: "icon title lang"; }
}
.rule-diagram { padding: 8px; text-align: center; }
.rule-diagram img { display: block; margin: 0 auto; width: 100%; max-width: 560px; height: auto; }
.rule-section + .rule-section { margin-top: 18px; padding-top: 18px; border-top: 1px solid var(--divider); }
.rule-section-head { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
.rule-section-head h3 { font-size: 16px; margin-bottom: 0; }
.rule-section h3 { font-size: 16px; margin-bottom: 6px; }
.rule-section p { font-size: 14px; line-height: 1.55; color: var(--text-soft); }
.grammar-table { margin-top: 10px; font-size: 13px; }
.grammar-table td:first-child { font-weight: 700; color: var(--muted); }
.example-list, .source-list { margin-top: 8px; display: flex; flex-direction: column; gap: 8px; }
.example-list li, .source-list li { list-style: none; display: flex; flex-direction: column; gap: 2px; font-size: 14px; }
.example-list .muted { font-size: 13px; }
.example-en { display: flex; align-items: baseline; gap: 8px; }
.example-en b { flex: 1; min-width: 0; }
.source-list a { color: var(--primary); font-weight: 700; }

/* ---------- reading a rule aloud (js/grammar_audio.js) ----------
   Recording-only, English-only. The 🎧 launcher opens the shared player; the
   🔊 next to an example just plays it. Both share the muted, unobtrusive look of
   the word 🔊 elsewhere so a rule page never fills up with loud controls. */
.rule-listen, .rule-ex-audio {
  flex: none; padding: 0; border-radius: 50%; line-height: 1;
  background: var(--surface-alt); border-color: transparent; opacity: 0.8;
}
.rule-listen { width: 30px; height: 30px; min-height: 30px; font-size: 14px; margin-left: auto; }
.rule-ex-audio { width: 26px; height: 26px; min-height: 26px; font-size: 12px; }
.rule-listen:active, .rule-ex-audio:active, .rule-ex-audio.speaking { opacity: 1; background: var(--primary-soft); }
/* Reserved, not yet recorded: dimmed so the spot is visibly held for a file to
   come, and it still takes the tap to say so (a toast). */
.rule-listen.pending, .rule-ex-audio.pending { opacity: 0.32; }

/* The floating player: a narrow bar low on the screen, never over the text. */
.rule-player {
  position: fixed; left: 50%; bottom: 16px; transform: translateX(-50%);
  z-index: 60; display: flex; flex-direction: column; gap: 6px;
  width: min(430px, calc(100vw - 24px)); padding: 8px 12px;
  background: var(--surface); border: 1px solid var(--border); border-radius: 16px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.28);
}
.rule-player.hidden { display: none; }
.rule-player-top { display: flex; align-items: center; gap: 8px; }
.rule-player-transport { display: flex; align-items: center; gap: 8px; }
.rule-player-btn {
  flex: none; width: 34px; height: 34px; min-height: 34px; padding: 0;
  border-radius: 50%; font-size: 14px; line-height: 1;
  background: var(--surface-alt); border-color: transparent;
}
.rule-player-btn.play { background: var(--primary); color: #fff; }
.rule-player-btn:disabled { opacity: 0.35; }
.rule-player-btn.close { background: transparent; font-size: 13px; color: var(--muted); }
.rule-player-meta { flex: 1; display: flex; flex-direction: column; min-width: 0; padding: 0 2px; }
.rule-player-count { font-size: 10px; font-weight: 800; color: var(--muted); }
.rule-player-label { font-size: 12px; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* The scrubber grows to fill the transport row; the time readout sits to its right. */
.rule-player-seek {
  flex: 1; min-width: 0; height: 4px; padding: 0; margin: 0; border: none;
  -webkit-appearance: none; appearance: none; border-radius: 999px;
  background: var(--surface-alt); cursor: pointer;
}
.rule-player-seek::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none; width: 14px; height: 14px;
  border-radius: 50%; background: var(--primary); cursor: pointer;
}
.rule-player-seek::-moz-range-thumb {
  width: 14px; height: 14px; border: none; border-radius: 50%;
  background: var(--primary); cursor: pointer;
}
.rule-player-time { flex: none; font-size: 11px; color: var(--muted); font-variant-numeric: tabular-nums; white-space: nowrap; }

/* exercise */
.gap-sentence { font-size: 20px; font-weight: 700; line-height: 1.6; margin: 10px 0; }
.gap-sentence .gap {
  display: inline-block; min-width: 76px; padding: 0 6px; margin: 0 2px;
  border-bottom: 2px dashed var(--primary); text-align: center;
}
.gap-sentence .gap.filled { border-bottom-style: solid; color: var(--primary); }
.cue { font-size: 15px; font-weight: 700; color: var(--muted); margin-bottom: 12px; }
.option-list { display: flex; flex-direction: column; gap: 8px; }
.rule-checks { display: flex; flex-direction: column; }
.rule-check {
  display: flex; align-items: center; gap: 10px; padding: 10px 4px;
  border-bottom: 1px solid var(--divider); font-size: 14px;
}
.rule-check:last-child { border-bottom: none; }
.rule-check input { width: 20px; height: 20px; flex: none; }

/* learned marker */
.learned-mark {
  flex: none; width: 22px; height: 22px; border-radius: 50%;
  border: 2px solid var(--outline); background: transparent;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 13px; font-weight: 900; color: #fff;
}
.learned-mark.on { background: var(--success); border-color: var(--success); }
.learned-mark.small { width: 18px; height: 18px; font-size: 11px; }
.rule-row.learned .rule-text b { color: var(--success); }
.rules-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
.rules-head .muted { font-size: 13px; font-weight: 700; }
.learned-row { margin-top: 14px; }
.learned-toggle {
  width: 100%; font-size: 14px; font-weight: 700;
  border: 2px dashed var(--outline); background: var(--surface-alt); color: var(--muted);
}
.learned-toggle.on {
  border-style: solid; border-color: var(--success);
  background: var(--success-soft); color: var(--success);
}

/* admin settings */
.tile.settings { background: linear-gradient(135deg, #5b6b7f, #3d4a5c); }
.setting-card h3 { font-size: 16px; }
.setting-help { font-size: 13px; line-height: 1.5; margin: 8px 0 12px; }
.setting-default { font-size: 12px; margin-top: 8px; }

/* words & idioms */
.tile.vocab { background: linear-gradient(135deg, #0f9b8e, #0a7268); }
.vocab-item { padding: 12px 0; }
.vocab-item.sep { border-top: 1px solid var(--divider); }
.vocab-term { font-size: 17px; font-weight: 800; display: flex; align-items: center; gap: 8px; }
.vocab-translation { font-size: 15px; color: var(--primary); font-weight: 700; margin-top: 2px; }
.vocab-meaning { font-size: 13px; margin-top: 4px; }
.vocab-example { font-size: 13px; margin-top: 4px; }

/* the login screen's own theme switch, out of the way in the corner */
.login-theme { position: absolute; top: calc(14px + var(--safe-top)); right: calc(14px + var(--safe-right)); }
.login-wrap { position: relative; }

/* ---------- useful links ---------- */
.tile.useful { background: linear-gradient(135deg, #d98324, #a95f10); }
.useful-note { font-size: 13px; margin-top: 6px; line-height: 1.45; }
.sort-row { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
.sort-chip {
  font-size: 13px; font-weight: 700; padding: 8px 12px; min-height: 0;
  border-radius: 999px; background: var(--surface-alt); border: 1px solid var(--border);
  color: var(--muted);
}
.sort-chip.on { background: var(--primary-soft); border-color: var(--primary); color: var(--primary); }
/* overflow:hidden, because a pinned row paints a background right up to the
   card's rounded corner — without clipping, that square corner sticks out of
   the card whenever the first link is the starred one. */
.link-list { padding: 4px 0; overflow: hidden; }
.link-row {
  display: flex; align-items: center; gap: 10px;
  padding: 10px 14px; border-bottom: 1px solid var(--divider);
}
.link-row:last-child { border-bottom: none; }
.link-row.pinned { background: var(--surface-alt); }
.link-main { display: flex; flex-direction: column; gap: 3px; min-width: 0; flex: 1; text-decoration: none; color: inherit; }
.link-title { font-weight: 700; font-size: 15px; color: var(--primary); }
.link-sub { display: flex; align-items: center; gap: 7px; flex-wrap: wrap; font-size: 12px; }
.rule-chip {
  font-size: 11px; font-weight: 700; padding: 2px 8px; border-radius: 999px;
  background: var(--surface-alt); border: 1px solid var(--border); color: var(--muted);
}
.link-row.pinned .rule-chip { background: var(--surface); }
.pin-btn {
  flex: none; width: 38px; height: 38px; min-height: 38px; padding: 0;
  font-size: 18px; border-radius: 10px; color: var(--muted);
  background: transparent; border: 1px solid transparent;
}
.pin-btn.on { color: var(--warning); }

/* ---------- admin overview charts ---------- */
/* Categorical slots 1–2 of the validated palette, stepped per mode. */
:root {
  --series-1: #2a78d6;
  --series-2: #eb6834;
  --chart-grid: #e7edf6;
  --chart-axis: #cfd9ea;
}
:root[data-theme="dark"] {
  --series-1: #3987e5;
  --series-2: #d95926;
  --chart-grid: #29334a;
  --chart-axis: #3b4760;
}
.tile.overview { background: linear-gradient(135deg, #2f8f6f, #1e6b52); }
.chart { width: 100%; height: auto; display: block; margin-top: 10px; }
.chart-grid { stroke: var(--chart-grid); stroke-width: 1; }
.chart-axis { stroke: var(--chart-axis); stroke-width: 1.5; }
.chart-tick { fill: var(--muted); font-size: 11px; font-family: inherit; }
.chart-bar { transition: opacity .12s ease; }
.chart-bar:hover { opacity: .75; }
.chart-legend { display: flex; flex-wrap: wrap; gap: 14px; margin-top: 8px; font-size: 13px; font-weight: 700; }
.legend-item { display: inline-flex; align-items: center; gap: 6px; }
.legend-swatch { width: 12px; height: 12px; border-radius: 3px; flex: none; }
.chart-table { margin-top: 10px; font-size: 13px; }
.chart-table summary { cursor: pointer; color: var(--muted); font-weight: 700; }
.user-card-head { display: flex; align-items: center; gap: 9px; }
.user-card-head h3 { font-size: 17px; }

/* ---------- activity calendar (root menu) ---------- */
/* One square per day, five shades. Sized in a variable so the phone breakpoint
   at the bottom of this file only has to change one number. */
.heatmap-card { --heat-cell: 13px; --heat-gap: 3px; margin-top: 14px; }
.heatmap-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; flex-wrap: wrap; }
.heatmap-sub { font-size: 13px; }
.heat-wrap { position: relative; margin-top: 12px; }
.heat-stats { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; }
.heat-stat {
  flex: 1 1 70px; display: flex; flex-direction: column; align-items: center; gap: 2px;
  padding: 8px 6px; border-radius: 12px; background: var(--surface-alt);
}
.heat-stat b { font-size: 18px; font-weight: 800; }
.heat-stat span { font-size: 11px; color: var(--muted); text-align: center; }
.heat-body { display: flex; gap: 6px; align-items: flex-start; }
/* The weekday column is fixed while the year scrolls next to it. */
.heat-days {
  display: grid; grid-template-rows: repeat(7, var(--heat-cell)); gap: var(--heat-gap);
  padding-top: 18px; flex: none;
}
.heat-day-label { font-size: 10px; color: var(--muted); line-height: var(--heat-cell); }
.heat-scroll { overflow-x: auto; overflow-y: hidden; flex: 1; scrollbar-width: thin; }
.heat-cols { display: inline-flex; flex-direction: column; gap: 4px; }
.heat-months { display: flex; gap: var(--heat-gap); height: 14px; }
.heat-month { width: var(--heat-cell); font-size: 10px; color: var(--muted); white-space: nowrap; }
.heat-grid { display: flex; gap: var(--heat-gap); }
.heat-week { display: grid; grid-template-rows: repeat(7, var(--heat-cell)); gap: var(--heat-gap); }
.heat-cell {
  width: var(--heat-cell); height: var(--heat-cell); border-radius: 3px;
  background: var(--heat-0); cursor: default;
}
.heat-cell:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }
.heat-cell.l1 { background: var(--heat-1); }
.heat-cell.l2 { background: var(--heat-2); }
.heat-cell.l3 { background: var(--heat-3); }
.heat-cell.l4 { background: var(--heat-4); }
.heat-legend { display: flex; align-items: center; justify-content: flex-end; gap: 4px; margin-top: 10px; font-size: 11px; }
.heat-legend .muted { margin: 0 4px; }
/* Absolutely positioned so it can sit over the grid without moving it; the
   JS clamps it inside the card. */
.heat-tip {
  position: absolute; z-index: 5; pointer-events: none; opacity: 0;
  transition: opacity .12s ease; max-width: 260px;
  background: var(--toast-bg); color: var(--toast-ink);
  padding: 6px 10px; border-radius: 8px; font-size: 12px; line-height: 1.35;
  box-shadow: var(--shadow);
}
.heat-tip.on { opacity: 1; }

/* A flawless round gets a green frame and a line saying so — the same ending in
   every module (js/result.js). */
.result-screen.perfect { border-color: var(--success); }
.perfect-line { color: var(--success); font-weight: 800; margin-top: 2px; }

/* Search across every word set (js/screens/vocabulary.js). */
.vocab-search { display: flex; flex-direction: column; gap: 8px; }
.vocab-search input[type="search"] { -webkit-appearance: none; appearance: none; }
.vocab-search .rule-row { padding: 10px 14px; }

/* "Favourites only" on the Useful page — a filter, next to the ordering chips. */
.only-pinned { display: inline-flex; align-items: center; gap: 8px; margin-top: 12px; cursor: pointer; font-size: 14px; font-weight: 700; }
.only-pinned input { width: 20px; height: 20px; min-height: 0; flex: none; accent-color: var(--primary); }

/* ---------- AI coach ---------- */
/* The tunnel/agent settings are a form inside a <details>, so the panels reuse
   .card and only add what a disclosure needs (js/screens/ai_advice.js). */
.tile.ai { background: linear-gradient(135deg, #7c5cf6, #5a35d1); }
.tile.ai-advice { background: linear-gradient(135deg, #4f7df5, #3055c8); }
.tag.activity-ai_advice { background: var(--primary-soft); color: var(--purple); }
.tag.danger { background: var(--danger-soft); color: var(--danger); }

.ai-lead { font-size: 13px; line-height: 1.5; margin: 8px 0 12px; }
/* Help text, not a label: label.field sets 600 on everything inside it, which
   made hints read as headings. */
.ai-hint { font-size: 12px; line-height: 1.45; margin-top: 6px; font-weight: 500; }
.ai-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; }

.ai-panel { padding: 0; }
.ai-panel > summary {
  padding: 16px; font-weight: 800; cursor: pointer; list-style: none;
  display: flex; align-items: center; gap: 8px;
}
.ai-panel > summary::-webkit-details-marker { display: none; }
.ai-panel > summary::after { content: "▾"; margin-left: auto; color: var(--muted); }
.ai-panel[open] > summary { border-bottom: 1px solid var(--divider); }
.ai-panel[open] > summary::after { content: "▴"; }
.ai-grid { padding: 16px; }
.ai-save { margin: 0 16px 16px; width: calc(100% - 32px); }

.ai-inline { display: flex; gap: 8px; align-items: center; }
.ai-inline input { flex: 1; }
.ai-clear { min-height: 0; padding: 6px 10px; font-size: 13px; flex: none; }
.ai-secret-row { display: flex; align-items: center; gap: 8px; margin-top: 6px; }
.ai-secret-state { font-size: 12px; color: var(--muted); font-weight: 500; flex: 1; }
.ai-warn {
  font-size: 12px; line-height: 1.45; color: var(--warning);
  background: var(--surface-alt); border-radius: 10px; padding: 10px 12px;
}

.ai-checks { display: flex; flex-wrap: wrap; gap: 10px 16px; }
.ai-check { display: inline-flex; align-items: center; gap: 8px; font-size: 14px; font-weight: 650; color: var(--text); cursor: pointer; }
.ai-check input { width: 20px; height: 20px; min-height: 0; flex: none; accent-color: var(--primary); }

textarea {
  font-family: inherit; font-size: 16px; color: var(--text);
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 12px; padding: 12px 14px; width: 100%; resize: vertical;
  line-height: 1.45;
}
textarea:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px rgba(47, 111, 237, 0.12); }

/* History: one tappable row per answer, newest first. */
.ai-history { display: flex; flex-direction: column; }
.ai-history-row {
  display: block; width: 100%; text-align: left; background: transparent;
  border: none; border-top: 1px solid var(--divider); border-radius: 0;
  padding: 12px 2px; min-height: 0;
}
.ai-history-row:first-child { border-top: none; }
.ai-history-when { font-size: 13px; font-weight: 800; }
.ai-history-preview {
  font-size: 13px; color: var(--muted); font-weight: 500; margin-top: 2px;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.ai-history-meta { margin-top: 6px; display: flex; flex-wrap: wrap; gap: 6px; }
/* The agent, highlighted: scanning the history you are looking for "which one
   answered this", and a model id alone does not say. */
.tag.agent { background: var(--primary-soft); color: var(--primary); }
.ai-empty { padding: 16px; }

/* The rendered answer. Markdown from an assistant, escaped first — the tags
   here are the ones js/markdown.js writes, and no others can appear. */
.ai-advice { font-size: 15px; line-height: 1.6; }
.ai-advice h3 { font-size: 17px; margin: 18px 0 6px; }
.ai-advice h4 { font-size: 15px; margin: 14px 0 4px; }
.ai-advice > h3:first-child, .ai-advice > h4:first-child { margin-top: 0; }
.ai-advice p { margin: 8px 0; }
.ai-advice ul, .ai-advice ol { margin: 8px 0; padding-left: 22px; }
.ai-advice li { margin: 4px 0; }
.ai-advice code {
  background: var(--surface-alt); border-radius: 6px; padding: 1px 5px;
  font-size: 13px;
}
.ai-advice a { color: var(--primary); }
.ai-advice blockquote {
  margin: 10px 0; padding: 6px 12px; border-left: 3px solid var(--border);
  color: var(--muted);
}
.ai-advice hr { border: none; border-top: 1px solid var(--divider); margin: 16px 0; }

/* ---------- AI coach: the other two tools ---------- */
.tile.ai-rule { background: linear-gradient(135deg, #0ea5a3, #0b7e7c); }
.tile.ai-ask { background: linear-gradient(135deg, #ec5d8b, #c43f6c); }
.tile.ai-usage { background: linear-gradient(135deg, #5b6b7f, #3d4a5c); }

/* The assistant's own title for an answer, above the answer itself. */
.ai-title { font-size: 20px; font-weight: 850; margin: 10px 0 12px; }

/* The prompt-only mode: a literal block of text to copy, so it is a <pre> and
   not rendered Markdown — formatting it would hide the characters being copied. */
.ai-prompt {
  margin: 12px 0 0; padding: 14px; border-radius: 12px;
  background: var(--surface-alt); border: 1px solid var(--border);
  font-size: 12px; line-height: 1.5; white-space: pre-wrap; word-break: break-word;
  max-height: 420px; overflow: auto;
}
.ai-mode { border-top: 1px solid var(--divider); padding-top: 12px; margin-top: 4px; }
.ai-mode .ai-check { font-size: 15px; }

/* Example questions on the Ask screen: taps that fill the box. */
.ai-examples { display: flex; flex-wrap: wrap; gap: 8px; margin: 12px 0; }
.ai-example {
  font-size: 12px; font-weight: 600; min-height: 0; padding: 8px 12px;
  border-radius: 999px; color: var(--muted); text-align: left;
}

/* "Still unclear?" on a rule page — quiet, full width, right under the summary
   where the doubt arrives (js/screens/grammar_rule.js). */
.explain-btn {
  width: 100%; margin-top: 12px; font-size: 14px; min-height: 44px;
  background: var(--primary-soft); border-color: transparent; color: var(--primary);
}

/* ---------- admin: AI usage ---------- */
.ai-kind-row { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 12px; }
.ai-usage-list { display: flex; flex-direction: column; gap: 14px; margin-top: 12px; }
.ai-usage-row { display: flex; flex-direction: column; gap: 6px; }
.ai-usage-name { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; font-size: 14px; }
.ai-usage-name .muted { font-size: 12px; font-weight: 500; }
.ai-usage-figures { font-size: 12px; }
.ai-usage-tags { display: flex; flex-wrap: wrap; gap: 6px; }
.ai-bar { height: 8px; border-radius: 4px; background: var(--track); overflow: hidden; }
.ai-bar > span { display: block; height: 100%; border-radius: 4px; background: var(--primary); }

/* ---------- AI coach: words, and the admin period picker ---------- */
.tile.ai-word { background: linear-gradient(135deg, #e59a25, #b87514); }

/* Straight from a word in a set to "how is it actually used?" — quiet, so the
   entry still reads as the entry (js/screens/vocabulary.js). */
.word-explain {
  margin-top: 8px; min-height: 36px; padding: 6px 12px; font-size: 12px;
  border-radius: 999px; background: var(--primary-soft);
  border-color: transparent; color: var(--primary);
}

.ai-quick { display: flex; flex-wrap: wrap; gap: 8px; margin: 12px 0; }
.ai-quick-btn {
  min-height: 38px; padding: 8px 14px; font-size: 13px; border-radius: 999px;
}
.ai-quick-btn.on { background: var(--primary); border-color: var(--primary); color: #fff; }
.ai-range input[type="date"] { min-height: 48px; }

/* ---------- AI: language, history controls, word search ---------- */
/* Answer language on the card, not behind a disclosure — it is the one setting
   that changes per question (js/ai_shared.js). */
.ai-langs { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-bottom: 12px; }
.ai-lang-label { font-size: 12px; font-weight: 700; color: var(--muted); }
.ai-lang { min-height: 36px; padding: 6px 14px; font-size: 13px; border-radius: 999px; }
.ai-lang.on { background: var(--primary-soft); border-color: var(--primary); color: var(--primary); }

.ai-history-controls { display: flex; flex-direction: column; gap: 8px; margin: 10px 0 6px; }
.ai-history-controls input[type="search"] { -webkit-appearance: none; appearance: none; }
.ai-history-controls .sort-row { margin: 0; }

/* Search results under the word field on the AI word screen. */
.ai-suggest {
  margin-top: 8px; border: 1px solid var(--border); border-radius: 12px;
  overflow: hidden; background: var(--surface-alt);
}
.ai-suggest-row {
  display: flex; align-items: center; gap: 10px; width: 100%; text-align: left;
  background: transparent; border: none; border-radius: 0; min-height: 0;
  padding: 10px 12px; border-top: 1px solid var(--divider);
}
.ai-suggest-row:first-child { border-top: none; }

/* Filter + order inside one word set (js/screens/vocabulary.js). */
.vocab-browse { display: flex; flex-direction: column; gap: 8px; }
.vocab-browse input[type="search"] { -webkit-appearance: none; appearance: none; }
.vocab-browse .sort-row { margin: 0; }

/* A native date input on iOS lays its own content out and will happily push it
   past the field's edge. Letting the box shrink and fixing the text size stops
   the value spilling out of the rounded border. */
input[type="date"] {
  -webkit-appearance: none; appearance: none;
  min-width: 0; max-width: 100%; font-size: 16px;
}

/* The period range: one column on a phone, with its own Apply. */
.ai-range { display: flex; flex-direction: column; gap: 10px; }
.ai-range .field { width: 100%; }
.ai-apply { width: 100%; }
@media (min-width: 700px) {
  .ai-range { flex-direction: row; align-items: flex-end; }
  .ai-range .field { flex: 1; }
  .ai-apply { width: auto; flex: none; }
}

/* ---------- foldable sections on the Useful page ---------- */
/* Whole sections shut away, because reaching the curated groups meant scrolling
   past every rule link first (js/screens/useful.js). */
.useful-head {
  display: flex; align-items: center; gap: 6px; width: 100%;
  background: transparent; border: none; border-radius: 12px;
  padding: 14px 8px; min-height: 48px; text-align: left; cursor: pointer;
}
.useful-head:active { background: var(--surface-alt); }
.useful-head-title { font-size: 17px; font-weight: 800; flex: 1; }
/* A real target rather than a 12px sliver: the caret is the part of the header
   that most looks like the thing to press, so it has to be one. */
.useful-caret {
  color: var(--muted); font-size: 13px; width: 28px; text-align: center; flex: none;
}
.useful-section.folded { display: none; }

/* ---------- your own page ---------- */
/* The app bar badge is a button now: it looks the same, it just goes somewhere. */
.appbar-me {
  display: flex; align-items: center; gap: 12px; flex: 1; min-width: 0;
  background: transparent; border: none; border-radius: 12px;
  padding: 2px; min-height: 0; text-align: left; color: inherit;
}
.appbar-me:active { transform: scale(0.98); }

.profile-head { display: flex; align-items: center; gap: 18px; }
/* Big on purpose: the rank art is the reward, so it is worth actually seeing.
   ~2x the app-bar badge; the who-column beside it is about this tall already, so
   the card barely changes height while the badge doubles. */
.profile-badge {
  width: 120px; height: 120px; border-radius: 50%; flex: none;
  display: flex; align-items: center; justify-content: center;
  color: #fff; font-weight: 900; font-size: 46px; overflow: hidden;
}
/* Same handover as `.rank-badge.has-art`: the art carries its own tier colour. */
.profile-badge.has-art { border-radius: 0; }
.profile-badge img { width: 100%; height: 100%; object-fit: contain; }
.profile-who { min-width: 0; }
.profile-rank { font-size: 14px; font-weight: 700; color: var(--primary); margin-top: 2px; }
.profile-since { font-size: 12px; margin-top: 4px; }
/* Jump straight to the rank map — noticeable (a filled accent pill with a down
   arrow) but no wider than its label so it does not crowd the plaque. */
.to-map-btn {
  display: inline-flex; align-items: center; gap: 6px; margin-top: 12px;
  padding: 8px 14px; border-radius: 999px; font-size: 13px; font-weight: 800;
  color: var(--primary); background: var(--primary-soft);
  border: 1px solid var(--primary); min-height: 0;
}
.to-map-btn:active { background: var(--primary); color: #fff; }
.to-map-arrow { font-weight: 900; animation: toMapBob 1.6s ease-in-out infinite; }
@keyframes toMapBob { 0%, 100% { transform: translateY(-1px); } 50% { transform: translateY(2px); } }
@media (prefers-reduced-motion: reduce) { .to-map-arrow { animation: none; } }
.profile-next { font-size: 13px; line-height: 1.5; margin-top: 10px; }
.profile-spread {
  display: flex; justify-content: space-between; gap: 12px;
  font-size: 13px; margin-top: 8px; padding-top: 8px;
  border-top: 1px solid var(--divider);
}
.profile-spread .muted { font-weight: 600; }
.profile-modules { margin-top: 10px; }
.profile-warn {
  margin-top: 10px; font-size: 12px; line-height: 1.45; color: var(--warning);
  background: var(--surface-alt); border-radius: 10px; padding: 10px 12px;
}
.profile-ladder-note { font-size: 13px; line-height: 1.5; margin: 6px 0 12px; }

/* Fifty ranks as a grid. Unreached ones are dimmed rather than hidden — seeing
   what is coming is most of the reason to open this. */
.rank-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
.rank-cell {
  background: var(--surface-alt); border: 1px solid transparent;
  border-radius: 12px; padding: 10px 6px; text-align: center; opacity: 0.45;
  /* Fifty 256px PNGs drawn at 44px is fifty decodes and fifty rasterisations,
     which is what made scrolling to this card stutter. Cells off screen are not
     rendered at all; the intrinsic size keeps the scrollbar honest anyway. */
  content-visibility: auto;
  contain-intrinsic-size: auto 104px;
}
.rank-cell.reached { opacity: 1; }
.rank-cell.current { border-color: var(--primary); background: var(--primary-soft); }
.rank-art { width: 44px; height: 44px; margin: 0 auto 6px; }
/* No round mask: the art is a badge on transparency, already carrying its own
   rim, and a circle inscribed in the square cuts off whatever spreads past it. */
.rank-art img { width: 100%; height: 100%; object-fit: contain; }
.rank-name { font-size: 11px; font-weight: 700; line-height: 1.3; }
.rank-level { font-size: 10px; color: var(--muted); margin-top: 2px; }
@media (min-width: 700px) {
  .rank-grid { grid-template-columns: repeat(5, 1fr); }

}

/* Coverage bars on your own page: how much of the app has been opened at all. */
.coverage-row { margin-top: 12px; }
.coverage-line {
  display: flex; justify-content: space-between; gap: 10px;
  font-size: 13px; font-weight: 650; margin-bottom: 6px;
}

/* A search box over a list, on the pages where the list is long enough to hunt
   in: grammar rules and Useful links. Both match the *name* only — matching the
   body would answer "which page mentions this" when the question is "where is
   it". */
.list-search { display: flex; flex-direction: column; gap: 8px; }
.list-search input[type="search"] { -webkit-appearance: none; appearance: none; }
.filtered-out { display: none; }

/* Back to the whole set after arriving from a search that pre-filtered it. */
.vocab-clear { align-self: flex-start; min-height: 34px; padding: 6px 12px; font-size: 12px; }

/* The way home, when Back means "where I came from" (js/appbar.js). */
.topbar-menu {
  margin-left: auto; min-height: 40px; min-width: 44px; padding: 8px 12px;
  font-size: 16px; color: var(--muted);
}

/* The rule name in a drill: printed when you picked the rule yourself, and a
   small "?" you can press when you did not (js/screens/grammar_drill.js). */
.question-head { position: relative; display: flex; align-items: center; gap: 8px; min-height: 22px; }
.rule-hint-btn {
  width: 24px; height: 24px; min-height: 24px; padding: 0; flex: none;
  border-radius: 50%; font-size: 12px; font-weight: 800;
  color: var(--muted); background: var(--surface-alt); border-color: transparent;
  opacity: 0.7;
}
.rule-hint-btn:active { opacity: 1; }
.rule-hint-pop {
  position: absolute; left: 0; top: 30px; z-index: 6; pointer-events: none;
  opacity: 0; transition: opacity .15s ease;
  background: var(--toast-bg); color: var(--toast-ink);
  padding: 6px 10px; border-radius: 8px; font-size: 12px; font-weight: 700;
  box-shadow: var(--shadow);
}
.rule-hint-pop.on { opacity: 1; }

/* Say it out loud. Quiet next to the word — it is an offer, not an instruction
   (js/speech.js). */
.speak-btn {
  width: 30px; height: 30px; min-height: 30px; padding: 0; flex: none;
  border-radius: 50%; font-size: 14px; line-height: 1;
  background: var(--surface-alt); border-color: transparent; opacity: 0.75;
}
.speak-btn:active, .speak-btn.speaking { opacity: 1; background: var(--primary-soft); }
/* Nothing to play from the source that is switched on — the three words that
   ship no recording, while recordings are the chosen voice. Faded and struck
   through rather than `disabled`, because it still has to take the tap that
   makes it explain itself (js/speech.js). */
.speak-btn.unavailable { opacity: 0.3; cursor: not-allowed; position: relative; }
.speak-btn.unavailable::after {
  content: ""; position: absolute; left: 15%; right: 15%; top: 50%;
  border-top: 2px solid currentColor; transform: rotate(-45deg);
}
.speak-btn.unavailable:active { opacity: 0.3; background: var(--surface-alt); }

/* ---------- spaced repetition ---------- */
/* The prompt on the root menu. Present only while something is due — there is
   no empty state for it, because "nothing to review" is not news
   (js/screens/review.js). */
.review-prompt {
  display: flex; align-items: center; gap: 12px; width: 100%; text-align: left;
  margin-bottom: 14px; padding: 14px 16px; min-height: 0;
  background: var(--primary-soft); border-color: transparent;
  border-radius: var(--radius);
}
.review-prompt-icon { font-size: 22px; flex: none; }
.review-prompt-text { display: flex; flex-direction: column; gap: 2px; flex: 1; min-width: 0; }
.review-prompt-text b { color: var(--primary); font-size: 15px; }
.review-prompt-text .muted { font-size: 12px; font-weight: 500; }
.review-prompt-go { color: var(--primary); font-size: 20px; font-weight: 800; flex: none; }

.review-queue-head { display: flex; align-items: center; gap: 12px; }
.review-icon { font-size: 22px; flex: none; }
.review-due { font-size: 12px; margin-top: 2px; }
.review-names { font-size: 13px; line-height: 1.5; margin-top: 10px; }
.review-queue .btn-block { margin-top: 12px; }

/* Each verb form gets its own 🔊 — they are three different words to say, and
   the one you cannot pronounce is rarely the infinitive. */
.form-say { display: inline-flex; align-items: center; gap: 8px; }
.form-say .speak-btn { width: 26px; height: 26px; min-height: 26px; font-size: 12px; }

/* ============================================================= *
 *  Rank ladder as a league map (js/screens/profile.js)
 * ============================================================= */
.card-eyebrow { font-size: 12px; font-weight: 800; letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted); margin-bottom: 12px; }
.card-eyebrow.center { text-align: center; }

/* League colours — a material ladder (wood → bronze → steel → silver → gold). */
.league-wood   { --l1: var(--lg-wood-1);   --l2: var(--lg-wood-2);   --link: var(--lg-wood-ink); }
.league-bronze { --l1: var(--lg-bronze-1); --l2: var(--lg-bronze-2); --link: var(--lg-bronze-ink); }
.league-steel  { --l1: var(--lg-steel-1);  --l2: var(--lg-steel-2);  --link: var(--lg-steel-ink); }
.league-silver { --l1: var(--lg-silver-1); --l2: var(--lg-silver-2); --link: var(--lg-silver-ink); }
.league-gold   { --l1: var(--lg-gold-1);   --l2: var(--lg-gold-2);   --link: var(--lg-gold-ink); }

.league-card {
  position: relative; overflow: hidden; padding: 16px;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: var(--shadow);
}
.league-card::before { content: ""; position: absolute; top: 0; left: 0; right: 0; height: 4px; z-index: 2; background: linear-gradient(90deg, var(--l1), var(--l2)); }
/* Placeholder tint until real league art lands at /assets/leagues/<key>-bg.jpg
   (see assets/leagues/README.md). Real art upgrades this in place. */
.league-bg { position: absolute; inset: 0; z-index: 0; opacity: 0.14; background: linear-gradient(135deg, var(--l1), var(--l2)); background-size: cover; background-position: center; }
.league-bg.has-img { opacity: 1; }
.league-bg.has-img::after { content: ""; position: absolute; inset: 0; background: var(--surface); opacity: 0.66; }
.league-head, .rank-path, .league-toggle { position: relative; z-index: 1; }

.league-head { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
.league-emblem {
  width: 56px; height: 56px; border-radius: 50%; flex: none; position: relative; overflow: hidden;
  background: radial-gradient(circle at 32% 28%, var(--l1), var(--l2));
  box-shadow: inset 0 0 0 3px rgba(255, 255, 255, 0.35), 0 3px 10px rgba(0, 0, 0, 0.28);
}
.league-emblem img { width: 100%; height: 100%; object-fit: contain; }
/* The real seal art already carries its own round frame on a transparent
   background, so drop the placeholder's disc, its inset white ring (which read
   as a grey outline over the art) and the circular clip — show the art as-is. */
.league-emblem.has-img { background: none; box-shadow: none; border-radius: 0; overflow: visible; }
.league-meta { flex: 1; min-width: 0; }
.league-name { font-weight: 850; font-size: 17px; }
.league-sub { font-size: 12px; color: var(--muted); }
.league-range { flex: none; font-weight: 800; font-size: 12px; padding: 4px 10px; border-radius: 999px; color: var(--link); background: var(--surface-alt); }

/* The path is only as wide as a full row, and centred — so the drop connectors
   below align under the node the path leaves by, whatever the container width. */
.rank-path { position: relative; display: flex; flex-direction: column; align-items: stretch; gap: 4px; width: fit-content; max-width: 100%; margin: 0 auto; }
.rank-row { display: flex; align-items: flex-start; justify-content: center; gap: 6px; }
.rank-row.rev { flex-direction: row-reverse; }
/* Spacers only now — the dashed line that used to live on their borders is
   drawn as one continuous curvy SVG path (js/screens/profile.js, .rank-trail). */
.rank-conn { flex: 0 0 26px; align-self: flex-start; margin-top: 27px; }
.rank-link { display: flex; height: 16px; }
.rank-link i { width: 0; }
/* The snaking dashed trail, threaded through the node centres, sitting behind
   the badges (which are lifted to z-index 1). overflow visible so a wide
   milestone arc is never clipped at the card edge. */
.rank-trail { position: absolute; inset: 0; z-index: 0; pointer-events: none; overflow: visible; }
.rank-trail path { fill: none; stroke: var(--outline); stroke-width: 2; stroke-dasharray: 5 6; stroke-linecap: round; stroke-linejoin: round; }

.rank-node { position: relative; z-index: 1; display: flex; flex-direction: column; align-items: center; gap: 5px; width: 84px; cursor: pointer; transition: transform 0.16s ease; }
.rank-avatar { position: relative; flex: none; }
.rank-avatar img { display: block; width: 100%; height: 100%; object-fit: contain; }
.rank-node-badge { width: 54px; height: 54px; transition: transform 0.16s ease; }
.rank-node-label { display: flex; flex-direction: column; align-items: center; line-height: 1.1; }
.rank-node-lv { font-size: 10px; font-weight: 800; color: var(--muted); }
.rank-node-name { font-size: 11px; font-weight: 700; text-align: center; line-height: 1.15; color: var(--text-soft); max-width: 84px; }
.node-star { position: absolute; top: -6px; right: -4px; font-size: 15px; color: var(--warning); text-shadow: 0 1px 2px rgba(0, 0, 0, 0.4); z-index: 2; }

.rank-node.locked .rank-avatar img { filter: grayscale(1) brightness(0.72); opacity: 0.55; }
.rank-node.locked .rank-node-lv, .rank-node.locked .rank-node-name { color: var(--placeholder); }
/* The current badge grows and glows along its own silhouette (drop-shadow, so a
   winged or laurelled badge keeps its shape). The extra room below is reserved so
   the enlarged art never reaches its own label. */
.rank-node.current .rank-node-badge { transform: scale(1.28); margin-bottom: 9px; filter: drop-shadow(0 0 5px var(--primary)) drop-shadow(0 3px 4px rgba(0, 0, 0, 0.3)); }
.rank-node.current .rank-node-name { font-weight: 850; color: var(--text); }
.rank-node.current .rank-node-lv { color: var(--primary); }

/* Press-and-hold: the node grows while the finger is down, eases back on
   release — the same feedback as the promotion badges. */
.rank-node.pressed { transform: scale(1.08); }

.league-toggle { width: 100%; margin-top: 16px; min-height: 40px; font-size: 13px; font-weight: 700; color: var(--muted); }

/* Next-rank card: the badge above you behind a lock. */
.next-rank-row { display: flex; align-items: center; gap: 16px; }
.next-rank-row .rank-avatar { width: 72px; height: 72px; }
.rank-avatar.locked img { filter: grayscale(1) brightness(0.68); opacity: 0.5; }
.avatar-lock { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-size: 26px; }
.next-rank-meta { flex: 1; min-width: 0; }
.rank-lv-pill { display: inline-block; padding: 2px 11px; border-radius: 999px; background: var(--surface-alt); color: var(--muted); font-weight: 800; font-size: 12px; }
.next-rank-name { font-size: 19px; font-weight: 800; margin: 7px 0 4px; }
.next-rank-need { font-size: 14px; }

/* League promotion: your badge → the next league's first, colour running across.
   Dressed up a little — the two league colours meet in a diagonal wash, a
   dusting of light specks sits over it, and a slow sheen sweeps through, so the
   boundary card reads as an occasion and not another row. */
.promo-card { position: relative; overflow: hidden; text-align: center; }
.promo-grad {
  position: absolute; inset: 0; z-index: 0; opacity: 0.2;
  background:
    radial-gradient(120% 85% at 12% -10%, var(--from), transparent 62%),
    radial-gradient(120% 85% at 88% 110%, var(--to), transparent 62%),
    linear-gradient(100deg, var(--from), var(--to));
}
.promo-card::before {
  content: ""; position: absolute; inset: 0; z-index: 0; opacity: 0.55; pointer-events: none;
  background:
    radial-gradient(1.6px 1.6px at 18% 28%, rgba(255, 255, 255, 0.9), transparent 60%),
    radial-gradient(1.4px 1.4px at 72% 18%, rgba(255, 255, 255, 0.7), transparent 60%),
    radial-gradient(2px 2px at 42% 72%, rgba(255, 255, 255, 0.6), transparent 60%),
    radial-gradient(1.5px 1.5px at 86% 64%, rgba(255, 255, 255, 0.8), transparent 60%),
    radial-gradient(1.4px 1.4px at 56% 40%, rgba(255, 255, 255, 0.5), transparent 60%);
}
.promo-card::after {
  content: ""; position: absolute; top: 0; bottom: 0; left: -60%; width: 45%; z-index: 0;
  pointer-events: none; transform: skewX(-18deg);
  background: linear-gradient(100deg, transparent, rgba(255, 255, 255, 0.16), transparent);
  animation: promoSheen 5.5s ease-in-out infinite;
}
.promo-card > *:not(.promo-grad) { position: relative; z-index: 1; }
.promo-title { font-size: 22px; font-weight: 850; margin: 4px 0 16px; }
.promo-row { display: flex; align-items: center; justify-content: center; gap: 20px; }
.promo-avatar { width: 78px; height: 78px; cursor: pointer; transition: transform 0.14s ease; }
/* Press-and-hold: grows while the finger is down, eases back on release. */
.promo-avatar.pressed { transform: scale(1.1); }
.promo-arrow { font-size: 30px; font-weight: 900; background: linear-gradient(90deg, var(--from), var(--to)); -webkit-background-clip: text; background-clip: text; color: transparent; }

/* Entrance animations, played once when the card scrolls into view (profile.js
   adds the class): the promotion card zooms up, the next-rank card zooms with a
   small shake — the rank you are actually chasing gets the more insistent one. */
@keyframes cardZoomIn {
  0% { transform: scale(0.8); opacity: 0; }
  60% { transform: scale(1.05); opacity: 1; }
  100% { transform: scale(1); }
}
@keyframes cardZoomShake {
  0% { transform: scale(0.78); opacity: 0; }
  55% { transform: scale(1.08); opacity: 1; }
  66% { transform: scale(1.03) translateX(-5px) rotate(-1deg); }
  76% { transform: scale(1.02) translateX(5px) rotate(1deg); }
  86% { transform: scale(1.01) translateX(-3px); }
  94% { transform: scale(1) translateX(2px); }
  100% { transform: scale(1) translateX(0) rotate(0); }
}
@keyframes promoSheen { 0% { left: -60%; } 55%, 100% { left: 130%; } }
.promo-card.zoom { animation: cardZoomIn 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) both; }
.next-rank.pop { animation: cardZoomShake 0.72s cubic-bezier(0.34, 1.56, 0.64, 1) both; }

.summit-card { text-align: center; }
.summit-emoji { font-size: 46px; margin-bottom: 6px; }

@media (prefers-reduced-motion: reduce) {
  .promo-card::after { animation: none; }
  .promo-card.zoom, .next-rank.pop { animation: none; }
}
