/* The shipped typefaces.
 *
 * Self-hosted, not fetched. The themes name faces the app never loaded — Inter, VT323, JetBrains
 * Mono — so all 44 fell back to the same system sans and every page looked the same. A Google
 * Fonts stylesheet would fix that and break two things Mynah cares about more: it is unreachable
 * from mainland China, and it is a network request in an app built to run offline.
 *
 * The pair was chosen on COVERAGE, not taste: pinyin sets ǐ 5,398 times and ǒ 6,620 in this app's
 * own data, and most rounded display faces — Fredoka, Nunito, Comfortaa, Rubik, Varela Round —
 * simply do not draw them. A missing glyph fails silently, swapping one letter into the fallback
 * face, so wǒ comes out with its o in a different typeface. Baloo 2 and M PLUS Rounded 1c are
 * complete. Both are SIL OFL 1.1, with the licences beside them in web/fonts/.
 *
 * `swap` because a face that has not arrived must never hide the text: the system fallback shows
 * immediately and is replaced when the file lands. On a cold load that is a flash; a blank page
 * would be worse.
 */
@font-face {
  font-family: 'Baloo 2';
  src: url('fonts/baloo2-var.woff2') format('woff2-variations');
  font-weight: 400 800;
  font-display: swap;
}
@font-face {
  font-family: 'MPLUS Rounded';
  src: url('fonts/mplus-rounded-400.woff2') format('woff2');
  font-weight: 400;
  font-display: swap;
}
@font-face {
  font-family: 'MPLUS Rounded';
  src: url('fonts/mplus-rounded-700.woff2') format('woff2');
  font-weight: 700;
  font-display: swap;
}

/* Mynah — one stylesheet, no framework.

   Colour comes from the Lumina design system (themes.css): every theme re-skins one set of
   custom properties, and this file consumes them through the alias layer below rather than
   defining a second palette that would have to be kept in sync. Swapping data-theme on <html>
   reskins the entire app.

   Tone colours are the exception and stay Mynah's own. They carry meaning — a first tone must
   not be mistaken for a fourth — so they cannot inherit a theme's accent and must stay mutually
   distinguishable on all 44 themes. They are chosen per light/dark instead, keyed off data-mode,
   which app.ts sets from the active theme's resolved color-scheme. */

:root {
  /* — Alias layer: Mynah's semantic names, resolved from whichever Lumina theme is active. — */
  --bg-raised: var(--card-bg, var(--bg-2));
  --bg-sunken: var(--bg-3);
  --line: var(--border);
  --line-strong: var(--border-2);
  --ink: var(--text);
  --ink-soft: var(--text-2);
  --ink-faint: var(--text-3);
  /* NOT named --accent: that is a Lumina token every theme defines, and this stylesheet loads
     after themes.css at equal specificity, so an alias by that name silently overwrote each
     theme's secondary colour app-wide. These carry Mynah's own prefix and leave the token alone. */
  --ui-accent: var(--primary);
  --ui-accent-ink: var(--on-primary);
  --ui-accent-soft: color-mix(in srgb, var(--primary) 13%, var(--bg-2));
  --good: var(--success);
  --warn: var(--warning);

  --han: 'PingFang SC', 'Hiragino Sans GB', 'Noto Sans CJK SC', 'Source Han Sans SC',
    'Microsoft YaHei', system-ui, sans-serif;
  /* Nunito for reading, Fredoka for anything large and loud. The theme's own token stays in the
     stack behind them, so a theme that one day ships its own face still gets it. */
  /* A panel you can see through. Every surface that sits on the page uses this rather than a solid
     fill, so the background blooms carry on behind the content instead of stopping at its edge —
     the glass on the home tiles is the same idea, just always on. Paired with a blur, because a
     translucent panel over a busy backdrop is only readable if the backdrop is softened. */
  --panel: color-mix(in srgb, var(--bg-raised) 62%, transparent);
  --panel-blur: blur(14px) saturate(1.3);

  --ui: 'MPLUS Rounded', var(--font-body);
  --display: 'Baloo 2', var(--font-head, var(--font-body));
  --mono: var(--font-mono);
}

/* Tone palettes. data-mode is set by the app from the theme's own color-scheme, because CSS
   cannot select on that property. Without the attribute the light set applies, which is the
   safe default on an unstyled page. */
:root,
[data-mode='light'] {
  --tone-1: #c0392b;
  --tone-2: #b4780c;
  --tone-3: #1f7a4d;
  --tone-4: #2f5ec4;
  --tone-5: #7a7a88;
}

[data-mode='dark'] {
  --tone-1: #ff7a6b;
  --tone-2: #ffc457;
  --tone-3: #5fd39a;
  --tone-4: #7aa8ff;
  --tone-5: #9a9ab0;
}

* { box-sizing: border-box; }

html, body { height: 100%; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--ui);
  font-size: 15px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  display: flex;
  flex-direction: column;
}

h1, h2, h3 { line-height: 1.25; margin: 0; letter-spacing: -0.011em; }

button, input, select { font: inherit; color: inherit; }

/* ---------- top bar ---------- */

.topbar {
  display: flex;
  align-items: center;
  gap: 20px;
  padding: 0 16px;
  height: 54px;
  background: color-mix(in srgb, var(--bg-raised) 78%, transparent);
  backdrop-filter: saturate(1.7) blur(16px);
  /* A ribbon of the theme's own colours instead of a grey hairline. Every hue comes from a token,
     so all 44 themes draw their own stripe rather than one borrowed palette. */
  border-bottom: 0;
  box-shadow: 0 1px 0 color-mix(in srgb, var(--ink) 8%, transparent);
  position: sticky;
  top: 0;
  z-index: 20;
  padding-top: env(safe-area-inset-top);
}

.topbar::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 3px;
  background: linear-gradient(
    90deg,
    var(--primary) 0%,
    var(--info) 22%,
    var(--success) 44%,
    var(--warning) 66%,
    var(--danger) 88%,
    var(--accent) 100%
  );
  opacity: 0.9;
}

.brand { display: flex; align-items: center; gap: 9px; text-decoration: none; color: var(--ink); }
.brand-mark {
  width: 26px;
  height: 26px;
  fill: var(--ui-accent);
  transition: transform 0.35s cubic-bezier(0.2, 1.5, 0.4, 1);
}
/* The bird tips its head when you reach for it. Small, and the only animation in the bar. */
.brand:hover .brand-mark { transform: rotate(-12deg) scale(1.08); }
.brand-eye { fill: var(--bg-raised); }
.brand-text {
  font-family: var(--display);
  font-weight: 700;
  letter-spacing: -0.01em;
  font-size: 19px;
}

.tabs a:hover { background: var(--bg-sunken); color: var(--ink); }
/* Each tab lights in its own colour, so the bar is a row of little coloured things rather than
   eight identical grey glyphs. */
.tabs a[data-route='home'][aria-current='page'] { color: var(--primary); }
.tabs a[data-route='decks'][aria-current='page'] { color: var(--primary); }
.tabs a[data-route='stories'][aria-current='page'] { color: var(--warning); }
.tabs a[data-route='ask'][aria-current='page'] { color: var(--info); }
.tabs a[data-route='drills'][aria-current='page'] { color: var(--accent); }
.tabs a[data-route='test'][aria-current='page'] { color: var(--danger); }
.tabs a[data-route='pronounce'][aria-current='page'] { color: var(--success); }
.tabs a[data-route='progress'][aria-current='page'] { color: var(--info); }
.tabs a[aria-current='page'] { background: color-mix(in srgb, currentColor 14%, transparent); }

/* No longer scrolls. It had to when the tabs were words — "Pronunciation" alone was wider than four
   icons — but eight glyphs fit a 375px phone with room over, and `overflow` would clip the tooltips
   that now hang below the bar. */
.tabs {
  display: flex;
  gap: 2px;
  margin-left: auto;
}
.tabs a {
  display: grid;
  place-items: center;
  width: 36px;
  height: 36px;
  border-radius: 11px;
  text-decoration: none;
  color: var(--ink-soft);
  transition: background-color 0.14s ease, color 0.14s ease;
}
.tabs a .deck-mark { width: 21px; height: 21px; }
.tabs a:hover { background: var(--bg-sunken); color: var(--ink); }

.icon-btn {
  display: grid;
  place-items: center;
  width: 34px; height: 34px;
  border: 1px solid transparent;
  border-radius: 9px;
  background: none;
  cursor: pointer;
  color: var(--ink-soft);
}
.icon-btn:hover { background: var(--bg-sunken); color: var(--ink); border-color: var(--line); }
.icon-btn svg { width: 18px; height: 18px; fill: currentColor; }

/* ---------- layout ---------- */

.view { flex: 1; width: min(920px, 100%); margin: 0 auto; padding: 24px 16px 64px; }
.view.study { display: flex; flex-direction: column; padding-bottom: 24px; }
.loading { color: var(--ink-faint); padding: 40px 0; text-align: center; }

.page-head { margin-bottom: 20px; }
.page-head h1 { font-size: 24px; font-weight: 640; }
.page-head p { color: var(--ink-soft); margin: 6px 0 0; max-width: 62ch; }

.section { margin-top: 34px; }
.section > h2 {
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--ink-faint);
  font-weight: 640;
  margin-bottom: 12px;
}

/* ---------- deck grid ---------- */

/* Roomier than a flat grid needs, because each tile now casts two card edges below and to the
   right and they must not run into the next tile. */
.decks { display: grid; gap: 22px 26px; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); }

.deck {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 15px;
  background: var(--panel);
  backdrop-filter: var(--panel-blur);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  text-decoration: none;
  color: inherit;
  cursor: pointer;
  text-align: left;
  width: 100%;
  /* The stack is drawn with box-shadows, so the tile cannot clip its own overflow — the sheen is
     kept inside by a radius-matched mask instead. */
  transition:
    border-color 0.22s ease,
    transform 0.22s cubic-bezier(0.2, 0.8, 0.3, 1),
    box-shadow 0.22s ease,
    background-color 0.22s ease,
    backdrop-filter 0.22s ease;
  /* A deck IS a stack of cards, so the tile says so: two edges peeking out behind it. Drawn with
     solid box-shadows rather than pseudo-elements — they inherit the border radius for free and
     cannot land in front of the content, and they read from the same surface and line tokens as
     the tile, so all 44 themes restate the stack in their own colours. */
  /* The stack edges stay SOLID even though the tile is see-through: they are meant to read as
     further cards underneath, and a translucent card behind a translucent card is just a smudge. */
  box-shadow:
    5px 5px 0 -1px var(--card-bg, var(--bg-raised)),
    5px 5px 0 0 var(--line-strong),
    10px 10px 0 -1px var(--card-bg, var(--bg-raised)),
    10px 10px 0 0 var(--line-strong);
}
.deck:hover {
  /* The stack keeps its colour from the deck's own hue now, not the one accent — so a tile lifts
     in the colour of its own mark. */
  border-color: color-mix(in srgb, currentColor 45%, var(--line-strong));
  transform: translateY(-3px);
  background: color-mix(in srgb, var(--bg-raised) 72%, transparent);
  backdrop-filter: blur(8px) saturate(1.35);
  /* The whole stack lifts and spreads a little, rather than the top card sliding off it — and a
     coloured glow sits under all of it. */
  box-shadow:
    inset 0 1px 0 color-mix(in srgb, #fff 40%, transparent),
    6px 6px 0 -1px var(--card-bg, var(--bg-raised)),
    6px 6px 0 0 currentColor,
    12px 12px 0 -1px var(--card-bg, var(--bg-raised)),
    12px 12px 0 0 color-mix(in srgb, currentColor 55%, var(--line-strong)),
    0 18px 38px -20px currentColor;
}
/* The sheen is a large rotated strip that hangs well outside the tile, so the TILE has to clip it.
   `clip-path` on the pseudo-element does not: inset(0) there means "all of my own box", and its own
   box is the oversized strip — so it clipped nothing and the gleam swept across the whole grid.
   `overflow` on the parent is the right tool, and it leaves the stack alone: a box-shadow is
   painted outside the border box and is not clipped by the element's own overflow. */
.deck, .quiz-opt { overflow: hidden; }
/* The mark leans in with the tile. */
.deck:hover .deck-mark, .deck:hover .story-badge { transform: scale(1.1) rotate(-3deg); }

@media (prefers-reduced-motion: reduce) {
  .deck { transition: none; }
}
.deck-body { min-width: 0; flex: 1; }
.deck-name { font-weight: 600; display: flex; align-items: center; gap: 8px; }
.deck-blurb { color: var(--ink-soft); font-size: 13px; margin-top: 2px; }
.deck-stats { color: var(--ink-faint); font-size: 12.5px; margin-top: 7px; font-variant-numeric: tabular-nums; }
.deck-stats .due { color: var(--ui-accent); font-weight: 600; }

.ring { width: 46px; height: 46px; flex: none; transform: rotate(-90deg); }
.ring circle { fill: none; stroke-width: 4; }
.ring .track { stroke: var(--bg-sunken); }
.ring .fill { stroke: var(--good); stroke-linecap: round; transition: stroke-dashoffset 0.4s; }
.ring-wrap { position: relative; display: grid; place-items: center; }
.ring-pct {
  position: absolute;
  font-size: 11px;
  font-weight: 640;
  font-variant-numeric: tabular-nums;
  color: var(--ink-soft);
}

/* ---------- study ---------- */

.study-bar {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 16px;
  flex-wrap: wrap;
}
.study-bar .back { color: var(--ink-soft); text-decoration: none; font-size: 14px; font-weight: 500; }
.study-bar .back:hover { color: var(--ink); }
.study-bar .deck-title { font-weight: var(--fw-bold, 650); letter-spacing: -0.01em; }
.study-bar .spacer { flex: 1; }

.seg { display: flex; background: var(--bg-sunken); border-radius: var(--radius-sm); padding: 2px; gap: 2px; }
.seg button {
  border: 0;
  background: none;
  padding: 5px 11px;
  border-radius: calc(var(--radius-sm) - 2px);
  font-size: 13px;
  cursor: pointer;
  color: var(--ink-soft);
  white-space: nowrap;
}
.seg button[aria-pressed='true'] { background: var(--bg-raised); color: var(--ink); font-weight: 600; box-shadow: var(--shadow-sm); }

.progress-line { height: 3px; background: var(--bg-sunken); border-radius: 2px; overflow: hidden; margin-bottom: 18px; }
.progress-line i { display: block; height: 100%; background: var(--ui-accent); transition: width 0.3s; }

/* --- the card itself: the characters sit in a tinted well, with the metadata on a footer rail.

   Two faces on one 3D-rotated element rather than a crossfade, because the flip is the app's one
   piece of theatre and a fake one reads as cheap. The stage owns the height so the card cannot
   resize mid-rotation, and both faces are absolutely positioned inside it. */

/* A CARD, not a banner.
 *
 * The stage used to be as wide as the content column with only a min-height, so on a wide screen a
 * two-character word sat marooned in the middle of a letterbox. Real proportions and a ceiling on
 * the width make it read as a thing you could pick up — which is what the flip is pretending it is.
 *
 * The lift lives HERE rather than on .card-3d. That element carries the rotateY the flip animates,
 * and a hover transform on it would fight the rotation — the bug MYN-8 already fixed once.
 */
.card-stage {
  perspective: 1600px;
  position: relative;
  width: min(100%, 30rem);
  margin: 0 auto 20px;
  transition: transform 0.3s cubic-bezier(0.2, 0.8, 0.3, 1);
}
.card-stage:hover { transform: translateY(-5px); }

/* The glow and the sheen sit on the FACE, so they rotate with it and never show through the back. */
.card-stage:hover .face {
  box-shadow:
    inset 0 1px 0 color-mix(in srgb, #fff 45%, transparent),
    0 26px 54px -26px var(--tone-accent, var(--ui-accent)),
    var(--card-shadow, var(--shadow));
  border-color: color-mix(in srgb, var(--tone-accent, var(--ui-accent)) 42%, var(--line));
}

.face::after {
  content: '';
  position: absolute;
  top: -60%;
  bottom: -60%;
  left: -75%;
  width: 45%;
  pointer-events: none;
  z-index: 3;
  opacity: 0;
  transform: translateX(0) rotate(18deg);
  background: linear-gradient(90deg, transparent, color-mix(in srgb, #fff 38%, transparent), transparent);
}
.card-stage:hover .face::after {
  opacity: 1;
  transform: translateX(420%) rotate(18deg);
  transition: transform 0.9s cubic-bezier(0.25, 0.6, 0.3, 1), opacity 0.2s ease;
}

@media (prefers-reduced-motion: reduce) {
  .card-stage { transition: none; }
  .card-stage:hover { transform: none; }
  .face::after { display: none; }
}

.card-3d {
  /* Both faces share one grid cell, so the card is as tall as the taller of the two and never
     scrolls inside itself. Absolutely positioning them was what forced the well to scroll: the
     stage's aspect ratio fixed the height, so an answer with a picture and a hint under it had
     nowhere to go. The ratio is still what shapes a card — it is just a floor now, not a ceiling,
     and because BOTH faces size the cell the card does not change height mid-flip. */
  position: relative;
  display: grid;
  /* The card shape lives here rather than on the stage, so the stage hugs the card instead of
     leaving a pocket of empty space under a short one. A long sentence must still fit: the ratio
     gives way before the content does. */
  aspect-ratio: 5 / 6;
  min-height: clamp(320px, 46vh, 440px);
  transform-style: preserve-3d;
  transition: transform 620ms cubic-bezier(0.2, 0.8, 0.25, 1);
  cursor: pointer;
  padding: 0;
  border: 0;
  background: none;
  text-align: inherit;
  font: inherit;
  color: inherit;
}
.card-3d.flipped { transform: rotateY(180deg); }
.card-3d:focus-visible { outline: none; }
.card-3d:focus-visible .face {
  box-shadow: 0 0 0 3px var(--ring, color-mix(in srgb, var(--ui-accent) 50%, transparent)),
    var(--card-shadow, var(--shadow));
}

.face {
  grid-area: 1 / 1;
  /* The containing block for the corner toggle. Without it the toggle anchors to the stage, which
     is the same box today and would stop being so the moment the stage grew any padding. */
  position: relative;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  border-radius: var(--card-radius, var(--radius-lg, 18px));
  border: var(--card-border, 1px solid var(--line));
  background: var(--card-bg, var(--panel));
  backdrop-filter: var(--panel-blur);
  box-shadow: var(--card-shadow, var(--shadow));
  transition: box-shadow 0.3s ease, border-color 0.3s ease;
}
.face.back { transform: rotateY(180deg); }

/* The well holds whatever the card is actually asking or answering. It is tinted by the word's
   own leading tone, so the card stays quietly colour-coded to what it teaches without tinting
   the characters themselves. */
.well {
  flex: 1;
  min-height: 0;
  margin: 13px 13px 0;
  padding: 20px 22px;
  border-radius: var(--radius);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 13px;
  text-align: center;
  background: color-mix(in srgb, var(--tone-accent, var(--ui-accent)) 10%, var(--bg-sunken));
  border: 1px solid color-mix(in srgb, var(--tone-accent, var(--ui-accent)) 24%, transparent);
}

/* A plain band under the well for the meaning, so it is never mistaken for part of the prompt. */
.card-body {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 9px;
  padding: 14px 20px 2px;
  text-align: center;
}

/* The rail: everything about the word that is not the word. */
.rail {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px 16px;
  margin-top: 12px;
  border-top: 1px solid var(--line);
  flex-wrap: wrap;
}
.rail .spacer { flex: 1; }
.rail .rail-han { font-family: var(--han); font-size: 21px; letter-spacing: 0.03em; }
.rail .rail-pin { font-size: 14.5px; color: var(--ink-soft); font-weight: 560; }

.card-meta { display: flex; gap: 7px; align-items: center; flex-wrap: wrap; }
.chip {
  padding: 2px 9px;
  border-radius: var(--radius-pill, 999px);
  background: var(--bg-sunken);
  border: 1px solid var(--line);
  font-size: 11px;
  font-weight: 600;
  color: var(--ink-soft);
  white-space: nowrap;
}

.hanzi {
  font-family: var(--han);
  font-size: clamp(58px, 15vw, 108px);
  line-height: 1.04;
  font-weight: 500;
  letter-spacing: 0.03em;
  color: var(--ink);
}
.face.back .hanzi { font-size: clamp(34px, 8.4vw, 58px); }
.hanzi-trad { font-family: var(--han); font-size: 17px; color: var(--ink-faint); letter-spacing: 0.03em; }

/* The reading and its sound are one thing. A captioned button on its own line read as a separate
   task to perform; a speaker sitting beside the pinyin reads as "this is how it sounds". */
.reading {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  flex-wrap: wrap;
}

.pinyin {
  font-size: 25px;
  font-weight: 560;
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  justify-content: center;
}
.pinyin .syl[data-tone='1'] { color: var(--tone-1); }
.pinyin .syl[data-tone='2'] { color: var(--tone-2); }
.pinyin .syl[data-tone='3'] { color: var(--tone-3); }
.pinyin .syl[data-tone='4'] { color: var(--tone-4); }
.pinyin .syl[data-tone='5'] { color: var(--tone-5); }
.pinyin.hidden-pinyin { filter: blur(8px); opacity: 0.5; user-select: none; }

/* The meaning is the ANSWER — it should be the loudest thing on this face, not a caption under the
   characters. It was 21px, the same weight as a paragraph. */
.gloss {
  font-family: var(--display);
  font-size: clamp(25px, 4.1vw, 35px);
  font-weight: 650;
  letter-spacing: -0.02em;
  max-width: 24ch;
  line-height: 1.15;
  color: var(--ink);
}
.gloss-hero { font-size: clamp(22px, 4.2vw, 32px); font-weight: 600; max-width: 22ch; line-height: 1.28; }
/* The other senses stay small: they are what else it can mean, not the answer. */
.gloss .extra, .extra {
  display: block;
  color: var(--ink-soft);
  font-family: var(--ui);
  font-size: 14.5px;
  font-weight: 400;
  letter-spacing: 0;
  margin-top: 8px;
  line-height: 1.5;
}

/* Per-syllable tone breakdown: the number, the syllable, and its pitch shape. */
.tone-row { display: flex; gap: 9px; flex-wrap: wrap; justify-content: center; }
.tone-pill {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 5px 11px 5px 6px;
  border-radius: var(--radius-pill, 999px);
  background: color-mix(in srgb, var(--pill-tone) 15%, var(--bg-raised));
  border: 1px solid color-mix(in srgb, var(--pill-tone) 42%, transparent);
}
.tone-pill b { font-size: 15px; font-weight: 620; color: var(--pill-tone); }
.tone-pill .n {
  display: grid; place-items: center;
  width: 19px; height: 19px;
  border-radius: 50%;
  background: var(--pill-tone);
  color: var(--card-bg, var(--bg-raised));
  font-size: 11px;
  font-weight: 700;
}
.tone-pill svg { width: 24px; height: 12px; }
.tone-pill svg path { fill: none; stroke: var(--pill-tone); stroke-width: 2.5; stroke-linecap: round; }

.sandhi-note {
  font-size: 12.5px;
  color: var(--warn);
  background: color-mix(in srgb, var(--warn) 13%, transparent);
  border: 1px solid color-mix(in srgb, var(--warn) 30%, transparent);
  padding: 5px 12px;
  border-radius: var(--radius-pill, 999px);
  max-width: 48ch;
}

.prompt-hint { color: var(--ink-faint); font-size: 13.5px; }

.speak {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  border: 1px solid var(--line);
  background: var(--bg-raised);
  color: var(--ink-soft);
  border-radius: var(--radius-pill, 999px);
  padding: 7px 15px;
  cursor: pointer;
  font-size: 14px;
}
.speak:hover { border-color: var(--ui-accent); color: var(--ui-accent); }
.speak svg { width: 15px; height: 15px; fill: currentColor; }
.speak[disabled] { opacity: 0.45; cursor: not-allowed; }
/* The speaker now sits inside the well under the pinyin, so it needs to read as secondary to the
   characters above it rather than competing with them. */
.well .speak { margin-top: 2px; }
.speak.big { padding: 17px 28px; font-size: 16px; font-weight: 600; }
.speak.big svg { width: 21px; height: 21px; }

/* --- card styles.

   All four share ONE markup shape (well · body · rail) and differ only in skin, so a variant
   cannot rot from disuse and the card logic never branches on which is selected. Framed is the
   base above; the three below re-dress it. */

/* Editorial — centred and airy, with a tone-coloured hairline across the top. */
[data-card='editorial'] .well {
  margin: 0;
  padding: 34px 26px 6px;
  background: none;
  border: 0;
  border-radius: 0;
  gap: 15px;
}
[data-card='editorial'] .face { position: absolute; }
[data-card='editorial'] .face::before {
  content: '';
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 3px;
  background: linear-gradient(90deg, transparent, var(--tone-accent, var(--ui-accent)), transparent);
}
[data-card='editorial'] .rail {
  border-top: 0;
  justify-content: center;
  margin-top: 0;
  padding-bottom: 18px;
}
[data-card='editorial'] .rail .spacer { display: none; }
[data-card='editorial'] .card-body { padding-top: 6px; }

/* Split — a saturated block of the theme's own brand colour over a clean detail panel. */
[data-card='split'] .well {
  margin: 0;
  border-radius: 0;
  border: 0;
  border-bottom: 3px solid var(--tone-accent, var(--ui-accent));
  padding: 26px 24px;
  color: var(--on-primary);
  background: linear-gradient(
    145deg,
    var(--primary),
    color-mix(in srgb, var(--primary) 58%, var(--ui-accent))
  );
}
[data-card='split'] .well .hanzi,
[data-card='split'] .well .gloss,
[data-card='split'] .well .prompt-hint,
[data-card='split'] .well .hanzi-trad { color: inherit; }
[data-card='split'] .well .prompt-hint { opacity: 0.72; }
/* Tone colours are illegible on a saturated block, so pinyin there reads plain — the
   tone-coloured breakdown still lives in the pills below, on a neutral surface. */
[data-card='split'] .well .pinyin .syl { color: inherit; opacity: 0.9; }
[data-card='split'] .well .tone-pill {
  background: color-mix(in srgb, var(--on-primary) 14%, transparent);
  border-color: color-mix(in srgb, var(--on-primary) 30%, transparent);
}
[data-card='split'] .well .tone-pill b { color: inherit; }
[data-card='split'] .well .tone-pill .n { background: currentColor; color: var(--primary); }
[data-card='split'] .well .tone-pill svg path { stroke: currentColor; }
[data-card='split'] .well .speak { background: color-mix(in srgb, var(--on-primary) 12%, transparent); border-color: color-mix(in srgb, var(--on-primary) 30%, transparent); color: inherit; }
[data-card='split'] .card-body { padding-top: 16px; }

/* Minimal — hairline rules, very large characters, small-caps labels. */
[data-card='minimal'] .face { border-radius: 6px; box-shadow: none; }
[data-card='minimal'] .well {
  margin: 0;
  padding: 34px 26px 10px;
  background: none;
  border: 0;
  border-radius: 0;
  gap: 16px;
}
[data-card='minimal'] .hanzi { font-weight: 400; letter-spacing: 0.05em; }
[data-card='minimal'] .card-body { padding-top: 12px; }
[data-card='minimal'] .card-body::before {
  content: '';
  width: 34px;
  height: 1px;
  background: var(--line-strong);
  margin-bottom: 10px;
}
[data-card='minimal'] .rail {
  justify-content: center;
  padding-bottom: 18px;
}
[data-card='minimal'] .rail .spacer { display: none; }
[data-card='minimal'] .rail .chip {
  background: none;
  border: 0;
  padding: 0 6px;
  text-transform: uppercase;
  letter-spacing: 0.16em;
  font-size: 10px;
  color: var(--ink-faint);
}
[data-card='minimal'] .rail .prompt-hint {
  text-transform: uppercase;
  letter-spacing: 0.14em;
  font-size: 10px;
}

/* --- advancing.

   There were four grade buttons here once, and then there were none: rating your own recall on a
   four-point scale is a second decision taken while you are still thinking about the word, and it
   is the part people found confusing. The row now says the one thing that is true — mark it
   Learned and it stops coming up, otherwise carry on and it will be back. */

.answer-actions { display: flex; flex-direction: column; gap: 12px; }

.action-head {
  display: flex;
  align-items: baseline;
  gap: 10px;
  justify-content: center;
  color: var(--ink-soft);
  font-size: 13.5px;
}
.action-head b { color: var(--ink); font-size: 14.5px; font-weight: 620; }

.primary-row { display: flex; gap: 10px; }
.reveal {
  flex: 1;
  padding: 15px;
  border-radius: var(--radius);
  border: 1px solid var(--ui-accent);
  background: var(--ui-accent);
  color: var(--ui-accent-ink);
  font-weight: 620;
  font-size: 15px;
  cursor: pointer;
  box-shadow: var(--shadow-sm);
}
.reveal:hover { filter: brightness(1.07); }
.skip {
  padding: 15px 18px;
  border-radius: var(--radius);
  border: 1px solid var(--line-strong);
  background: var(--bg-raised);
  color: var(--ink-soft);
  font-weight: 550;
  font-size: 14.5px;
  cursor: pointer;
  white-space: nowrap;
}
.skip:hover { border-color: var(--ink-faint); color: var(--ink); }

.done-panel { text-align: center; padding: 48px 16px; }
.done-panel h2 { font-size: 22px; margin-bottom: 8px; }
.done-panel p { color: var(--ink-soft); max-width: 48ch; margin: 0 auto 20px; }

/* ---------- pronunciation ---------- */

.tone-grid { display: grid; gap: 12px; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); }
.tone-card {
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 16px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.tone-card h3 { font-size: 14.5px; }
.tone-card[data-tone='1'] { border-left: 3px solid var(--tone-1); }
.tone-card[data-tone='2'] { border-left: 3px solid var(--tone-2); }
.tone-card[data-tone='3'] { border-left: 3px solid var(--tone-3); }
.tone-card[data-tone='4'] { border-left: 3px solid var(--tone-4); }
.tone-card[data-tone='5'] { border-left: 3px solid var(--tone-5); }
.tone-card p { margin: 0; font-size: 13.5px; color: var(--ink-soft); }
.tone-card .cue { color: var(--ink); }
.tone-ex { display: flex; align-items: center; gap: 10px; margin-top: auto; padding-top: 8px; }
.tone-ex .h { font-family: var(--han); font-size: 30px; }
.tone-ex .p { font-size: 17px; font-weight: 600; }
.tone-ex .m { color: var(--ink-faint); font-size: 13px; }

.contour { width: 100%; height: 34px; }
.contour path { fill: none; stroke-width: 2.5; stroke-linecap: round; }

.sound-table { display: flex; flex-direction: column; gap: 22px; }
.sound-group h3 { font-size: 15px; margin-bottom: 4px; }
.sound-group > p { color: var(--ink-soft); font-size: 13.5px; margin: 0 0 10px; max-width: 68ch; }
.rows { border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; background: var(--bg-raised); }
.row {
  display: grid;
  grid-template-columns: 66px 130px 1fr 40px;
  gap: 12px;
  align-items: center;
  padding: 11px 14px;
  border-top: 1px solid var(--line);
  text-align: left;
  width: 100%;
  background: none;
  border-left: 0; border-right: 0; border-bottom: 0;
  cursor: pointer;
  color: inherit;
}
.row:first-child { border-top: 0; }
.row:hover { background: var(--bg-sunken); }
.row .sym { font-size: 19px; font-weight: 640; font-family: var(--mono); }
.row .ex { display: flex; align-items: baseline; gap: 7px; }
.row .ex .h { font-family: var(--han); font-size: 19px; }
.row .ex .p { font-weight: 600; }
.row .note { font-size: 13.5px; color: var(--ink-soft); }
.row .note .warn { display: block; color: var(--warn); font-size: 12.5px; margin-top: 2px; }
.row .play { color: var(--ink-faint); justify-self: end; }
.row .play svg { width: 16px; height: 16px; fill: currentColor; }
.row:hover .play { color: var(--ui-accent); }

.rule { background: var(--bg-raised); border: 1px solid var(--line); border-radius: var(--radius); padding: 16px; margin-bottom: 12px; }
.rule h3 { font-size: 15px; margin-bottom: 6px; }
.rule .what { margin: 0 0 8px; font-size: 14px; }
.rule .why { margin: 0 0 12px; font-size: 13.5px; color: var(--ink-soft); font-style: italic; }
.rule-ex { display: flex; flex-wrap: wrap; gap: 8px; }
.rule-ex button {
  display: flex; align-items: baseline; gap: 8px;
  border: 1px solid var(--line); background: var(--bg-sunken);
  border-radius: var(--radius-sm); padding: 8px 12px; cursor: pointer; text-align: left;
}
.rule-ex button:hover { border-color: var(--ui-accent); }
.rule-ex .h { font-family: var(--han); font-size: 18px; }
.rule-ex .arrow { color: var(--ink-faint); }
.rule-ex .was { color: var(--ink-faint); text-decoration: line-through; font-size: 13px; }
.rule-ex .now { color: var(--ui-accent); font-weight: 600; font-size: 14px; }
.rule-ex .m { color: var(--ink-faint); font-size: 12.5px; }

/* tone drill */
.drill { background: var(--bg-raised); border: 1px solid var(--line); border-radius: var(--radius); padding: 24px; text-align: center; }
.drill .syl { font-size: 40px; font-weight: 600; margin: 10px 0 4px; }
.drill .han { font-family: var(--han); font-size: 26px; color: var(--ink-faint); }
.drill-opts { display: grid; grid-template-columns: repeat(5, 1fr); gap: 8px; margin-top: 20px; }
.drill-opts button { padding: 14px 4px; border-radius: var(--radius-sm); border: 1px solid var(--line); background: var(--bg-sunken); cursor: pointer; font-weight: 600; }
.drill-opts button:hover { border-color: var(--ui-accent); }
.drill-opts button.right { background: color-mix(in srgb, var(--good) 20%, transparent); border-color: var(--good); color: var(--good); }
.drill-opts button.wrong { background: color-mix(in srgb, var(--danger) 18%, transparent); border-color: var(--danger); color: var(--danger); }
.drill-score { color: var(--ink-faint); font-size: 13px; font-variant-numeric: tabular-nums; }
.drill-feedback { min-height: 24px; margin-top: 14px; font-size: 14px; }

/* ---------- progress ---------- */

.stat-grid { display: grid; gap: 12px; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
.stat {
  background: var(--panel);
  backdrop-filter: var(--panel-blur);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 15px;
}
.stat b { display: block; font-size: 27px; font-weight: 650; font-variant-numeric: tabular-nums; letter-spacing: -0.02em; }
.stat span { color: var(--ink-faint); font-size: 12.5px; }

.heat { display: flex; gap: 3px; flex-wrap: wrap; }
.heat i { width: 13px; height: 13px; border-radius: 3px; background: var(--bg-sunken); display: block; }
.heat i[data-l='1'] { background: color-mix(in srgb, var(--good) 30%, var(--bg-sunken)); }
.heat i[data-l='2'] { background: color-mix(in srgb, var(--good) 55%, var(--bg-sunken)); }
.heat i[data-l='3'] { background: color-mix(in srgb, var(--good) 78%, var(--bg-sunken)); }
.heat i[data-l='4'] { background: var(--good); }

.table { width: 100%; border-collapse: collapse; font-size: 14px; }
.table th, .table td { text-align: left; padding: 9px 10px; border-bottom: 1px solid var(--line); }
.table th { color: var(--ink-faint); font-weight: 600; font-size: 12px; text-transform: uppercase; letter-spacing: 0.05em; }
.table td.num { text-align: right; font-variant-numeric: tabular-nums; }

/* ---------- controls, sheet, toast ---------- */

.btn {
  border: 1px solid var(--line-strong);
  background: var(--bg-raised);
  border-radius: var(--radius-sm);
  padding: 9px 16px;
  font-weight: 550;
  cursor: pointer;
}
.btn:hover { border-color: var(--ink-faint); }
.btn-primary { background: var(--ui-accent); border-color: var(--ui-accent); color: var(--ui-accent-ink); }
.btn-primary:hover { filter: brightness(1.07); }
.btn-danger { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 40%, var(--line)); }
.btn-danger:hover { background: color-mix(in srgb, var(--danger) 12%, transparent); }

.sheet {
  border: 1px solid var(--line);
  border-radius: 18px;
  padding: 0;
  background: var(--bg-raised);
  color: var(--ink);
  width: min(460px, calc(100vw - 32px));
  box-shadow: var(--shadow);
}
.sheet::backdrop { background: rgb(0 0 0 / 45%); backdrop-filter: blur(2px); }
.sheet-inner { padding: 22px; display: flex; flex-direction: column; gap: 18px; }
.sheet h2 { font-size: 18px; }
.field { display: flex; flex-direction: column; gap: 5px; }
.field-label { font-weight: 600; font-size: 13.5px; }
.field-hint { color: var(--ink-faint); font-size: 12.5px; }
.field input, .field select {
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  padding: 8px 10px;
  background: var(--bg);
}
.check { display: flex; gap: 9px; align-items: flex-start; font-size: 14px; }
.check input { margin-top: 2px; }
.danger { border-top: 1px solid var(--line); padding-top: 16px; }
.danger h3 { font-size: 13.5px; color: var(--danger); margin-bottom: 4px; }
.danger p { color: var(--ink-faint); font-size: 12.5px; margin: 0 0 10px; }
.sheet-actions { display: flex; justify-content: flex-end; }

.toast {
  position: fixed;
  left: 50%;
  bottom: 26px;
  transform: translate(-50%, 14px);
  background: var(--ink);
  color: var(--bg);
  padding: 9px 16px;
  border-radius: 999px;
  font-size: 13.5px;
  font-weight: 500;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s, transform 0.2s;
  z-index: 50;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }

.empty { color: var(--ink-faint); text-align: center; padding: 36px 0; }

@media (max-width: 620px) {
  .tabs a { width: 32px; height: 32px; border-radius: 9px; }
  .tabs a .deck-mark { width: 19px; height: 19px; }
  .brand-text { display: none; }
  .row { grid-template-columns: 52px 1fr 34px; }
  .row .note { grid-column: 1 / -1; }
  .card { padding: 32px 18px; min-height: 40vh; }
}

@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; }
}

/* A notice that something about the environment needs fixing — louder than a plain hint,
   because a silently bad voice reads as a broken app rather than a missing download. */
.notice {
  border: 1px solid color-mix(in srgb, var(--warn) 45%, var(--line));
  background: color-mix(in srgb, var(--warn) 10%, transparent);
  color: var(--ink);
  border-radius: var(--radius-sm);
  padding: 11px 14px;
  font-size: 13.5px;
  margin: 0;
  max-width: 74ch;
}

/* --- sentence cards.

   A sentence is shown word by word — characters above, tone-coloured pinyin below — because the
   spacing IS the teaching. Chinese is written without gaps, so a beginner cannot see where one
   word ends and the next begins; showing the segmentation is half of what makes a sentence
   learnable rather than a wall of characters. */

.sent {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: flex-end;
  gap: 6px 16px;
}
.sw { display: flex; flex-direction: column; align-items: center; gap: 5px; }
/* Punctuation is not a word: no pinyin under it, no tap target, and it sits tight against the word
   before it rather than a full gap away. It also aligns to the TOP — the row bottom-aligns so the
   pinyin lines up, which drops a block carrying no pinyin down onto the pinyin's line. */
.sent .sw.punct { margin-left: -10px; color: var(--ink-soft); align-self: flex-start; }
.sw .h {
  font-family: var(--han);
  font-size: clamp(26px, 5.4vw, 44px);
  line-height: 1.1;
  letter-spacing: 0.03em;
  color: var(--ink);
}
.sw .p { font-size: 14px; font-weight: 560; display: flex; gap: 3px; }
.sw .p .syl[data-tone='1'] { color: var(--tone-1); }
.sw .p .syl[data-tone='2'] { color: var(--tone-2); }
.sw .p .syl[data-tone='3'] { color: var(--tone-3); }
.sw .p .syl[data-tone='4'] { color: var(--tone-4); }
.sw .p .syl[data-tone='5'] { color: var(--tone-5); }
.sw .p.hidden-pinyin { filter: blur(5px); opacity: 0.5; user-select: none; }

/* Long sentences need to breathe more than a single word does. */
.face .well:has(.sent) { gap: 16px; }
[data-card='split'] .well .sw .h,
[data-card='split'] .well .sw .p .syl { color: inherit; }

/* --- deck marks.

   Each deck's drawn mark sits inside its progress ring, so one element says both what the deck is
   and how far through it you are. The mark's colour comes from a THEME token via `color` on the
   wrapper, and the SVG uses only currentColor — so all 44 themes restate the deck list in their
   own palette instead of fighting a hardcoded one. */

.ring-wrap {
  position: relative;
  display: grid;
  place-items: center;
  width: 52px;
  height: 52px;
  flex: none;
  /* A faint disc behind the mark so it reads as a considered badge rather than a floating glyph. */
  background: color-mix(in srgb, currentColor 9%, transparent);
  border-radius: 50%;
}
.ring-wrap .ring { position: absolute; inset: -3px; width: calc(100% + 6px); height: calc(100% + 6px); }
.ring-wrap .ring .fill { stroke: currentColor; }
.ring-wrap .ring .track { stroke: color-mix(in srgb, currentColor 15%, transparent); }
.deck-mark {
  width: 27px;
  height: 27px;
  position: relative;
  color: inherit;
}
.deck:hover .deck-mark { transform: scale(1.06); }
.deck-mark { transition: transform 0.15s ease; }

@media (prefers-reduced-motion: reduce) {
  .deck-mark { transition: none; }
}

/* --- moving through a session.

   Position is shown between the two controls, not off to one side: half of feeling stuck on a
   card is not knowing where you are in the deck. */

/* The card between the two ways out of it. `align-items: center` puts the arrows against the
   middle of the card rather than the middle of the column, which includes the counter. */
.card-row {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 14px;
  margin: 0 0 14px;
}
.card-column { flex: 0 1 30rem; min-width: 0; display: grid; justify-items: center; }
.card-column .card-stage { margin-bottom: 8px; }

/* Sized as real controls. These are the primary way through a session — quiet ghost pills at the
   edges is what made the session feel stuck in the first place, and moving them beside the card is
   not a licence to make them faint again. */
.card-arrow {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 52px;
  height: 52px;
  border: 1px solid var(--line-strong);
  border-radius: 50%;
  background: var(--bg-raised);
  color: var(--ink);
  cursor: pointer;
  box-shadow: var(--shadow-sm);
  transition: background-color 0.14s ease, border-color 0.14s ease, transform 0.12s ease,
    box-shadow 0.14s ease;
}
.card-arrow:hover:not([disabled]) {
  border-color: var(--ui-accent);
  color: var(--ui-accent);
  transform: scale(1.06);
}
.card-arrow:active:not([disabled]) { transform: scale(0.96); }
.card-arrow[disabled] { opacity: 0.32; cursor: not-allowed; box-shadow: none; }
/* One glyph, mirrored, so the two arrows are exactly each other. */
.card-arrow:not(.back) svg { transform: rotate(180deg); }
.card-arrow:focus-visible {
  outline: 2px solid var(--ring, var(--ui-accent));
  outline-offset: 2px;
}

/* No room beside the card on a phone: smaller arrows, and the card takes what is left. */
@media (max-width: 620px) {
  .card-row { gap: 6px; }
  .card-arrow { width: 40px; height: 40px; }
  .card-arrow svg { width: 18px; height: 18px; }
}

.nav-pos {
  color: var(--ink-soft);
  font-size: 13.5px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.01em;
}

@media (prefers-reduced-motion: reduce) {
  .card-arrow { transition: none; }
}

/* A toggle that is ON should look it — the study bar's other buttons are actions, this one is a
   state, and a session behaves differently while it is set. */
.btn.toggle.on {
  background: var(--ui-accent-soft);
  border-color: var(--ui-accent);
  color: var(--ui-accent);
  font-weight: 620;
}

/* A word in a sentence the learner has not learned yet. Understated on purpose: it should say
   "this is the new one" without competing with the tone colours, which carry meaning. */
.sw.new .h {
  text-decoration: underline;
  text-decoration-style: dotted;
  text-decoration-thickness: 2px;
  text-underline-offset: 5px;
  text-decoration-color: color-mix(in srgb, var(--ui-accent) 65%, transparent);
}

/* --- tapping a word in a sentence.

   The explanation appears inline under the sentence rather than as a floating popover: the card
   rotates in 3D, and anchored overlays inside a transformed element are a positioning fight with
   nothing to gain on a phone, where an inline strip is easier to hit and read anyway. */

.sw[role='button'] { cursor: pointer; border-radius: 8px; padding: 2px 4px; margin: -2px -4px; }
.sw[role='button']:hover { background: color-mix(in srgb, var(--ui-accent) 12%, transparent); }
.sw[role='button']:focus-visible {
  outline: 2px solid var(--ui-accent);
  outline-offset: 1px;
}
/* A flash of acknowledgement, not a state: it fades over a second so it cannot linger and compete
   with the dotted underline, which means something. */
.sw.tapped { animation: sw-tap 1s ease-out forwards; }
@keyframes sw-tap {
  from { background: color-mix(in srgb, var(--ui-accent) 26%, transparent); }
  to { background: transparent; }
}
@media (prefers-reduced-motion: reduce) {
  .sw.tapped { animation: none; }
}

.word-panel {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  justify-content: center;
  padding: 9px 14px;
  border-radius: var(--radius);
  background: var(--bg-raised);
  border: 1px solid var(--line);
  box-shadow: var(--shadow-sm);
  max-width: 100%;
}
/* The meaning is the answer the tap asked for, so it leads and it is the largest thing here. */
.word-panel .wp-gloss {
  font-size: 18px;
  font-weight: 650;
  color: var(--ink);
  max-width: 34ch;
  letter-spacing: -0.01em;
}
/* The characters follow as a small anchor — you can already see them, larger, on the card above. */
.word-panel .wp-han {
  font-family: var(--han);
  font-size: 17px;
  letter-spacing: 0.03em;
  color: var(--ink-soft);
}
.word-panel .speak { padding: 5px 11px; font-size: 13px; }
.speak.icon-only { padding: 6px; width: 30px; height: 30px; justify-content: center; gap: 0; }
.word-panel .wp-close {
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  border: 1px solid var(--line);
  background: none;
  color: var(--ink-faint);
  font-size: 16px;
  line-height: 1;
  cursor: pointer;
}
.word-panel .wp-close:hover { border-color: var(--danger); color: var(--danger); }

/* It arrives on tap and leaves on its own, so it should announce itself rather than blink in. */
.word-panel { animation: wp-in 160ms ease-out; }
@keyframes wp-in {
  from { opacity: 0; transform: translateY(-4px); }
  to { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .word-panel { animation: none; }
}

/* The answer side of a sentence card leads with the English, so the Chinese below it needs room to
   breathe rather than the tight spacing a one-line gloss wanted. */
.card-body.sentence-detail {
  gap: 14px;
  padding-top: 18px;
  padding-bottom: 6px;
}

/* ---------------------------------------------------------------- stories */

/* A story tile is a deck tile — same stack-of-cards surface — with the cover initial in place of
   the progress ring, and the Chinese title under the English one. */
.story-badge {
  flex: none;
  width: 44px;
  height: 44px;
  display: grid;
  place-items: center;
  border-radius: 12px;
  /* The tint comes from the mark's own colour, set inline per story, so a cover carries one hue
     through fill, border and stroke without naming a colour twice. */
  background: color-mix(in srgb, currentColor 13%, var(--bg-raised));
  border: 1px solid color-mix(in srgb, currentColor 28%, transparent);
  transition: transform 0.15s ease;
}
.story-badge .deck-mark { width: 26px; height: 26px; }
.deck:hover .story-badge { transform: scale(1.06); }
.story-title-zh { font-size: 15px; margin-top: 1px; color: var(--ink-soft); }

.back-link {
  display: inline-block;
  margin-bottom: 10px;
  color: var(--ink-soft);
  text-decoration: none;
  font-size: 13.5px;
}
.back-link:hover { color: var(--ink); }

.reader-controls {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
  margin-bottom: 20px;
  /* Stays in reach while the story scrolls under it — the play control is the one thing a reader
     wants at any point, and hunting back up the page for it breaks the reading. */
  position: sticky;
  top: 0;
  z-index: 3;
  padding: 10px 0;
  background: linear-gradient(var(--bg) 78%, transparent);
}
.reader-toggle {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 13.5px;
  color: var(--ink-soft);
  cursor: pointer;
}

.story-body { display: flex; flex-direction: column; gap: 6px; }

.story-line {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 4px 12px;
  align-items: start;
  padding: 12px 14px;
  border-radius: var(--radius);
  border: 1px solid transparent;
  transition: background-color 0.25s ease, border-color 0.25s ease;
}
.story-line .line-en { grid-column: 2; }

/* The line being spoken. A tinted band rather than a colour change on the text itself: the words
   carry tone colours already, and recolouring them to show position would overwrite the one thing
   the colours are for. */
.story-line.current {
  background: color-mix(in srgb, var(--ui-accent) 11%, transparent);
  border-color: color-mix(in srgb, var(--ui-accent) 26%, transparent);
}

.line-play {
  flex: none;
  width: 30px;
  height: 30px;
  display: grid;
  place-items: center;
  margin-top: 2px;
  border-radius: 50%;
  border: 1px solid var(--line);
  background: var(--bg-raised);
  color: var(--ink-soft);
  cursor: pointer;
  transition: color 0.15s, border-color 0.15s;
}
.line-play:hover { color: var(--ui-accent); border-color: var(--ui-accent); }
.story-line.current .line-play { color: var(--ui-accent); border-color: var(--ui-accent); }

/* Left-aligned and smaller than on a card. A flashcard centres one sentence because it is the only
   thing on the card; a story is read down the page, and a centred line moves the start of every
   line to a different place, which is exactly what a reader following along cannot afford. */
.line-zh .sent {
  justify-content: flex-start;
  gap: 8px 14px;
}
/* Blurred rather than gone is a CARD affordance: it hides the answer while letting you peek. A
   story is not a quiz, so switching pinyin off here removes it — otherwise every line carries a
   grey smear under it and the page reads as broken rather than as plain Chinese. */
.line-zh .sw .p.hidden-pinyin { display: none; }
.line-zh .sw .h { font-size: clamp(22px, 3.4vw, 30px); }
.line-zh .sw .p { font-size: 12.5px; }
.line-zh .sw { gap: 3px; }
.line-en {
  color: var(--ink-soft);
  font-size: 14px;
  margin-top: 6px;
}

/* The explanation panel sits under the line it was opened from. */
.line-zh .word-panel { margin-top: 10px; }

@media (prefers-reduced-motion: reduce) {
  .story-line, .story-badge { transition: none; }
}

@media (max-width: 560px) {
  .story-line { padding: 10px; gap: 4px 8px; }
  .reader-controls { gap: 8px; }
}

/* ---------------------------------------------------------------- drills and the test */

.drill-bar {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin-bottom: 12px;
}
.chips { display: flex; gap: 6px; flex-wrap: wrap; }
/* A selectable chip, unlike the static .chip label used on cards. */
button.chip {
  padding: 7px 14px;
  font-size: 13px;
  cursor: pointer;
  transition: border-color 0.15s, color 0.15s, background-color 0.15s;
}
button.chip:hover { border-color: var(--ink-faint); color: var(--ink); }
button.chip.on {
  background: var(--ui-accent-soft);
  border-color: color-mix(in srgb, var(--ui-accent) 40%, transparent);
  color: var(--ui-accent);
}
.select {
  padding: 6px 10px;
  border-radius: 9px;
  background: var(--bg-raised);
  border: 1px solid var(--line);
  color: var(--ink);
  margin-left: auto;
}

.drill-score-row {
  display: flex;
  align-items: stretch;
  gap: 10px;
  flex-wrap: wrap;
  margin-bottom: 18px;
}
.drill-score-row .stat {
  flex: 1;
  min-width: 84px;
  padding: 10px 13px;
  text-align: center;
}
.drill-score-row .stat b { font-size: 21px; }
.drill-score-row .btn { margin-left: auto; align-self: center; }

.quiz-card {
  background: var(--panel);
  backdrop-filter: var(--panel-blur);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 26px 22px;
  text-align: center;
}

/* The prompt. Sized by what it holds: characters carry the question when they ARE the question, and
   an English gloss set at the same size would shout. */
.drill-ask {
  font-size: 20px;
  font-weight: 600;
  min-height: 62px;
  display: grid;
  place-items: center;
}
.drill-ask.han { font-family: var(--han); font-size: clamp(44px, 11vw, 76px); line-height: 1.1; }
.drill-ask.en { font-size: clamp(21px, 4vw, 30px); }
.drill-en { color: var(--ink-soft); font-size: 14px; margin-top: 10px; }

/* The reading, and a way to hear it, under the characters on the Meaning card.
   The pinyin fades in on hover or keyboard focus; the speaker is always there, because a button you
   cannot see is a button nobody presses. */
.drill-prompt { display: grid; justify-items: center; gap: 6px; }
.prompt-aids { display: flex; align-items: center; gap: 10px; min-height: 26px; }
.prompt-pinyin {
  font-size: 16px;
  font-weight: 560;
  color: var(--ink-soft);
  opacity: 0;
  transition: opacity 0.18s ease;
}
.drill-prompt:hover .prompt-pinyin,
.drill-prompt:focus-within .prompt-pinyin { opacity: 1; }
/* Touch has no hover, so on a coarse pointer the reading simply shows. */
@media (hover: none) {
  .prompt-pinyin { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .prompt-pinyin { transition: none; }
}

.gap-sent { justify-content: center; gap: 6px 12px; }
.gap-sent .sw .h { font-size: clamp(24px, 4.6vw, 36px); }
/* The blank is the question, so it is the one thing on the line wearing the accent. */
.gap-sent .sw.gap .h {
  color: var(--ui-accent);
  border-bottom: 2px solid var(--ui-accent);
  letter-spacing: 2px;
}

.quiz-opts {
  display: grid;
  gap: 9px;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  margin-top: 22px;
}
.quiz-opt {
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 13px 15px;
  border-radius: var(--radius);
  border: 1px solid var(--line);
  background: var(--panel);
  backdrop-filter: var(--panel-blur);
  color: inherit;
  cursor: pointer;
  text-align: left;
  font-size: 15px;
  transition: border-color 0.12s, background-color 0.12s;
}
.quiz-opt {
  color: var(--ui-accent);
  transition:
    border-color 0.2s ease,
    background-color 0.2s ease,
    transform 0.2s cubic-bezier(0.2, 0.8, 0.3, 1),
    box-shadow 0.2s ease;
}
/* The label must stay ink, not the tile's hue — only the glow borrows the colour. */
.quiz-opt > span:not(.opt-key) { color: var(--ink); }
.quiz-opt:hover:not([disabled]) {
  border-color: currentColor;
  background: color-mix(in srgb, var(--bg-sunken) 80%, transparent);
  transform: translateY(-2px);
  box-shadow:
    inset 0 1px 0 color-mix(in srgb, #fff 35%, transparent),
    0 12px 26px -16px currentColor;
}
.quiz-opt[disabled] { cursor: default; }
.quiz-opt .opt-key {
  flex: none;
  width: 22px;
  height: 22px;
  display: grid;
  place-items: center;
  border-radius: 6px;
  background: var(--bg-sunken);
  color: var(--ink-faint);
  font-size: 12px;
  font-weight: 600;
}
.quiz-opt .opt-han { font-family: var(--han); font-size: 24px; }
.quiz-opt .opt-pinyin { font-size: 17px; font-weight: 560; }

/* Right and wrong are stated in colour AND in words below, never colour alone. */
.quiz-opt.right { border-color: var(--good); background: color-mix(in srgb, var(--good) 12%, transparent); }
.quiz-opt.wrong { border-color: var(--danger); background: color-mix(in srgb, var(--danger) 12%, transparent); }

.quiz-feedback {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 11px;
  flex-wrap: wrap;
  margin-top: 20px;
  padding: 13px;
  border-radius: var(--radius);
  background: var(--bg-sunken);
}
.quiz-feedback .verdict { font-weight: 650; }
.quiz-feedback.good .verdict { color: var(--good); }
.quiz-feedback.bad .verdict { color: var(--danger); }
.quiz-feedback .answer-han { font-family: var(--han); font-size: 25px; }
.quiz-feedback .answer-p { font-weight: 560; }
.quiz-feedback .answer-en { color: var(--ink-soft); font-size: 14px; }
.quiz-next { margin-top: 18px; }

.test-intro { display: grid; gap: 12px; justify-items: center; }
.test-progress {
  display: flex;
  justify-content: space-between;
  align-items: center;
  color: var(--ink-faint);
  font-size: 13px;
  margin-bottom: 8px;
}
.band-tag {
  padding: 3px 10px;
  border-radius: 999px;
  background: var(--bg-sunken);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.test-result {
  background: var(--panel);
  backdrop-filter: var(--panel-blur);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 26px 22px;
}
.test-headline {
  display: flex;
  align-items: baseline;
  gap: 12px;
  flex-wrap: wrap;
  font-size: 16px;
  color: var(--ink-soft);
}
.test-headline b {
  font-size: clamp(40px, 9vw, 62px);
  font-weight: 680;
  letter-spacing: -0.03em;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.test-delta { color: var(--ui-accent); font-weight: 600; font-size: 14px; }

.band-chart { display: flex; flex-direction: column; gap: 8px; margin: 22px 0; }
.band-row { display: grid; grid-template-columns: 92px 1fr 68px; gap: 12px; align-items: center; }
.band-label { font-size: 13px; color: var(--ink-soft); font-variant-numeric: tabular-nums; }
.band-bar {
  height: 10px;
  border-radius: 999px;
  background: var(--bg-sunken);
  overflow: hidden;
}
.band-fill { display: block; height: 100%; background: var(--ui-accent); border-radius: 999px; }
.band-pct { font-size: 12.5px; color: var(--ink-faint); text-align: right; font-variant-numeric: tabular-nums; }
/* A band the test never got to reads as absent, not as zero — they mean different things. */
.band-row.unasked { opacity: 0.45; }

.test-next { border-top: 1px solid var(--line); padding-top: 18px; }
.test-next p { color: var(--ink-soft); font-size: 14px; margin-bottom: 12px; }
.test-actions { display: flex; gap: 10px; flex-wrap: wrap; }
.test-actions .btn { text-decoration: none; }

.test-history { display: flex; flex-direction: column; gap: 2px; }
.test-row {
  display: grid;
  grid-template-columns: 1fr auto auto;
  gap: 14px;
  padding: 11px 14px;
  border-radius: 10px;
  font-size: 14px;
  font-variant-numeric: tabular-nums;
}
.test-row:nth-child(odd) { background: var(--bg-raised); }
.test-when { color: var(--ink-soft); }
.test-known { font-weight: 600; }
.test-score { color: var(--ink-faint); }

@media (prefers-reduced-motion: reduce) {
  button.chip, .quiz-opt { transition: none; }
}

@media (max-width: 560px) {
  .quiz-card { padding: 20px 14px; }
  .quiz-opts { grid-template-columns: 1fr; }
  .select { margin-left: 0; width: 100%; }
  /* Two by two rather than three-then-one: four counters that wrap unevenly read as a layout
     accident, and these four belong together. */
  .drill-score-row .stat { flex: 0 0 calc(50% - 5px); }
  .drill-score-row .btn { margin-left: 0; width: 100%; }
  .band-row { grid-template-columns: 76px 1fr 54px; gap: 8px; }
}

/* ---------------------------------------------------------------- ask (the tutor) */

.ask-bar { display: flex; gap: 10px; margin-bottom: 22px; }
.ask-input {
  flex: 1;
  min-width: 0;
  padding: 12px 15px;
  border-radius: var(--radius);
  border: 1px solid var(--line);
  background: var(--bg-raised);
  color: var(--ink);
  font-size: 15px;
}
.ask-input:focus { outline: none; border-color: var(--ui-accent); }
.ask-input::placeholder { color: var(--ink-faint); }

/* Browsing, for before anything has been asked. A bare search box only helps someone who already
   knows what is in the box. */
.ask-browse { display: flex; flex-wrap: wrap; gap: 7px; }
.browse-chip {
  padding: 8px 14px;
  border-radius: var(--radius-pill, 999px);
  border: 1px solid var(--line);
  background: var(--bg-raised);
  color: var(--ink-soft);
  font-size: 13.5px;
  cursor: pointer;
  transition: border-color 0.15s, color 0.15s;
}
.browse-chip:hover { border-color: var(--ui-accent); color: var(--ui-accent); }

.answer {
  background: var(--panel);
  backdrop-filter: var(--panel-blur);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 20px 22px;
  margin-bottom: 14px;
}
.answer-head { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
.answer-head h3 { font-size: 19px; font-weight: 640; }
.answer-body { line-height: 1.65; color: var(--ink); }
/* The Chinese inside an explanation should read as Chinese, not as the body font's fallback. */
.answer-body { font-variant-east-asian: simplified; }

.answer-examples-head {
  margin: 20px 0 10px;
  font-size: 12px;
  font-weight: 650;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.answer-examples { display: flex; flex-direction: column; gap: 4px; }
.answer-example {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 2px 12px;
  align-items: center;
  padding: 11px 13px;
  border-radius: 10px;
  background: var(--bg-sunken);
}
.answer-example .sent { justify-content: flex-start; gap: 6px 12px; }
.answer-example .sw .h { font-size: clamp(20px, 3.2vw, 26px); }
.answer-example .sw .p { font-size: 11.5px; }
.answer-example .sw { gap: 2px; }
.answer-example > :not(.speak) { grid-column: 1; }
.answer-example .speak { grid-column: 2; grid-row: 1 / span 2; align-self: center; }
.example-en { color: var(--ink-soft); font-size: 13.5px; }

/* Saying nothing is a real answer here, so it gets a real panel rather than a grey line. */
.answer.none { border-style: dashed; }
.answer.none p { color: var(--ink-soft); line-height: 1.6; margin: 8px 0 16px; }

@media (prefers-reduced-motion: reduce) {
  .browse-chip { transition: none; }
}

@media (max-width: 560px) {
  .ask-bar { flex-wrap: wrap; }
  .ask-bar .btn { width: 100%; }
  .answer { padding: 16px 14px; }
}

/* ---------------------------------------------------------------- translations */

.answer.translation { padding: 16px 18px; margin-bottom: 10px; }
.translation-head { display: flex; align-items: center; justify-content: flex-end; gap: 8px; }
.answer.translation .sent { justify-content: flex-start; gap: 8px 14px; }
.answer.translation .sw .h { font-size: clamp(24px, 4vw, 34px); }
.answer.translation .sw .p { font-size: 12.5px; }
.answer.translation .sw { gap: 3px; }
/* The English of the match, always shown: it is the only way someone who cannot read the Chinese
   can tell the sentence found is not the one they meant. */
.translation-en { color: var(--ink-soft); font-size: 14px; margin-top: 10px; }

.translation-words { display: flex; flex-direction: column; gap: 4px; }
.translation-word {
  display: grid;
  grid-template-columns: auto auto 1fr auto;
  align-items: baseline;
  gap: 14px;
  padding: 11px 14px;
  border-radius: 10px;
  background: var(--panel);
  backdrop-filter: var(--panel-blur);
  border: 1px solid var(--line);
  text-decoration: none;
  color: inherit;
}
.tw-han { font-family: var(--han); font-size: 25px; }
.tw-p { font-weight: 560; color: var(--ink-soft); }
.tw-en { color: var(--ink-faint); font-size: 14px; }
.translation-word .speak { align-self: center; }

@media (max-width: 560px) {
  .translation-word { grid-template-columns: auto 1fr auto; gap: 10px; }
  .tw-en { grid-column: 1 / -1; }
}

.account-line { display: flex; align-items: center; gap: 12px; justify-content: space-between; }
.account-line span { font-weight: 560; }

/* ---------------------------------------------------------------- home */

/* No cards and no path. Nothing here has a border, a panel or a shadow, and nothing joins one place
   to the next — these are independent destinations, not a route. A line between them would promise
   an order that does not exist. */

.home { max-width: 1000px; margin: 0 auto; }

/* The greeting, set in Chinese with the English under it.
   The themes name typefaces the app never loads, so every one of the 44 fell back to the same
   system sans. The CJK face is the one distinctive type already on every device — and on a Mandarin
   app it is also the most fitting. */
.hello { text-align: center; padding: 22px 0 10px; }
.hello h1 {
  font-size: clamp(34px, 6.4vw, 56px);
  font-weight: 650;
  line-height: 1.05;
  letter-spacing: -0.03em;
  color: var(--ink);
}
.hello-zh {
  font-family: var(--han);
  font-size: clamp(21px, 3.4vw, 29px);
  font-weight: 550;
  letter-spacing: 0.24em;
  /* Tracking pushes the last glyph right; pull the block back so it still reads as centred. */
  text-indent: 0.24em;
  color: var(--ink-faint);
  margin-top: 10px;
}
.hello p { color: var(--ink-soft); font-size: 16px; margin-top: 10px; }

.hello-go {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  margin-top: 20px;
  padding: 12px 24px;
  border-radius: 999px;
  background: var(--ui-accent);
  color: var(--ui-accent-ink);
  font-weight: 600;
  font-size: 15px;
  text-decoration: none;
  transition: transform 0.15s ease, filter 0.15s ease;
}
.hello-go:hover { transform: translateY(-1px); filter: brightness(1.07); }
.hello-arrow { transition: transform 0.15s ease; }
.hello-go:hover .hello-arrow { transform: translateX(3px); }

/* The places. A loose flow rather than a table — they wrap where they fit and nothing lines up in
   columns down the page, which is what keeps nine independent things from reading as a list. */
.places {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 30px 22px;
  padding: 34px 0 48px;
}

.place {
  flex: 0 1 268px;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  padding: 8px 10px 14px;
  text-decoration: none;
  color: inherit;
  border-radius: 20px;
  transition: transform 0.2s cubic-bezier(0.2, 0.7, 0.3, 1), background-color 0.2s ease;
}
/* THE HIGHLIGHT — shared by every tile you can click.
 *
 * Four things happen at once, and each is doing a different job:
 *
 *   glass      the tile frosts — a translucent panel over the background blooms, with a hairline
 *              that catches light along the top edge the way a real bevel would
 *   spotlight  a soft pool of the tile's OWN colour that follows the pointer, so it feels lit by
 *              the cursor rather than merely switched on
 *   sheen      a narrow diagonal gleam that sweeps across once, like light moving over glass
 *   glow       a coloured shadow beneath, so the tile reads as lifted off the page
 *
 * The colour throughout is `currentColor`, set on each tile from its own hue, so every layer agrees
 * without any of them naming a colour.
 *
 * One rule for all of them rather than three copies: these are the same gesture in three places,
 * and three copies is three things to forget when one changes.
 */
.place,
.deck,
.quiz-opt {
  position: relative;
  isolation: isolate;
}

/* The cursor-tracking pool. Its own layer so it can fade independently of the glass. */
.place::before,
.deck::before,
.quiz-opt::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  opacity: 0;
  border-radius: inherit;
  transition: opacity 0.28s ease;
  background: radial-gradient(
    14rem 14rem at var(--mx, 50%) var(--my, 40%),
    color-mix(in srgb, currentColor 32%, transparent),
    transparent 68%
  );
}
.place:hover::before,
.place:focus-visible::before,
.deck:hover::before,
.deck:focus-visible::before,
.quiz-opt:hover:not([disabled])::before,
.quiz-opt:focus-visible::before { opacity: 1; }

/* The sheen. Parked off to the left, sweeps across once when the pointer arrives. */
.place::after,
.deck::after,
.quiz-opt::after {
  content: '';
  position: absolute;
  top: -60%;
  bottom: -60%;
  left: -75%;
  width: 55%;
  z-index: -1;
  pointer-events: none;
  opacity: 0;
  transform: translateX(0) rotate(18deg);
  background: linear-gradient(90deg, transparent, color-mix(in srgb, #fff 42%, transparent), transparent);
}
.place:hover::after,
.deck:hover::after,
.quiz-opt:hover:not([disabled])::after {
  opacity: 1;
  transform: translateX(360%) rotate(18deg);
  transition: transform 0.85s cubic-bezier(0.25, 0.6, 0.3, 1), opacity 0.2s ease;
}

.place {
  overflow: hidden;
  border: 1px solid transparent;
  transition:
    transform 0.28s cubic-bezier(0.2, 0.8, 0.3, 1),
    box-shadow 0.28s ease,
    border-color 0.28s ease,
    background-color 0.28s ease,
    backdrop-filter 0.28s ease;
}

.place:hover,
.place:focus-visible {
  transform: translateY(-6px);
  background: color-mix(in srgb, var(--bg-raised) 55%, transparent);
  backdrop-filter: blur(10px) saturate(1.5);
  border-color: color-mix(in srgb, currentColor 34%, transparent);
  box-shadow:
    inset 0 1px 0 color-mix(in srgb, #fff 55%, transparent),
    0 20px 44px -20px currentColor,
    0 6px 18px -12px color-mix(in srgb, var(--ink) 35%, transparent);
}

/* The picture lifts a little further than the tile, which is what gives the parallax. */
.place:hover .place-art { transform: translateY(-3px) scale(1.04); }

.place-art { transform: translateY(-3px) scale(1.04); }

.place-art {
  width: 172px;
  transition: transform 0.28s cubic-bezier(0.2, 0.8, 0.3, 1);
}
.home-art { display: block; width: 100%; height: auto; overflow: visible; }

/* English is the heading. Sentence case rather than caps: at this size caps shout, and two of these
   names are five words long. */
.place-name {
  font-size: 23px;
  font-weight: 650;
  line-height: 1.15;
  letter-spacing: -0.02em;
  color: var(--ink);
  margin-top: 6px;
}
/* The Chinese is the subtitle — small, widely tracked, in the CJK face. It still does the job the
   large setting did, which is to look like nothing else in the interface. */
.place-zh {
  font-family: var(--han);
  /* Larger than a subtitle would normally be. Chinese carries less ink per em than Latin at the
     same size — a 15px 词语 sits visually smaller than 15px "Words" — so matching the label's size
     numerically would leave it looking shrunken. */
  font-size: 19px;
  font-weight: 550;
  letter-spacing: 0.3em;
  text-indent: 0.3em;
  color: currentColor;
  margin-top: 7px;
}
.place-blurb {
  color: var(--ink-soft);
  font-size: 13.5px;
  line-height: 1.55;
  margin-top: 8px;
  max-width: 25ch;
}
.place-note {
  color: var(--ink-faint);
  font-size: 12px;
  margin-top: 10px;
  letter-spacing: 0.04em;
  font-variant-numeric: tabular-nums;
}
.place-note .due { color: var(--ui-accent); font-weight: 700; }

/* With motion turned down nothing travels: the tile still lights and frosts, because that is
   information about what is under the pointer, but it does not move and nothing sweeps. */
/* With motion turned down nothing travels: tiles still light and frost, because that is
   information about what is under the pointer, but nothing moves and nothing sweeps. */
@media (prefers-reduced-motion: reduce) {
  .place, .place::before, .place-art, .deck, .deck::before, .quiz-opt, .quiz-opt::before,
  .hello-go, .hello-arrow { transition: none; }
  .place:hover, .place:focus-visible, .deck:hover { transform: none; }
  .place:hover .place-art { transform: none; }
  .place::after, .deck::after, .quiz-opt::after { display: none; }
}

@media (max-width: 620px) {
  .places { gap: 26px 16px; padding: 24px 0 40px; }
  .place { flex: 0 1 150px; padding: 6px 4px 10px; }
  .place-art { width: 116px; }
  .place-name { font-size: 17px; }
  .place-zh { font-size: 16px; letter-spacing: 0.22em; text-indent: 0.22em; }
  .place-blurb { font-size: 12.5px; }
}


/* ---------------------------------------------------------------- display type */

/* Baloo 2 anywhere the type is meant to be heard rather than read: the greeting, page titles, the
   names on the home page, the big characters on a card's answer. Both faces are rounded, so the
   two sit together; Baloo is simply louder. */
.hello h1,
.page-head h1,
.place-name,
.study-bar .deck-title,
.deck-name,
.area-name,
.answer-head h3,
.test-headline b,
.resume-deck,
.hello-go,
.btn {
  font-family: var(--display);
  letter-spacing: -0.01em;
}
.hello h1 { font-weight: 700; }
.place-name { font-weight: 600; }

/* ---------------------------------------------------------------- the background */

/* Flat expanses of one colour are what made every page feel like a form. Two layers fix that
   without getting in the way of reading:
   
   Blooms — a few very large, very soft washes of the theme's own hues, fixed to the viewport so
   they do not slide about while you scroll. Drawn from tokens, so a dark theme blooms dark and a
   warm one blooms warm; nothing here names a colour.
   
   Characters — a sparse scatter of hanzi at the edge of visibility. On a Mandarin app that is the
   most fitting texture available, and at this opacity it reads as paper rather than as content. */
/* `isolation` is doing real work here, not tidying. Without a stacking context on body, its
   negative-z pseudo-elements join the root's context and are painted BEFORE body's own background —
   so an opaque body colour covered both layers completely and the blooms never showed. With one,
   they paint inside body: after its background, beneath its content. */
body { position: relative; isolation: isolate; }

/* `hidden` must actually hide.
 *
 * The browser's own rule is `[hidden] { display: none }`, and ANY author rule that sets display
 * beats it — `.field { display: flex }` was enough to put the account row back on screen with its
 * hidden attribute still set, so local mode showed a "Sign out" button for a session that does not
 * exist. This is the one place `!important` is the right tool: hidden is not a suggestion. */
[hidden] { display: none !important; }

body::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: -2;
  pointer-events: none;
  background:
    radial-gradient(52rem 38rem at 12% -6%, color-mix(in srgb, var(--primary) 30%, transparent), transparent 70%),
    radial-gradient(44rem 34rem at 92% 4%, color-mix(in srgb, var(--info) 26%, transparent), transparent 70%),
    radial-gradient(46rem 40rem at 78% 96%, color-mix(in srgb, var(--success) 24%, transparent), transparent 70%),
    radial-gradient(40rem 32rem at 4% 88%, color-mix(in srgb, var(--warning) 22%, transparent), transparent 70%);
}

body::after {
  content: '';
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  /* The tile is authored as one SVG data URI rather than as elements: it is decoration, it must
     never be selectable or reachable by a screen reader, and it costs no DOM. */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='420' height='420' viewBox='0 0 420 420'%3E%3Cg fill='%23000' font-family='system-ui' font-size='42'%3E%3Ctext x='30' y='70'%3E%E8%AF%8D%3C/text%3E%3Ctext x='250' y='120'%3E%E8%AF%B4%3C/text%3E%3Ctext x='120' y='210'%3E%E9%97%AE%3C/text%3E%3Ctext x='330' y='260'%3E%E5%AD%97%3C/text%3E%3Ctext x='60' y='330'%3E%E5%AD%A6%3C/text%3E%3Ctext x='215' y='390'%3E%E8%AF%BB%3C/text%3E%3C/g%3E%3C/svg%3E");
  background-size: 420px 420px;
  /* Barely there. Enough to catch the light, not enough to read. */
  opacity: 0.022;
}
/* On a dark theme the same black characters would vanish; inverting turns them to light ink. */
@media (prefers-color-scheme: dark) {
  body::after { filter: invert(1); opacity: 0.035; }
}
:root[data-theme] body::after { filter: none; }

/* Panels sit ON the background, so they need to stop being transparent where they overlap a bloom. */
.topbar { isolation: isolate; }

/* ------------------------------------------------------- what you already know */

.known-body { margin-top: 16px; }

/* A dense grid, because the job is to get through hundreds of these. Small enough that a screenful
   is a real dent in the list, big enough that the characters are legible at a glance. */
.word-chips {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(132px, 1fr));
  gap: 8px;
}

.word-chip {
  display: grid;
  gap: 1px;
  padding: 9px 10px;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--panel);
  text-align: left;
  cursor: pointer;
  font: inherit;
  color: var(--ink);
  transition: border-color 0.14s ease, background-color 0.14s ease, transform 0.1s ease;
}
/* Hovering the only thing it does — undo — reads as taking the tick back, so it cools rather than
   warms. Everything on this page is already marked; green-on-hover would say the opposite. */
.word-chip:hover {
  border-color: color-mix(in srgb, var(--ink) 32%, var(--line));
  background: var(--panel);
}
.word-chip:hover .chip-hanzi { opacity: 0.6; }
.word-chip:active { transform: scale(0.97); }
.word-chip.on {
  border-color: color-mix(in srgb, var(--success) 55%, transparent);
  background: color-mix(in srgb, var(--success) 14%, transparent);
}
.chip-hanzi { font-family: var(--han); font-size: 20px; line-height: 1.2; }
.chip-pinyin { font-size: 12.5px; color: var(--ui-accent); }
.chip-gloss {
  font-size: 12px;
  color: var(--ink-faint);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.known-empty {
  display: grid;
  justify-items: center;
  gap: 4px;
  padding: 44px 20px;
  text-align: center;
  border: 1px dashed var(--line);
  border-radius: var(--radius);
}
.known-empty p { margin: 0; max-width: 40ch; }
.known-empty kbd {
  font: inherit;
  font-size: 12px;
  padding: 1px 6px;
  border: 1px solid var(--line);
  border-radius: 5px;
  background: var(--bg-sunken);
}

.more {
  display: block;
  margin: 16px auto 0;
  padding: 10px 18px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--panel);
  color: var(--ink);
  font: inherit;
  cursor: pointer;
}
.more:hover { border-color: var(--ui-accent); }
a.more { width: max-content; text-decoration: none; margin-top: 14px; }

/* "I already know this", in the corner of the answer side.
   Above the well's own stacking so it stays clickable, and inside the face so it turns with it. */
/* `.face >` rather than a bare class, because [data-tip] sets `position: relative` further down
   the file at the same specificity — and this button carries a tooltip. That is what pinned it to
   the top of the card as a full-width flex item instead of tucking it into the corner. */
/* No chip. A bordered pill in the corner of the card competes with the answer for attention, and
   this control is a note about the card rather than a thing to do to it. Plain text until it is
   set; then it earns its colour. */
.face > .know-toggle {
  position: absolute;
  /* Placed ON the card rather than stuck to its border, and with room for the focus ring. */
  top: 15px;
  right: 18px;
  z-index: 3;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 4px 2px;
  border: 0;
  background: none;
  color: var(--ink-faint);
  font: inherit;
  font-size: 12.5px;
  font-weight: 560;
  cursor: pointer;
  transition: color 0.14s ease;
}
/* The tick is the whole signal, so it does not exist until there is something to signal. */
.face > .know-toggle svg {
  width: 0;
  opacity: 0;
  transition: width 0.16s ease, opacity 0.16s ease;
}
.face > .know-toggle:hover { color: var(--success); }
.face > .know-toggle.on { color: var(--success); }
.face > .know-toggle.on svg { width: 15px; opacity: 1; }
.face > .know-toggle:focus-visible {
  outline: 2px solid var(--ring, var(--ui-accent));
  outline-offset: 3px;
  border-radius: 6px;
}

/* The corner belongs to the toggle, so the well starts below it. */
.face.back .well { padding-top: 30px; }

/* ------------------------------------------------------------ made for you */

.make-bar { display: flex; gap: 10px; flex-wrap: wrap; margin: 4px 0 10px; }
.theme-input {
  flex: 1 1 260px;
  min-width: 0;
  padding: 10px 14px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--bg-raised);
  color: var(--ink);
  font: inherit;
}
.theme-input:focus-visible { outline: 2px solid var(--ui-accent); outline-offset: 1px; }
.write-btn { border-radius: 999px; padding: 10px 22px; white-space: nowrap; }
.write-btn[disabled] { opacity: 0.5; cursor: not-allowed; }

.made-list { display: grid; gap: 10px; margin-top: 14px; }

/* The Chinese leads and the English follows in the way a translation should: available, and not
   competing. Here more than anywhere, since the whole claim is that you can read the Chinese. */
.made {
  position: relative;
  padding: 16px 44px 14px 18px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel);
}
.made-en { margin: 8px 0 0; color: var(--ink-faint); font-size: 14.5px; }
.made-drop {
  position: absolute;
  top: 10px;
  right: 10px;
  width: 26px;
  height: 26px;
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: var(--ink-faint);
  font-size: 17px;
  line-height: 1;
  cursor: pointer;
  opacity: 0;
  transition: opacity 0.14s ease, color 0.14s ease, background-color 0.14s ease;
}
.made:hover .made-drop, .made-drop:focus-visible { opacity: 1; }
.made-drop:hover { background: var(--bg-sunken); color: var(--danger); }

/* --------------------------------------------------------- stories made for you */

/* The handful of words a made story is allowed beyond what you have marked, named up front. */
.new-words {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 8px 14px;
  margin: 0 0 16px;
  padding: 11px 14px;
  border: 1px dashed color-mix(in srgb, var(--warning) 45%, var(--line));
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--warning) 7%, transparent);
  font-size: 14px;
}
.new-words > b { color: var(--ink-soft); font-size: 12.5px; text-transform: uppercase; letter-spacing: 0.06em; }
.new-word { display: inline-flex; align-items: baseline; gap: 5px; }
.new-word b { font-family: var(--han); font-size: 17px; font-weight: 500; }
.new-word i { color: var(--ui-accent); font-style: normal; font-size: 13px; }
.new-word span { color: var(--ink-faint); font-size: 13px; }


.made-shelf { margin-bottom: 26px; }
.made-shelf h2 {
  font-family: var(--display);
  font-size: 22px;
  margin: 0 0 10px;
  letter-spacing: -0.01em;
}
.made-shelf .decks:empty { display: none; }
.made-shelf .make-bar { margin-bottom: 6px; }

/* --------------------------------------------------------- the model setting */

.model-field { gap: 8px; }
.model-field input {
  width: 100%;
  padding: 9px 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--bg-raised);
  color: var(--ink);
  font: inherit;
  font-size: 14px;
}
.model-field input:focus-visible { outline: 2px solid var(--ui-accent); outline-offset: 1px; }
.model-row2 { display: flex; gap: 8px; }
.model-row2 input { flex: 1 1 auto; min-width: 0; }
.model-row2 .btn { white-space: nowrap; }
#model-state { font-weight: 500; }

/* ------------------------------------------------- the study controls, in the bar */

/* They sit in the title bar because they describe the SESSION — what the next card will be —
   rather than this moment in it. Condensed to match a bar rather than a page. */
.bar-tools { display: flex; align-items: center; margin-left: auto; }
.study-tools { display: flex; align-items: center; gap: 6px; flex-wrap: nowrap; }
.study-tools .seg { padding: 2px; gap: 2px; }
.study-tools .seg button { padding: 5px 9px; font-size: 12.5px; }
.study-tools .btn {
  padding: 6px 10px;
  font-size: 12.5px;
  border-radius: 9px;
  white-space: nowrap;
}

/* A phone has no room for the mode switch beside everything else; it is the one control with a
   home elsewhere, since the deck page offers the same three modes. */
@media (max-width: 760px) {
  .study-tools .seg { display: none; }
}
@media (max-width: 520px) {
  .study-tools .btn { padding: 6px 8px; font-size: 12px; }
}

/* ------------------------------------------------------------------ sign in */

/* Nothing in the bar leads anywhere until you are signed in. */
:root[data-signed-out] #nav,
:root[data-signed-out] #btn-settings { display: none; }

.sign-in {
  max-width: 30rem;
  margin: 14vh auto 0;
  text-align: center;
  display: grid;
  gap: 10px;
  justify-items: center;
}
.sign-in h1 { font-family: var(--display); font-size: 44px; margin: 0; letter-spacing: -0.02em; }
.sign-in p { margin: 0; max-width: 34ch; }
.sign-in-btn {
  margin-top: 14px;
  padding: 12px 26px;
  border: 0;
  border-radius: 999px;
  background: var(--ui-accent);
  color: var(--on-accent, #fff);
  text-decoration: none;
  font-weight: 650;
  box-shadow: var(--shadow-sm);
}
.sign-in-btn:hover { filter: brightness(1.06); }

/* ---------------------------------------------------------------- tooltips */

/* Instant, because the native ones are not.
 *
 * A browser waits roughly a second before showing a `title`, which is useless to somebody scanning
 * a row of eight pictures for the one they want — by the time it appears they have moved on. These
 * show on the first frame and fade out slowly, which is the right way round: appearing late is a
 * failure, disappearing late is barely noticed.
 *
 * Driven by `data-tip` so the accessible name stays on `aria-label` and no element carries both a
 * custom tooltip and a native one.
 */
[data-tip] { position: relative; }

[data-tip]::after {
  content: attr(data-tip);
  position: absolute;
  top: calc(100% + 9px);
  left: 50%;
  transform: translateX(-50%) translateY(-3px);
  padding: 5px 10px;
  border-radius: 8px;
  background: color-mix(in srgb, var(--ink) 92%, transparent);
  color: var(--bg-raised);
  font-family: var(--ui);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.01em;
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  z-index: 40;
  /* No delay in, a slow fade out. */
  transition: opacity 0.18s ease 0.16s, transform 0.18s ease 0.16s;
}

/* The little nib, so it points at the thing it names. */
[data-tip]::before {
  content: '';
  position: absolute;
  top: calc(100% + 4px);
  left: 50%;
  width: 8px;
  height: 8px;
  transform: translateX(-50%) rotate(45deg) translateY(-3px);
  background: color-mix(in srgb, var(--ink) 92%, transparent);
  pointer-events: none;
  opacity: 0;
  z-index: 40;
  transition: opacity 0.18s ease 0.16s, transform 0.18s ease 0.16s;
}

[data-tip]:hover::after,
[data-tip]:hover::before,
[data-tip]:focus-visible::after,
[data-tip]:focus-visible::before {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
  /* Instant on the way in — the delay above is only for the fade out. */
  transition-delay: 0s;
}
[data-tip]:hover::before,
[data-tip]:focus-visible::before {
  transform: translateX(-50%) rotate(45deg) translateY(0);
}

/* The last control in the bar sits against the page edge, and a tooltip centred under it hangs off
   that edge — which gave the whole app a horizontal scrollbar. Note WHEN: the tooltip is invisible
   until you point at it, but `opacity: 0` leaves the box in the layout, so the page could be dragged
   sideways before anyone had hovered anything. Anchoring the panel to the button's right edge is the
   fix rather than clipping it: clipping would cut the tooltip in half, and this one has nowhere to
   the right to go anyway. The nib stays centred over the icon, because the nib is what points. */
.topbar > :last-child[data-tip]::after {
  left: auto;
  right: 0;
  transform: translateX(0) translateY(-3px);
}
.topbar > :last-child[data-tip]:hover::after,
.topbar > :last-child[data-tip]:focus-visible::after {
  transform: translateX(0) translateY(0);
}

/* Touch has no hover, and a tooltip that fires on tap just covers what you tapped. */
@media (hover: none) {
  [data-tip]::after, [data-tip]::before { display: none; }
}
@media (prefers-reduced-motion: reduce) {
  [data-tip]::after, [data-tip]::before { transition: none; }
}

/* ---------------------------------------------------------------- character hints */

/* What a character is made of, on the answer side of a card. Set quietly: it is an aside that
   rewards a second look, not a second answer competing with the first. */
.hints {
  margin-top: 14px;
  padding-top: 12px;
  border-top: 1px dashed color-mix(in srgb, var(--ink) 14%, transparent);
  display: flex;
  flex-direction: column;
  gap: 7px;
}
.hints-head {
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.hint-row { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.hint-char { font-family: var(--han); font-size: 20px; color: var(--ink); }
.hint-parts { display: flex; align-items: baseline; gap: 6px; flex-wrap: wrap; font-size: 13.5px; }
.hint-parts b { font-family: var(--han); font-size: 18px; font-weight: 500; color: var(--ui-accent); }
.hint-sense { color: var(--ink-soft); }
.hint-plus { color: var(--ink-faint); }
.hint-kind { color: var(--ink-faint); font-style: italic; }

/* The picture, where a word is a thing you could point at. Sized to be seen and then dismissed —
   it is a mnemonic, not the answer, and the meaning above it stays the headline. */
.word-picture {
  display: flex;
  justify-content: center;
  margin-top: 2px;
  color: var(--tone-accent, var(--ui-accent));
}
.word-art { width: 132px; height: auto; display: block; }
