/* OneOneTyping v6 — typing engine.
 *
 * The layout follows typing.com's lesson page, measured off
 * /student/lesson/333/skill-builder rather than invented:
 *
 *   body            #f7f7f7 — a neutral floor, so the one coloured thing on the
 *                   page is the text you are typing
 *   .eng-progress   a full-width 6px green bar pinned at the very top
 *   .eng-toolbar    a thin row: back, title, live numbers, tools
 *   .eng-stage      centred, max-width 970
 *     .eng-screen   white card, radius 10, padding 5px 10px — the passage
 *     .keyboard     800px board on a #d9dadb tray, radius 20, padding 30/15/10
 *   .eng-footer     small print
 *
 * Typography is theirs too: Roboto Mono 30px on a 45px line, each character a
 * 21px centred cell, and a 3px #0099ff underline for the caret.
 *
 * The engine keeps its own typeface on purpose: a drill should look like a
 * drill, not like the marketing site around it.
 */

.engine {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  background: var(--bg);
}

/* ---------------------------------------------------------- progress bar
   Full width, pinned under the bar. 6px of green, nothing else — it is the one
   piece of colour on the page that is always moving. */

.eng-progress {
  position: fixed;
  top: var(--bar-h);
  left: 0;
  z-index: 999;
  width: 100%;
  height: 7px;
  padding: 1px 0;
  background: rgba(255, 255, 255, 0.97);
}

.eng-progress > i {
  display: block;
  width: 0;
  height: 5px;
  background: var(--green);
  transition: width 0.3s linear;
}

/* -------------------------------------------------------------- top bar
   What belongs up here, and why:

     ← back            one way out, always in the same place
     title + sub       what you are typing, and where you are inside it
     live stats        speed / accuracy / progress, centred so the eye finds
                       them without hunting
     clock             only on a timed test, where it is the one number that
                       changes the shape of what you do
     tool buttons      restart, keyboard, hands, sound, read aloud

   Both reference sites keep this row to a single line and leave it borderless:
   typing.com uses 31px of flat blue, edclub 43px of white. Ours is 52px of
   white so the title can carry a second line, and it gains a hairline only once
   the page scrolls. */

.engine {
  --bar-h: 52px;
}

.eng-toolbar {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 1000;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: center;
  gap: 16px;
  width: 100%;
  height: var(--bar-h);
  padding: 0 16px;
  background: rgba(255, 255, 255, 0.97);
  -webkit-backdrop-filter: saturate(180%) blur(10px);
  backdrop-filter: saturate(180%) blur(10px);
  transition: box-shadow 0.2s, border-color 0.2s;
  border-bottom: 1px solid transparent;
}

.eng-toolbar.is-scrolled {
  border-bottom-color: var(--line-soft);
}

/* ---- left: back + title ------------------------------------------------ */

.eng-toolbar-left {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
}

.eng-toolbar-title {
  min-width: 0;
}

.eng-toolbar-title b {
  display: block;
  color: var(--text-strong);
  font-size: 15px;
  font-weight: var(--fw-bold);
  line-height: 1.25;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.eng-toolbar-title span {
  display: block;
  margin-top: 1px;
  color: var(--text-dim);
  font-size: 12px;
  line-height: 1.2;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ---- centre: the live numbers ------------------------------------------ */

/* The live readout for a lesson. It occupies the same middle slot as the test
   bar's label and carries the same weight: one line of small quiet text.
   The gauge it replaced was a bordered, filled three-cell block 41px tall
   inside a 64px bar — the heaviest element on the row, and it duplicated the
   speed and accuracy targets already shown in the title block. */
.eng-live {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 14px;
  color: var(--text-dim);
  font-size: 12px;
  white-space: nowrap;
}

/* --text-muted, not --text-strong: near-black numbers made this the heaviest
   element on the row, which is exactly what the test bar's quiet middle line
   avoids. The units sit one step lighter again, so the numbers still lead
   without out-weighing the rest of the bar. */
.eng-live b {
  color: var(--text-muted);
  font-family: var(--font-mono);
  font-size: 13px;
  font-weight: var(--fw-semibold);
  font-variant-numeric: tabular-nums;
}

/* ---- right: clock + tools ---------------------------------------------- */

.eng-toolbar-right {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 4px;
}

.eng-clock {
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 74px;
  height: 34px;
  margin-right: 6px;
  padding: 0 12px;
  border: 1px solid var(--line-soft);
  border-radius: 10px;
  background: var(--bg-soft);
  color: var(--text-strong);
  font-family: var(--font-mono);
  font-size: 17px;
  font-weight: var(--fw-semibold);
  font-variant-numeric: tabular-nums;
  line-height: 1;
}

.eng-clock.is-low {
  border-color: var(--red-soft);
  background: var(--red-soft);
  color: var(--wrong-ink);
}

/* ---- buttons ----------------------------------------------------------- */

.eng-back,
.eng-tool {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  padding: 0;
  border: 0;
  border-radius: 9px;
  background: transparent;
  color: var(--text-muted);
  transition: color 0.16s, background-color 0.16s;
}

.eng-back:hover,
.eng-tool:hover {
  background: var(--accent-tint);
  color: var(--accent-ink);
}

.eng-tool[aria-pressed='true'] {
  background: var(--accent-tint);
  color: var(--accent);
}

.eng-back svg,
.eng-tool svg {
  width: 17px;
  height: 17px;
}

/* The segment chip: "第 1 / 3 段", the equivalent of typing.com's
   "Screen 1 of 4" in its title. */
.eng-seg {
  display: inline-flex;
  align-items: center;
  height: 34px;
  margin-right: 6px;
  padding: 0 11px;
  border: 1px solid var(--line-soft);
  border-radius: 10px;
  background: var(--bg-soft);
  color: var(--text-muted);
  font-size: 12px;
  font-weight: var(--fw-semibold);
  white-space: nowrap;
}

/* A per-character annotation (a 五笔 or 郑码 code, for instance) reads as data,
   so it gets monospace and the accent colour rather than the muted running
   commentary style. */
.practice-screen-head .is-code {
  color: var(--accent-ink);
  font-family: var(--font-mono);
  font-size: 12.5px;
}

.practice-screen-head .is-code b {
  font-weight: var(--fw-bold);
}

/* ------------------------------------------------------- new-key gate
   The teaching card that precedes a lesson's first keystroke. It sits over the
   stage while the keyboard stays visible below — the learner has to look at the
   real keyboard to find the key, so hiding it would defeat the exercise. */

.key-intro {
  margin: 0 auto 22px;
  max-width: 640px;
}

.key-intro.hidden {
  display: none;
}

/* Dim the passage while the gate is up: the drill is not accepting input yet,
   and a full-brightness wall of text competes with the card. */
/* While the new-key card is up, the passage and the input area are hidden
   outright rather than dimmed: there is nothing to type yet, and leaving a
   wall of placeholder text on screen competes with the card for attention.
   The keyboard and hands below it stay fully visible and the layout closes up,
   which is what pulls them into view — the learner is being asked to find a
   key, so the keyboard is the thing they need on screen. */
.eng-stage.is-intro .eng-screen {
  display: none;
}

/* The new-key card. A big keycap in the finger's colour (the same colour the
   keyboard's finger chart uses), the hand and finger named large, one line on
   where the finger starts and which way it reaches, then dots that fill as
   the key is pressed. It eases in, the cap goes down on every press, and the
   finished state holds a moment before the card slides away. */
.ki-card {
  --ki: var(--accent);
  display: flex;
  align-items: center;
  gap: 26px;
  max-width: 640px;
  margin: 0 auto;
  padding: 22px 28px 22px 22px;
  border: 1px solid var(--line);
  border-radius: 20px;
  background:
    radial-gradient(120% 140% at 0% 50%, color-mix(in srgb, var(--ki) 12%, transparent), transparent 55%),
    var(--bg-elevated);
  box-shadow: 0 12px 34px rgba(16, 24, 40, 0.1);
  transition: opacity 0.28s ease, transform 0.28s cubic-bezier(.2, .8, .3, 1), border-color 0.2s;
}
.ki-card.is-enter { opacity: 0; transform: translateY(8px) scale(0.98); }
.ki-card.is-out { opacity: 0; transform: translateY(-6px) scale(0.98); transition-duration: 0.24s; }
.ki-card.is-found { border-color: color-mix(in srgb, var(--green-ink) 55%, var(--line)); }

.ki-keywrap {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 112px;
  height: 112px;
  border-radius: 26px;
  background: color-mix(in srgb, var(--ki) 14%, transparent);
}
.ki-key {
  display: grid;
  place-items: center;
  min-width: 76px;
  height: 76px;
  padding: 0 14px;
  border: 2px solid var(--ki);
  border-radius: 16px;
  background: var(--bg-elevated);
  color: var(--ki);
  font-family: var(--font-mono);
  font-size: 36px;
  font-weight: var(--fw-bold);
  box-shadow: 0 6px 0 color-mix(in srgb, var(--ki) 55%, #000 10%);
  transform: translateY(-3px);
}
.ki-key.is-press { animation: ki-press 0.22s ease-out; }
.ki-key.is-bad { animation: key-shake 0.2s ease-in-out; }
.ki-card.is-found .ki-key {
  border-color: var(--green-ink);
  color: #fff;
  background: var(--green-ink);
  box-shadow: 0 6px 0 color-mix(in srgb, var(--green-ink) 70%, #000);
}
@keyframes ki-press {
  0% { transform: translateY(-3px); box-shadow: 0 6px 0 color-mix(in srgb, var(--ki) 55%, #000 10%); }
  40% { transform: translateY(3px); box-shadow: 0 0 0 color-mix(in srgb, var(--ki) 55%, #000 10%); }
  100% { transform: translateY(-3px); box-shadow: 0 6px 0 color-mix(in srgb, var(--ki) 55%, #000 10%); }
}

.ki-body { min-width: 0; flex: 1 1 auto; }
/* the lesson's other cards (讲解, 游戏关) share the frame */
.ki-lead { color: var(--text-dim); font-size: 12.5px; }
.ki-found-title { color: var(--text-strong); font-size: 21px; font-weight: var(--fw-bold); }
.ki-eyebrow {
  color: var(--text-dim);
  font-size: 12px;
  font-weight: var(--fw-semibold);
  letter-spacing: 0.06em;
}
.ki-title {
  display: flex;
  align-items: baseline;
  gap: 8px;
  margin-top: 2px;
  color: var(--text-strong);
  font-size: 26px;
  font-weight: var(--fw-bold);
  line-height: 1.25;
}
.ki-hand { color: var(--text-muted); font-size: 18px; font-weight: var(--fw-semibold); }
.ki-fname { color: var(--ki); }
.ki-hint {
  margin-top: 6px;
  color: var(--text-muted);
  font-size: 14px;
  line-height: 1.6;
}
.ki-progress {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-top: 14px;
  min-height: 24px;
}
.ki-pips { display: inline-flex; gap: 7px; }
.ki-pip {
  width: 12px;
  height: 12px;
  border: 2px solid color-mix(in srgb, var(--ki) 50%, var(--line));
  border-radius: 50%;
  transition: background 0.15s, border-color 0.15s, transform 0.15s;
}
.ki-pip.is-on { border-color: var(--ki); background: var(--ki); transform: scale(1.15); }
.ki-card.is-found .ki-pip { border-color: var(--green-ink); background: var(--green-ink); }
.ki-feedback {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: var(--text-dim);
  font-size: 13.5px;
  font-weight: var(--fw-semibold);
}
.ki-feedback svg { width: 18px; height: 18px; }
.ki-feedback.is-ok { color: var(--green-ink); font-size: 15px; }
.ki-feedback.is-bad { color: var(--wrong-ink); }

@media (prefers-reduced-motion: reduce) {
  .ki-card, .ki-card.is-out { transition: none; transform: none; }
  .ki-key.is-press, .ki-key.is-bad { animation: none; }
}

@media (max-width: 560px) {
  .ki-card { gap: 16px; padding: 16px; }
  .ki-keywrap { width: 84px; height: 84px; border-radius: 20px; }
  .ki-key { min-width: 58px; height: 58px; font-size: 28px; }
  .ki-title { font-size: 21px; }
}

/* ------------------------------------------------- per-character speed
   Milliseconds per keystroke, slowest first. Coloured by threshold rather than
   by rank, so the reader learns the scale instead of comparing chips: green is
   a key you know, red is one you are still hunting for. */

/* The strip sits under the weak-key row inside the same note card. */
.cs-block {
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid var(--line-soft);
}

.cs-block:first-child {
  margin-top: 0;
  padding-top: 0;
  border-top: 0;
}

.cs-title {
  display: block;
  color: var(--text-dim);
  font-size: 12.5px;
}

.cs-strip {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 10px;
}

.cs-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 9px 5px 8px;
  border: 1px solid var(--line-soft);
  border-radius: 9px;
  background: var(--bg-elevated);
}

.cs-chip b {
  min-width: 18px;
  color: var(--text-strong);
  font-family: var(--font-mono);
  font-size: 13px;
  font-weight: var(--fw-bold);
  text-align: center;
}

.cs-chip > span {
  color: var(--text-muted);
  font-family: var(--font-mono);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}

.cs-chip > span i {
  margin-left: 1px;
  color: var(--text-dim);
  font-style: normal;
  font-size: 10px;
}

.cs-chip.is-slow {
  border-color: var(--red-soft);
  background: var(--red-soft);
}

.cs-chip.is-slow > span,
.cs-chip.is-slow b {
  color: var(--red-ink);
}

.cs-chip.is-fast {
  border-color: var(--green-soft);
  background: var(--green-soft);
}

.cs-chip.is-fast > span,
.cs-chip.is-fast b {
  color: var(--green-ink);
}

.cs-more {
  align-self: center;
  color: var(--text-dim);
  font-size: 12px;
}

/* ---------------------------------------------------------------- stage
   edclub's typing column is 910px wide and sits straight on the page — there is
   no card behind the passage, and the only structure is the keyboard tray. The
   text block is 292px tall, which is about four lines at their line spacing. */

.eng-stage {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 100%;
  max-width: calc(var(--type-width) + 30px);
  margin: 0 auto;
  padding: 76px 15px 30px;
}

/* The passage block. No background, no border, no radius. */
.eng-screen {
  width: 100%;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: transparent;
}

/* Line viewport: fixed height + overflow hidden, as in edclub's .inview — the
   passage slides up one line when the caret leaves the last visible one. */
.inview {
  position: relative;
  height: var(--type-viewport);
  overflow: hidden;
  transition: height 0.2s var(--ease);
}

/* ------------------------------------------------------------- passage
   THE INPUT-AREA STANDARD. Every number below is derived, not picked:

     font      Roboto Mono, self-hosted. Both reference sites use a monospace
               for the passage (typing.com Roboto Mono, edclub Droid Sans Mono);
               Roboto Mono is the one we already ship.
     size      32px — typing.com runs 30px, edclub 35px. 32 is the midpoint and
               keeps ~42 characters on a 910px line, which is what both land on
               (typing.com 45, edclub 43).
     line      60px — the full top-to-top pitch. edclub is 71.5px (35px type on
               a 47px box plus 24px of air), typing.com is 45px (30px type, no
               air). 60px keeps edclub's airiness without its sprawl.
     cell      0.68em = 21.8px, centred. typing.com uses 0.7em (21px at 30px);
               0.68em keeps the same proportion at our size.
     lines     4 visible — edclub shows 4, typing.com 3.
     viewport  4 x 60 = 240px.
     caret     3px underline, one cell wide, #0099ff — typing.com's exactly.

   A word and its trailing space are one unbreakable unit, so a line can never
   split mid-word. typing.com does the same with display:flex. */

.engine {
  --type-font: var(--font-mono);
  --type-size: 32px;
  --type-pitch: 60px;
  --type-cell: 0.68em;
  --type-lines: 4;
  --type-width: 910px;
  --type-viewport: 240px;
}

/* Ruled lines under the passage, the way typing.com draws them: a repeating
   gradient that paints one hairline per line pitch. It is a background rather
   than a border, so it never affects layout. */
.typing-text {
  position: relative;
  background-image: repeating-linear-gradient(
    180deg,
    transparent 0,
    transparent calc(var(--type-pitch) - 1px),
    var(--rule, #d9d9d9) calc(var(--type-pitch) - 1px),
    var(--rule, #d9d9d9) var(--type-pitch)
  );
  font-family: var(--type-font);
  font-size: var(--type-size);
  font-weight: 500;
  line-height: var(--type-pitch);
  letter-spacing: 0;
  color: var(--lesson-ink);
  user-select: none;
  outline: none;
}

.typing-text .word {
  display: inline-block;
  white-space: nowrap;
}

.typing-text .ch {
  display: inline-block;
  min-width: var(--type-cell);
  text-align: center;
  transition: color 0.09s linear;
}

.typing-text .ch.is-pending {
  color: var(--lesson-ink);
}

.typing-text .ch.is-correct {
  color: var(--correct-ink);
}

.typing-text .ch.is-wrong {
  color: var(--wrong-ink);
}

.typing-text .ch.is-current {
  color: var(--accent);
}

.typing-text .ch.is-current.is-wrong {
  color: var(--wrong-ink);
}

/* typing.com shakes the cell on a wrong keystroke. */
.typing-text .ch.is-shake {
  animation: key-shake 0.18s ease-in-out;
}

@keyframes key-shake {
  0%,
  100% {
    transform: translateX(0);
  }
  25% {
    transform: translateX(-3px);
  }
  75% {
    transform: translateX(3px);
  }
}


/* --------------------------------------------------------- word speed
   A finished word's speed, pinned to the word itself. The whole reason it is
   here rather than in the toolbar is that reading it must not cost an eye
   movement away from the line being typed. It fades after a moment so a long
   passage does not end up buried under numbers. */

/* .word must NOT be positioned. The engine places its caret from
   `offsetLeft`/`offsetTop` on the character cells, and those resolve against
   the nearest positioned ancestor — so making the word positioned silently
   re-anchored every one of them to the word instead of the line, which pinned
   the caret to the start of each word. The badge below is positioned from
   .typing-text instead, which is the same frame the caret uses. */

/* ---------------------------------------------------------------- caret
   A 3px underline that slides between cells — typing.com's exact shape and
   colour, with a 150ms ease-out follow. */

/* No transition. A text caret must be instant: with a 150ms ease-out on `left`,
   typing at any normal speed re-triggers the animation before it finishes, so
   the caret spends the whole run chasing the text and never arrives — which
   reads as the cursor being broken. Smoothing long jumps (a line wrap, a
   scroll) is not worth that; an instant jump is what a caret is supposed to do.
   The blink stays, because that one is conventional. */
.cursor {
  position: absolute;
  height: 3px;
  border-radius: 0;
  background: var(--caret);
  pointer-events: none;
}

.cursor.is-blinking {
  animation: caret-pulse 1.1s ease-in-out infinite;
}

@keyframes caret-pulse {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.2;
  }
}

/* ------------------------------------------------------------ start hint */

.eng-hint {
  padding: 8px 0 12px;
  text-align: center;
  color: var(--text-dim);
  font-size: 13px;
}

/* -------------------------------------------------------------- footer */

.eng-footer {
  padding: 18px 18px 24px;
  border-top: 1px solid var(--line-soft);
  text-align: center;
  color: var(--text-dim);
  font-size: 12px;
}

.eng-footer a {
  color: var(--text-muted);
}

.eng-footer a:hover {
  color: var(--accent-ink);
}

.eng-footer span + span::before {
  content: '·';
  margin: 0 8px;
  color: var(--line);
}

/* --------------------------------------------------------------- results
   What the screen has to answer, in order:

     1. how did I do          two big numbers, nothing competing with them
     2. is that good          the level verdict and the gauge
     3. the details           one quiet strip, not four shouting cards
     4. what now              the actions

   The old version printed the speed twice — once as a headline and again as a
   hero figure — and split the details into four equal cards that read as four
   separate facts. Here the number appears once, the details share one strip with
   hairline dividers, and the verdict carries the headline. */

.result {
  width: 100%;
  max-width: 660px;
  margin: 0 auto;
  padding: 4px 0 30px;
}

/* ---- 1. the two numbers ------------------------------------------------ */

.result-hero {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px;
  margin-bottom: 20px;
}

.result-big {
  padding: 24px 18px 20px;
  border: 1px solid var(--line-soft);
  border-radius: var(--radius-card);
  background: var(--bg-elevated);
  text-align: center;
}

.result-big b {
  display: block;
  color: var(--accent-ink);
  font-family: var(--font-mono);
  font-size: 58px;
  font-weight: var(--fw-bold);
  font-variant-numeric: tabular-nums;
  line-height: 1;
  letter-spacing: -0.02em;
}

.result-big.is-acc b {
  color: var(--green-ink);
}

.result-big span {
  display: block;
  margin-top: 10px;
  color: var(--text-dim);
  font-size: 11px;
  font-weight: var(--fw-bold);
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

/* A personal best gets said out loud, once. */
.result-big.is-best {
  border-color: var(--gold-400);
  background: linear-gradient(180deg, var(--gold-100), var(--bg-elevated) 70%);
}

.result-best-flag {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  margin-top: 9px;
  padding: 3px 9px;
  border-radius: 999px;
  background: var(--gold-300);
  color: var(--gold-ink);
  font-size: 11px;
  font-weight: var(--fw-bold);
}

.result-best-flag svg {
  width: 12px;
  height: 12px;
}

/* ---- stars ------------------------------------------------------------- */

.result-stars {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  margin-bottom: 16px;
}

.result-stars svg {
  width: 26px;
  height: 26px;
  color: var(--line);
}

.result-stars svg.is-on {
  color: var(--gold-400);
}

/* ---- 2. how it compares with last time --------------------------------- */

/* Sits directly under the two headline numbers: "am I getting better" is the
   next question anyone asks, so it must not be buried under the details. */
.result-delta {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  flex-wrap: wrap;
  margin: -8px 0 18px;
  padding: 10px 14px;
  border-radius: var(--radius-card);
  background: var(--bg-soft);
  color: var(--text-muted);
  font-size: 13.5px;
}

.result-delta b {
  color: inherit;
  font-family: var(--font-mono);
  font-weight: var(--fw-bold);
  font-variant-numeric: tabular-nums;
}

.result-delta svg {
  width: 15px;
  height: 15px;
  flex: 0 0 auto;
}

.result-delta.is-up {
  background: var(--green-soft);
  color: var(--green-ink);
}

.result-delta.is-down {
  background: var(--red-soft);
  color: var(--red-ink);
}

.result-delta.is-first {
  color: var(--text-dim);
}

/* The reference point, quieter than the comparison itself. */
.result-delta-ref {
  padding-left: 9px;
  border-left: 1px solid currentColor;
  opacity: 0.55;
  font-size: 12.5px;
  font-variant-numeric: tabular-nums;
}

/* ---- 3. the verdict ---------------------------------------------------- */

.result-verdict {
  margin-bottom: 8px;
  text-align: center;
}

.result-verdict h2 {
  color: var(--text-strong);
  font-size: 21px;
  font-weight: var(--fw-bold);
}

.result-verdict p {
  margin-top: 6px;
  color: var(--text-muted);
  font-size: 13.5px;
  line-height: 1.6;
}

.result-verdict p b {
  color: var(--text-strong);
  font-weight: var(--fw-semibold);
}

.result-meta {
  margin-top: 12px;
  color: var(--text-dim);
  font-size: 12px;
  text-align: center;
}

.result-meta span + span::before {
  content: '·';
  margin: 0 7px;
  color: var(--line);
}

/* ---- 4. the details, as one strip -------------------------------------- */

.result-strip {
  display: grid;
  /* One row whatever the count: the lesson screen has four cells, the test
     screen three, and a fixed three-column grid wrapped the fourth alone. */
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  margin: 22px 0;
  border-top: 1px solid var(--line-soft);
  border-bottom: 1px solid var(--line-soft);
}

.result-strip > div {
  padding: 14px 8px;
  text-align: center;
}

.result-strip > div + div {
  border-left: 1px solid var(--line-soft);
}

.result-strip b {
  display: block;
  color: var(--text-strong);
  font-family: var(--font-mono);
  font-size: 18px;
  font-weight: var(--fw-semibold);
  font-variant-numeric: tabular-nums;
  line-height: 1.2;
}

.result-strip span {
  display: block;
  margin-top: 3px;
  color: var(--text-dim);
  font-size: 11.5px;
}

/* ---- notes ------------------------------------------------------------- */

.result-note {
  margin-top: 12px;
  padding: 13px 15px;
  border: 1px solid var(--line-soft);
  border-radius: var(--radius-card);
  background: var(--bg-soft);
  color: var(--text);
  font-size: 13.5px;
  line-height: 1.65;
}

.result-note.is-gold {
  border-color: var(--gold-400);
  background: var(--gold-100);
  color: var(--gold-ink);
}

.weak-keys {
  display: flex;
  align-items: center;
  gap: 7px;
  flex-wrap: wrap;
  margin-top: 10px;
}

.weak-keys > span {
  color: var(--text-dim);
  font-size: 12.5px;
}

.weak-keys kbd {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 30px;
  height: 30px;
  padding: 0 7px;
  border: 1px solid var(--line);
  border-bottom-width: 2px;
  border-radius: var(--radius-ctl);
  background: var(--bg-elevated);
  color: var(--wrong-ink);
  font-family: var(--font-mono);
  font-size: 13px;
  font-weight: var(--fw-semibold);
}

/* ---- 4. what now ------------------------------------------------------- */

.result-actions {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  flex-wrap: wrap;
  margin-top: 26px;
}

@media (max-width: 560px) {
  .result-big b {
    font-size: 42px;
  }

  .result-strip {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .result-strip > div:nth-child(3) {
    border-left: 0;
  }

  .result-strip > div:nth-child(n + 3) {
    border-top: 1px solid var(--line-soft);
  }
}

/* ------------------------------------------------------ countdown splash */

.countdown {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--bg);
}

.countdown b {
  color: var(--accent);
  font-size: 110px;
  font-weight: var(--fw-bold);
  line-height: 1;
  animation: pop 0.6s var(--ease) both;
}

@keyframes pop {
  from {
    opacity: 0;
    transform: scale(0.7);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}

/* ------------------------------------------------------------ responsive */

@media (max-width: 860px) {
  .engine {
    --type-size: 22px;
    --type-pitch: 40px;
    --type-viewport: 160px;
  }

  .eng-stage {
    padding: 72px 12px 24px;
  }

  .result-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .eng-toolbar {
    grid-template-columns: minmax(0, 1fr) auto;
  }

  /* Drop the title, not the numbers. The title is repeated in the page heading
     right below this bar; the live readout appears nowhere else. Hiding the
     numbers to make room for something already on screen was the wrong trade —
     and at this width it made them vanish with no way to get them back. */
  .eng-toolbar-left {
    display: none;
  }
}

@media (max-width: 560px) {
  .eng-toolbar {
    gap: 8px;
    padding: 0 10px;
  }

  .eng-toolbar-title span {
    display: none;
  }

  .eng-seg {
    display: none;
  }
}

@media (max-width: 560px) {
  .result-big b {
    font-size: 40px;
  }
}

/* The workspace gives the exercise a clear entry point without competing
   with the passage. Theme tokens keep the same hierarchy in dark mode. */
.practice-workspace { --bar-h: 64px; background: var(--bg-soft); }
.practice-workspace .eng-toolbar { background: var(--bg-elevated); border-bottom: 1px solid var(--line-soft); }
.practice-workspace .eng-progress { height: 3px; padding: 0; background: var(--line-soft); }
.practice-workspace .eng-progress > i { height: 3px; }
.practice-workspace .eng-stage { max-width: 990px; padding: 104px 20px 28px; }
.practice-heading { width: 100%; padding: 0 20px 24px; }
.practice-eyebrow { color: var(--accent-ink); font-size: 12px; font-weight: 700; letter-spacing: .06em; }
.practice-heading-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin: 10px 0; }
.practice-heading h1 { color: var(--text-strong); font-size: clamp(22px, 3vw, 28px); letter-spacing: -.035em; line-height: 1.35; }
.practice-description { color: var(--text-muted); font-size: 13px; line-height: 1.7; max-width: 680px; }
.practice-state { display: inline-flex; align-items: center; gap: 7px; flex-shrink: 0; color: var(--text-muted); font-size: 12px; }
.practice-state::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: var(--text-dim); }
.practice-state.is-active::before { background: var(--green); box-shadow: 0 0 0 4px var(--green-soft); }
.practice-workspace .eng-screen { position: relative; border: 1px solid var(--line); border-radius: 16px; padding: 0 19px; background: var(--bg-elevated); box-shadow: 0 3px 14px rgba(16,24,40,.025); }
.practice-screen-head { display: flex; justify-content: space-between; align-items: center; gap: 16px; min-height: 46px; border-bottom: 1px solid var(--line-soft); color: var(--text-dim); font-size: 12px; }
.practice-screen-head #progressText { flex-shrink: 0; font-family: var(--font-mono); font-size: 11px; font-variant-numeric: tabular-nums; }
.practice-workspace .eng-hint { min-height: 42px; padding: 12px 0; font-size: 12px; }
.practice-workspace .typing-text .is-newline { color: var(--text-dim); }
/* Only the glyph shrinks — see the comment in engine.js render().
   `line-height: 1` is load-bearing: the ↵ glyph is not in Roboto Mono, so it
   falls back to a system face whose metrics, combined with the inherited 60px
   line-height at a 19.2px font size, made the cell 65px tall instead of 60.
   That stretched the row it sits on and pushed every later row 5px down, which
   is what made the line viewport scroll to irregular offsets. Collapsing the
   glyph's own line box leaves the cell's strut to set the height. */
.practice-workspace .typing-text .is-newline .nl-glyph { font-size: .6em; line-height: 1; }
.practice-workspace .typing-text .is-newline.is-current { color: var(--accent); }
.practice-workspace .typing-text .is-wrong { background: var(--red-soft); border-radius: 3px; }
.practice-workspace .typing-text .word { max-width: 100%; white-space: normal; overflow-wrap: anywhere; }
.practice-input { position: absolute; opacity: 0; pointer-events: none; width: 2px; height: 20px; left: 20px; top: 64px; resize: none; padding: 0; border: 0; font-size: 16px; line-height: 20px; overflow: hidden; white-space: nowrap; }
.practice-resume { position: absolute; inset: 0; z-index: 5; width: 100%; border: 0; background: var(--bg-elevated); color: var(--text-strong); cursor: pointer; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; }
.practice-resume b { font-size: 20px; font-weight: 600; }
.practice-resume span { color: var(--text-muted); font-size: 13px; }
.practice-resume:focus-visible { outline: 2px solid var(--accent); outline-offset: -4px; border-radius: 8px; }
.practice-keyboard-label { width: 100%; max-width: 910px; display: flex; justify-content: space-between; margin: 24px 0 14px; font-size: 12px; color: var(--text-muted); }
.practice-keyboard-label span { color: var(--text-dim); }
.practice-workspace #keyboardMount { width: 100%; max-width: 910px; align-self: center; padding-bottom: 26px; overflow: clip; }
.practice-workspace .eng-footer { border: 0; padding-top: 4px; }
.eng-focus-toggle { width: auto; min-width: 48px; padding: 0 10px; font-size: 12px; white-space: nowrap; }
.eng-test-label { color: var(--text-dim); font-size: 12px; }
.is-focused .practice-keyboard-label, .has-no-keyboard .practice-keyboard-label { display: none; }
.is-focused .eng-screen { margin-top: 20px; }
.practice-workspace .eng-tool:focus-visible, .practice-workspace .eng-back:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
@media (max-width: 1100px) {
  .practice-workspace .eng-toolbar { grid-template-columns: auto minmax(0, 1fr) auto; gap: 8px; }
  .practice-workspace .eng-toolbar-left { display: flex; }
  .practice-workspace .eng-title, .practice-workspace .eng-toolbar-left > div, .eng-test-label { display: none; }
}
@media (min-width: 861px) and (max-height: 850px) {
  .practice-workspace { --type-pitch: 48px; --type-viewport: 192px; }
  .practice-workspace .eng-stage { padding-top: 84px; }
  .practice-heading { padding-bottom: 16px; }
  .practice-heading-row { margin: 6px 0; }
  .practice-heading h1 { font-size: 24px; }
  .practice-screen-head { min-height: 36px; }
  .practice-workspace .eng-hint { min-height: 34px; padding: 8px 0; }
  .practice-keyboard-label { margin: 16px 0 10px; }
}
@media (max-width: 600px) {
  .practice-workspace { --bar-h: 104px; }
  .practice-workspace .eng-toolbar { display: flex; flex-wrap: wrap; gap: 2px; padding: 8px 12px; align-content: center; }
  .practice-workspace .eng-toolbar-left { width: auto; flex: 0 0 auto; }
  /* The bar wraps at this width, so the readout gets its own row and stays. */
  .practice-workspace .eng-live { order: 3; width: 100%; justify-content: flex-start; }
  .practice-workspace .eng-toolbar { row-gap: 4px; }
  .practice-workspace .eng-toolbar-right { width: auto; flex: 1; flex-wrap: wrap; justify-content: flex-end; gap: 2px; }
  .practice-workspace .eng-stage { padding: 132px 12px 20px; }
  .practice-heading { padding: 0 4px 18px; }
  .practice-heading-row { gap: 10px; align-items: flex-start; }
  .practice-state { margin-top: 6px; }
  .practice-workspace .eng-screen { padding: 0 12px; border-radius: 12px; }
  .practice-screen-head { font-size: 11px; }
  .practice-workspace .eng-tool { min-height: 36px; }
  .practice-keyboard-label { margin-top: 20px; }
  .practice-workspace .eng-footer { font-size: 11px; }
  .practice-workspace #keyboardMount { overflow-x: auto; }
}
@media (prefers-reduced-motion: reduce) {
  .practice-workspace *, .practice-workspace *::before { animation: none !important; transition: none !important; }
}

/* ---- growth: XP, level bar, today's goal -------------------------------- */

.result-growth {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: 18px;
  margin-top: 20px;
  padding: 14px 18px;
  border: 1px solid var(--line-soft);
  border-radius: 14px;
  background: var(--bg-elevated);
}

.result-growth.is-levelup {
  border-color: var(--gold-300);
  background: linear-gradient(90deg, var(--gold-soft), var(--bg-elevated) 70%);
}

.rg-xp {
  display: flex;
  align-items: baseline;
  gap: 3px;
  color: var(--gold-ink);
}

html[data-theme='dark'] .rg-xp {
  color: var(--gold-400);
}

.rg-xp b {
  font-size: 24px;
  font-weight: var(--fw-bold);
  font-variant-numeric: tabular-nums;
}

.rg-xp span {
  font-size: 12px;
  font-weight: var(--fw-bold);
}

.rg-level {
  display: flex;
  flex-direction: column;
  gap: 7px;
  min-width: 0;
}

.rg-level-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
}

.rg-level-head b {
  color: var(--text-strong);
  font-size: 14px;
}

.rg-level-head span {
  color: var(--text-dim);
  font-size: 12px;
  white-space: nowrap;
}

.result-growth .ps-xp i {
  transition: width 1.1s cubic-bezier(0.2, 0.8, 0.2, 1) 0.25s;
}

.rg-goal {
  display: flex;
  align-items: center;
  gap: 10px;
  color: var(--text-dim);
  font-size: 12px;
  line-height: 1.35;
}

.rg-goal .pg-ring {
  position: relative;
  gap: 0;
}

.rg-goal .pg-ring svg {
  width: 48px;
  height: 48px;
}

.rg-goal .pg-ring-text {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
}

.rg-goal .pg-ring-text b {
  font-size: 12px;
}

.rg-goal .pg-ring-text b small {
  color: var(--text-dim);
  font-size: 9px;
}

.rg-goal .pg-ring-text span {
  display: none;
}

@media (max-width: 560px) {
  .result-growth {
    grid-template-columns: minmax(0, 1fr);
  }
  .rg-goal {
    grid-column: 1 / -1;
  }
}

/* ---- celebration -------------------------------------------------------- */

.result-stars {
  position: relative;
}

.result-stars svg {
  animation: star-pop 0.45s cubic-bezier(0.3, 1.6, 0.5, 1) both;
}

.result-stars svg:nth-child(2) { animation-delay: 0.08s; }
.result-stars svg:nth-child(3) { animation-delay: 0.16s; }
.result-stars svg:nth-child(4) { animation-delay: 0.24s; }
.result-stars svg:nth-child(5) { animation-delay: 0.32s; }

@keyframes star-pop {
  from { opacity: 0; transform: scale(0.2) rotate(-40deg); }
  to { opacity: 1; transform: scale(1) rotate(0); }
}

.result-burst {
  position: absolute;
  left: 50%;
  top: 50%;
  pointer-events: none;
}

.result-burst i {
  position: absolute;
  width: 7px;
  height: 7px;
  margin: -3.5px;
  border-radius: 2px;
  background: var(--gold-400);
  opacity: 0;
  animation: spark 0.9s ease-out both;
}

.result-burst i:nth-child(3n) { background: var(--accent); border-radius: 50%; }
.result-burst i:nth-child(4n) { background: var(--green); }

@keyframes spark {
  0% { opacity: 1; transform: translate(0, 0) scale(1) rotate(0); }
  100% { opacity: 0; transform: translate(var(--dx), var(--dy)) scale(0.4) rotate(180deg); }
}

@media (prefers-reduced-motion: reduce) {
  .result-stars svg { animation: none; }
  .result-growth .ps-xp i { transition: none; }
}

.result-keys {
  margin-top: 12px;
  color: var(--text-dim);
  font-size: 12px;
  text-align: center;
}

.result-keys kbd {
  display: inline-block;
  padding: 1px 6px;
  border: 1px solid var(--line);
  border-bottom-width: 2px;
  border-radius: 5px;
  background: var(--bg-elevated);
  color: var(--text-muted);
  font-family: var(--font-mono);
  font-size: 11px;
}

/* ======================================================================
   Practice layout, second pass (after typing.com / edclub).

   The eyes are on the passage, so the numbers that matter while typing sit on
   the passage's top edge — not in a quiet line in the toolbar, where nobody
   looks mid-sentence. And the title block above the passage is one tight row:
   the toolbar already names the lesson, and three stacked headings pushed the
   bottom row of the keyboard off a 900px-tall screen.
   ====================================================================== */

.practice-workspace .eng-stage {
  padding-top: 84px;
}

.practice-heading {
  padding-bottom: 14px;
}

.practice-heading-row {
  margin: 4px 0 2px;
}

.practice-heading h1 {
  font-size: clamp(19px, 2.2vw, 22px);
}

.practice-description {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}

.practice-screen-head {
  min-height: 58px;
  gap: 20px;
}

.practice-screen-head .eng-live {
  justify-content: flex-start;
  gap: 22px;
  color: var(--text-dim);
}

.practice-screen-head .eng-live span {
  display: inline-flex;
  align-items: baseline;
  gap: 4px;
}

.practice-screen-head .eng-live b {
  color: var(--text-strong);
  font-size: 20px;
  font-weight: var(--fw-bold);
}

.practice-screen-head .eng-live .live-main b {
  color: var(--accent-ink);
  font-size: 26px;
}

.practice-screen-head .eng-live small {
  font-size: 12px;
}

.screen-side {
  display: inline-flex;
  align-items: center;
  gap: 14px;
  margin-left: auto;
  min-width: 0;
  color: var(--text-dim);
  font-size: 12px;
  text-align: right;
}

.screen-side > span:first-child {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The test's countdown leads the row: time left is the one number a test
   taker checks. */
.practice-screen-head .eng-clock {
  height: 40px;
  min-width: 88px;
  margin-right: 0;
  font-size: 22px;
}

@media (max-width: 720px) {
  /* The live row already carries 进度 %; the "30 / 99" counter had no room
     left and was drawn underneath the WPM figure. */
  .screen-side {
    display: none;
  }
  .practice-screen-head .eng-live {
    gap: 14px;
  }
  .practice-screen-head .eng-live b {
    font-size: 16px;
  }
  .practice-screen-head .eng-live .live-main b {
    font-size: 20px;
  }
}

/* Heading as one line: where you are and what this is. The lesson tip is in
   the new-key card and the toolbar carries the target, so the paragraph under
   the title only pushed the keyboard down. */
.practice-heading {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 4px 14px;
}

.practice-heading .practice-eyebrow {
  order: 0;
}

.practice-heading-row {
  flex: 1 1 auto;
  margin: 0;
}

.practice-heading .practice-description {
  display: none;
}

.practice-workspace .keyboard-mount {
  margin-top: 18px;
}


/* ======================================================= round 5: the way out
   A labelled exit that says where it goes, and a menu of the whole site next
   to it. The bare chevron was a 34px grey glyph with no words. */

.eng-back.has-label {
  width: auto;
  height: 36px;
  gap: 6px;
  padding: 0 12px 0 9px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--bg-elevated);
  color: var(--text-strong);
  font-size: 13px;
  font-weight: var(--fw-semibold);
  white-space: nowrap;
}

.eng-back.has-label:hover {
  border-color: var(--accent-line);
}

.eng-back-label {
  line-height: 1;
}

.eng-menu-btn {
  width: 36px;
  height: 36px;
}

.eng-toolbar-left {
  position: relative;
}

.eng-menu {
  position: absolute;
  top: calc(100% + 12px);
  left: 0;
  z-index: 1100;
  width: 260px;
  padding: 8px;
  border: 1px solid var(--line-soft);
  border-radius: 14px;
  background: var(--bg-elevated);
  box-shadow: 0 18px 44px rgba(15, 35, 60, 0.16), 0 2px 6px rgba(15, 35, 60, 0.06);
}

.eng-menu-title {
  padding: 6px 10px 8px;
  color: var(--text-dim);
  font-size: 11.5px;
  font-weight: var(--fw-semibold);
  letter-spacing: 0.04em;
}

.eng-menu a {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 10px;
  border-radius: 9px;
  color: var(--text-strong);
  font-size: 14px;
  text-decoration: none;
}

.eng-menu a svg {
  width: 17px;
  height: 17px;
  color: var(--text-muted);
}

.eng-menu a:hover,
.eng-menu a:focus-visible {
  outline: none;
  background: var(--accent-tint);
  color: var(--accent-ink);
}

.eng-menu a:hover svg,
.eng-menu a:focus-visible svg {
  color: var(--accent);
}

.eng-menu a.is-here {
  color: var(--accent-ink);
}

.eng-menu a small {
  margin-left: auto;
  color: var(--text-dim);
  font-size: 11px;
}

.eng-menu-keys {
  margin-top: 6px;
  padding: 9px 10px 4px;
  border-top: 1px solid var(--line-soft);
  color: var(--text-dim);
  font-size: 11.5px;
  line-height: 1.8;
}

.eng-menu-keys kbd,
.resume-actions kbd,
.rs kbd,
.rs-bar kbd {
  display: inline-block;
  min-width: 18px;
  padding: 0 5px;
  border: 1px solid currentColor;
  border-bottom-width: 2px;
  border-radius: 5px;
  font-family: var(--font-mono);
  font-size: 10.5px;
  line-height: 16px;
  text-align: center;
  opacity: 0.7;
}

/* The pause panel now offers both ways forward, each with its key. */
.practice-resume {
  cursor: pointer;
}

.practice-resume:focus {
  outline: none;
}

/* The panel lies over the line viewport, and a one- or two-line lesson sizes
   that viewport to its own text (engine.js measure: is-short) — too short for
   title + note + buttons, which then got cut off at the bottom. While the panel
   is up the viewport is at least as tall as the panel. */
.is-unfocused .inview { min-height: 190px; }

.resume-actions {
  display: flex;
  gap: 10px;
  margin-top: 6px;
}

.practice-resume .resume-actions .btn {
  color: inherit;
  font-size: 13px;
}

.practice-resume .resume-actions .btn-gold {
  color: var(--gold-ink);
}

@media (max-width: 600px) {
  .eng-back-label {
    display: none;
  }
  .eng-back.has-label {
    width: 36px;
    padding: 0;
  }
}

/* =================================================== round 5: word rhythm
   Finished words get a brief tint in their speed tier; slow words keep a faint
   amber wash so hesitations show when you look back; the numbers themselves
   live in the rhythm strip in the screen head. No badges on the text. */

.typing-text .word.ws-flash {
  border-radius: 6px;
  animation: ws-flash 0.9s ease-out both;
  --ws-c: rgba(13, 118, 201, 0.14);
}

.typing-text .word.ws-flash.is-fast {
  --ws-c: rgba(113, 177, 107, 0.26);
}

.typing-text .word.ws-flash.is-slow {
  --ws-c: rgba(241, 188, 63, 0.34);
}

@keyframes ws-flash {
  from { background-color: var(--ws-c); }
  to { background-color: transparent; }
}

.typing-text .word.ws-hesitant {
  background-image: linear-gradient(transparent 16%, rgba(241, 188, 63, 0.16) 16%, rgba(241, 188, 63, 0.16) 88%, transparent 88%);
}

.ws-strip {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-left: 4px;
  padding-left: 18px;
  border-left: 1px solid var(--line-soft);
}

.ws-bars {
  display: flex;
  align-items: flex-end;
  justify-content: flex-end;
  gap: 2px;
  width: 124px;
  height: 24px;
}

.ws-bars i {
  flex: 0 0 5px;
  border-radius: 2px;
  background: var(--accent-mid);
  transition: height 0.2s ease-out;
}

.ws-bars i.is-fast { background: var(--green); }
.ws-bars i.is-slow { background: var(--gold-500); }
.ws-bars i.is-last { box-shadow: 0 0 0 1.5px var(--bg-elevated), 0 0 0 3px currentColor; color: var(--accent-line); }

.ws-last {
  display: inline-flex;
  align-items: baseline;
  gap: 3px;
  min-width: 52px;
}

.ws-last b {
  color: var(--text-strong);
  font-family: var(--font-mono);
  font-size: 16px;
  font-weight: var(--fw-bold);
  font-variant-numeric: tabular-nums;
}

.ws-last.is-fast b { color: var(--green-ink); }
.ws-last.is-slow b { color: var(--gold-600); }

.ws-last small {
  color: var(--text-dim);
  font-size: 11px;
}

.ws-strip.is-empty {
  visibility: hidden;
}

.no-word-speed .ws-strip {
  display: none;
}

.no-word-speed .typing-text .word.ws-flash,
.no-word-speed .typing-text .word.ws-hesitant {
  animation: none;
  background: none;
}

@media (max-width: 900px) {
  .ws-strip { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .typing-text .word.ws-flash { animation: none; }
}

/* ===================================================== round 6: result view
   A dark dial band as the focal point, three short tiles, details behind a
   disclosure, and a fixed action bar. See assets/js/result-view.js. */

.rs {
  width: 100%;
  max-width: 1040px;
  margin: 0 auto;
  padding: 0 0 36px;
  outline: none;
}

/* ---- the band ------------------------------------------------------------ */

.rs-hero {
  --rs-ink: #ffffff;
  --rs-dim: rgba(255, 255, 255, 0.66);
  --rs-track: rgba(255, 255, 255, 0.14);
  --rs-tick: rgba(255, 255, 255, 0.28);
  position: relative;
  border-radius: 22px;
  background:
    radial-gradient(120% 90% at 50% 0%, rgba(255, 255, 255, 0.10), transparent 60%),
    linear-gradient(165deg, #1d4f86 0%, #163d6b 55%, #102c50 100%);
  box-shadow: 0 24px 60px -24px rgba(16, 44, 80, 0.55);
  color: var(--rs-ink);
  text-align: center;
  overflow: hidden;
}

.rs-hero.is-best {
  background:
    radial-gradient(120% 90% at 50% 0%, rgba(244, 201, 101, 0.22), transparent 60%),
    linear-gradient(165deg, #1d4f86 0%, #163d6b 55%, #102c50 100%);
}

.rs-hero.is-fail {
  background:
    radial-gradient(120% 90% at 50% 0%, rgba(255, 255, 255, 0.08), transparent 60%),
    linear-gradient(165deg, #3a4a5e 0%, #2b394b 60%, #202c3b 100%);
}

.rs-hero-in {
  padding: 28px 28px 26px;
}

.rs-stars {
  margin-bottom: 8px;
}

.rs-stars svg {
  width: 34px;
  height: 34px;
  color: rgba(255, 255, 255, 0.18);
  filter: drop-shadow(0 2px 3px rgba(0, 0, 0, 0.25));
}

.rs-stars svg.is-on {
  color: #f4c965;
}

.rs-stars svg:nth-child(2),
.rs-stars svg:nth-child(4) {
  transform: translateY(-6px);
}

.rs-stars svg:nth-child(3) {
  width: 40px;
  height: 40px;
  transform: translateY(-10px);
}

.rs-eyebrow {
  color: var(--rs-dim);
  font-size: 12.5px;
  font-weight: var(--fw-semibold);
  letter-spacing: 0.02em;
}

.rs-title {
  margin-top: 6px;
  color: var(--rs-ink);
  font-size: 28px;
  font-weight: var(--fw-bold);
  line-height: 1.3;
}

.rs-title-aside {
  margin-left: 12px;
  padding: 3px 10px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.12);
  color: #bfe0ff;
  font-size: 14px;
  font-weight: var(--fw-semibold);
  vertical-align: 4px;
}

.rs-sub {
  margin-top: 6px;
  color: var(--rs-dim);
  font-size: 13.5px;
}

.rs-sub b {
  color: var(--rs-ink);
}

/* ---- dials -------------------------------------------------------------- */

.rs-dials {
  display: flex;
  align-items: flex-end;
  justify-content: center;
  gap: 34px;
  margin: 18px 0 6px;
}

.rs-dial {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 164px;
  --arc: #7cc4ff;
}

.rs-dial.is-main {
  width: 210px;
  --arc: #f4c965;
}

.rs-dial.is-good { --arc: #7fd08a; }
.rs-dial.is-bad { --arc: #ff9f8a; }
.rs-dial.is-time { --arc: rgba(255, 255, 255, 0.55); }

.rs-dial-svg {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
}

.rs-dial-track,
.rs-dial-fill {
  fill: none;
  stroke-width: 10;
  stroke-linecap: round;
}

.rs-dial.is-main .rs-dial-track,
.rs-dial.is-main .rs-dial-fill {
  stroke-width: 12;
}

.rs-dial-track {
  stroke: var(--rs-track);
}

.rs-dial-fill {
  stroke: var(--arc);
  filter: drop-shadow(0 0 6px color-mix(in srgb, var(--arc) 45%, transparent));
  transition: stroke-dashoffset 1.1s cubic-bezier(0.2, 0.8, 0.2, 1) 0.15s;
}

.rs-dial-ticks line {
  stroke: var(--rs-tick);
  stroke-width: 1.4;
}

.rs-dial-target {
  stroke: #ffffff;
  stroke-width: 3;
  stroke-linecap: round;
}

.rs-dial-read {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  aspect-ratio: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
}

.rs-dial-read b {
  color: var(--rs-ink);
  font-family: var(--font-mono);
  font-size: 40px;
  font-weight: var(--fw-bold);
  font-variant-numeric: tabular-nums;
  line-height: 1;
  letter-spacing: -0.02em;
}

.rs-dial.is-main .rs-dial-read b {
  font-size: 62px;
}

.rs-dial-read b small {
  margin-left: 1px;
  color: var(--rs-dim);
  font-size: 0.42em;
  font-weight: var(--fw-semibold);
}

.rs-dial-sub {
  color: #f4c965;
  font-size: 12.5px;
  font-weight: var(--fw-semibold);
}

.rs-dial-label {
  margin-top: -12px;
  color: var(--rs-ink);
  font-size: 13.5px;
  font-weight: var(--fw-bold);
}

.rs-dial-note {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  min-height: 20px;
  margin-top: 3px;
  color: var(--rs-dim);
  font-size: 12px;
}

.rs-dial-note em {
  font-style: normal;
  font-weight: var(--fw-bold);
}

.rs-dial-note em.is-ok { color: #7fd08a; }
.rs-dial-note em.is-miss { color: #ff9f8a; }

.rs-flag {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin: 8px 0 4px;
  padding: 6px 14px;
  border-radius: 999px;
  background: linear-gradient(180deg, #f7d78c, #f1bc3f);
  color: #4a3a0e;
  font-size: 13px;
  font-weight: var(--fw-bold);
  box-shadow: 0 6px 18px -6px rgba(241, 188, 63, 0.7);
}

.rs-flag svg {
  width: 15px;
  height: 15px;
}

.rs-hero .result-delta {
  display: inline-flex;
  margin: 10px auto 0;
  padding: 7px 14px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.1);
  color: var(--rs-dim);
}

.rs-hero .result-delta.is-up {
  background: rgba(127, 208, 138, 0.16);
  color: #9fe0a8;
}

.rs-hero .result-delta.is-down {
  background: rgba(255, 159, 138, 0.14);
  color: #ffb8a8;
}

.rs-hero .result-delta b {
  color: #ffffff;
}

/* The guest invitation sits between the verdict and the tiles. */
.rs-guest { margin-top: 16px; }

/* ---- tiles -------------------------------------------------------------- */

.rs-tiles {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  /* tiles take their own height: stretching the short 最需要练的键 tile to match
     这次的细节 left ~200px of blank card under its one line (2026-10-11) */
  align-items: start;
  gap: 14px;
  margin-top: 16px;
}

.rs-tiles[data-count='2'] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.rs-tiles[data-count='1'] { grid-template-columns: minmax(0, 1fr); }

.rs-tile {
  min-width: 0;
  padding: 16px 18px;
  border: 1px solid var(--line-soft);
  border-radius: var(--radius-card-lg);
  background: var(--bg-elevated);
}

.rs-tile h3,
.rs-detail h3 {
  display: flex;
  align-items: center;
  gap: 7px;
  margin-bottom: 12px;
  color: var(--text-strong);
  font-size: 13.5px;
  font-weight: var(--fw-bold);
}

.rs-tile h3 svg {
  width: 16px;
  height: 16px;
  color: var(--accent);
}

.rs-tile .weak-keys {
  margin-top: 0;
}

.rs-tile .result-growth {
  grid-template-columns: auto minmax(0, 1fr);
  gap: 12px;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
}

.rs-tile .rg-goal {
  grid-column: 1 / -1;
}

.rs-facts {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 8px;
}

.rs-facts.is-wide {
  grid-template-columns: repeat(4, minmax(0, 1fr));
}

.rs-facts > div {
  padding: 10px 8px;
  border-radius: 10px;
  background: var(--bg-soft);
  text-align: center;
}

.rs-facts b {
  display: block;
  color: var(--text-strong);
  font-family: var(--font-mono);
  font-size: 19px;
  font-weight: var(--fw-bold);
  font-variant-numeric: tabular-nums;
}

.rs-facts span {
  color: var(--text-dim);
  font-size: 11.5px;
}

.rs-hint {
  margin-top: 10px;
  color: var(--text-dim);
  font-size: 12px;
  line-height: 1.6;
}

.rs-tip {
  color: var(--text);
  font-size: 13.5px;
  line-height: 1.7;
}

.rs-badges {
  margin-top: 12px;
  color: var(--gold-ink);
  font-size: 13px;
  font-weight: var(--fw-semibold);
}

html[data-theme='dark'] .rs-badges { color: var(--gold-500); }

.rs-words {
  display: grid;
  gap: 7px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.rs-words li {
  display: grid;
  grid-template-columns: 92px minmax(0, 1fr) 34px;
  align-items: center;
  gap: 10px;
  font-size: 13px;
}

.rs-words span {
  overflow: hidden;
  color: var(--text-strong);
  font-family: var(--font-mono);
  text-overflow: ellipsis;
  white-space: nowrap;
}

.rs-words i {
  height: 8px;
  border-radius: 4px;
  background: var(--gold-400);
}

.rs-words b {
  color: var(--text-muted);
  font-family: var(--font-mono);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  text-align: right;
}

/* ---- details ------------------------------------------------------------ */

.rs-details {
  margin-top: 14px;
  border: 1px solid var(--line-soft);
  border-radius: var(--radius-card-lg);
  background: var(--bg-elevated);
}

.rs-details summary {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 14px 18px;
  color: var(--text-strong);
  font-size: 13.5px;
  font-weight: var(--fw-bold);
  cursor: pointer;
  list-style: none;
}

.rs-details summary::-webkit-details-marker { display: none; }

.rs-details summary svg {
  width: 16px;
  height: 16px;
  color: var(--accent);
}

.rs-details summary small {
  overflow: hidden;
  color: var(--text-dim);
  font-size: 12px;
  font-weight: normal;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* its chevron: base.css, with every other section that opens */

.rs-detail {
  padding: 4px 18px 18px;
}

.rs-detail + .rs-detail {
  padding-top: 16px;
  border-top: 1px solid var(--line-soft);
}

.rs-detail .lvl {
  margin: 4px 0 0;
}

.rs-elsewhere {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 8px;
  margin-top: 22px;
  color: var(--text-dim);
  font-size: 12.5px;
}

.rs-elsewhere a {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 12px;
  border: 1px solid var(--line-soft);
  border-radius: 999px;
  background: var(--bg-elevated);
  color: var(--text-muted);
  text-decoration: none;
}

.rs-elsewhere a:hover {
  border-color: var(--accent-line);
  color: var(--accent-ink);
}

.rs-elsewhere svg {
  width: 14px;
  height: 14px;
}

/* ---- the action bar ------------------------------------------------------ */

.rs-bar {
  position: fixed;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 900;
  border-top: 1px solid var(--line-soft);
  background: rgba(255, 255, 255, 0.96);
  -webkit-backdrop-filter: saturate(180%) blur(10px);
  backdrop-filter: saturate(180%) blur(10px);
  box-shadow: 0 -8px 24px rgba(15, 35, 60, 0.06);
  animation: rs-bar-in 0.28s ease-out both;
}

html[data-theme='dark'] .rs-bar {
  background: rgba(22, 27, 34, 0.94);
}

@keyframes rs-bar-in {
  from { transform: translateY(100%); }
  to { transform: none; }
}

.rs-bar-in {
  display: flex;
  align-items: center;
  gap: 16px;
  max-width: 1040px;
  margin: 0 auto;
  padding: 12px 16px;
}

.rs-bar-side {
  display: flex;
  align-items: center;
  gap: 8px;
}

.rs-bar-side.is-end {
  margin-left: auto;
}

.rs-bar-msg {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  color: var(--text-muted);
  font-size: 13px;
  text-align: center;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.rs-bar .btn {
  gap: 7px;
  white-space: nowrap;
}

.rs-bar .btn svg {
  width: 15px;
  height: 15px;
}

.rs-bar .btn-gold {
  min-height: 46px;
  padding: 0 22px;
  font-size: 15px;
}

.rs-bar .btn-gold span {
  max-width: 260px;
  overflow: hidden;
  text-overflow: ellipsis;
}

.has-result-bar #toastHost {
  bottom: 92px !important;
  z-index: 950 !important;
}

.rs-bar .btn kbd {
  margin-left: 2px;
  font-size: 10px;
  font-weight: var(--fw-semibold);
}

/* The result screen: the shortcut small print (Esc 暂停…) means nothing here, and
   stacked under a full-window stage it made every result page ~100px taller than
   the window. It goes; the stage keeps the room for the fixed bar itself. */
.has-result-bar .eng-footer {
  display: none;
}

.has-result-bar #resultStage {
  min-height: 100vh;
  min-height: 100dvh;
  padding-bottom: 88px;
}

@media (max-width: 960px) {
  .rs-tiles,
  .rs-tiles[data-count='2'] { grid-template-columns: minmax(0, 1fr); }
  .rs-bar-msg { display: none; }
}

@media (max-width: 720px) {
  .rs-hero { border-radius: 16px; }
  .rs-hero-in { padding: 22px 12px 20px; }
  .rs-title { font-size: 22px; }
  .rs-title-aside { display: inline-block; margin: 6px 0 0; }
  .rs-dials { flex-wrap: wrap; gap: 4px 10px; }
  .rs-dial.is-main { order: -1; width: 100%; max-width: 220px; margin: 0 calc(50% - 110px); }
  .rs-dial { width: 132px; }
  .rs-dial-read b { font-size: 30px; }
  .rs-dial.is-main .rs-dial-read b { font-size: 54px; }
  .rs-facts.is-wide { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .rs-bar-in { flex-wrap: wrap; gap: 8px; padding: 10px 12px; }
  .rs-bar-side { flex-wrap: wrap; }
  .rs-bar-side.is-end { width: 100%; margin-left: 0; }
  .rs-bar-side.is-end .btn-gold { flex: 1 1 auto; justify-content: center; }
  .rs-bar kbd { display: none; }
  .has-result-bar #resultStage { padding-bottom: 130px; }
}

@media (prefers-reduced-motion: reduce) {
  .rs-bar { animation: none; }
  .rs-dial-fill { transition: none; }
}

/* ===================================================== round 6: toolbar
   Left: exit, site menu, title. Right: 重来 and 设置. Nothing in the middle —
   the old grid put the tool icons in the centre column, which read as a
   floating cluster with no owner. */

.practice-workspace .eng-toolbar {
  display: flex;
  justify-content: space-between;
  gap: 16px;
  border-bottom-color: var(--line-soft);
}

.practice-workspace .eng-toolbar-left {
  flex: 1 1 auto;
  min-width: 0;
}

.practice-workspace .eng-toolbar-right {
  position: relative;
  display: flex;
  align-items: center;
  flex: 0 0 auto;
  gap: 6px;
}

.eng-toolbar-left .eng-seg {
  margin: 0 0 0 4px;
  flex: 0 0 auto;
}

.eng-tool.eng-tool-text {
  width: auto;
  height: 36px;
  gap: 6px;
  padding: 0 12px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--bg-elevated);
  color: var(--text-strong);
  font-size: 13px;
  font-weight: var(--fw-semibold);
}

.eng-tool.eng-tool-text:hover,
.eng-tool.eng-tool-text[aria-expanded='true'] {
  border-color: var(--accent-line);
  background: var(--accent-tint);
  color: var(--accent-ink);
}

.eng-tool.eng-tool-text svg {
  width: 16px;
  height: 16px;
}

/* The progress bar hugs the toolbar's bottom edge. */
.practice-workspace .eng-progress {
  top: var(--bar-h);
}

/* Live numbers can be switched off in settings. */
.no-live-stats .eng-live,
.no-live-stats .ws-strip {
  display: none !important;
}

/* The hands reach below the board, as they do on edclub; the mount leaves
   room for the palms and fades them out. */
.practice-workspace #keyboardMount {
  padding-bottom: 70px;
}

/* ---- settings panel ---------------------------------------------------- */

.set-panel {
  position: absolute;
  top: calc(100% + 12px);
  right: 0;
  z-index: 1100;
  width: 360px;
  max-height: calc(100vh - var(--bar-h) - 24px);
  overflow-y: auto;
  padding: 6px 0 0;
  border: 1px solid var(--line-soft);
  border-radius: 16px;
  background: var(--bg-elevated);
  box-shadow: 0 22px 50px rgba(15, 35, 60, 0.18), 0 2px 6px rgba(15, 35, 60, 0.06);
}

/* wide enough for the groups side by side: three columns on a desktop, two, one on a phone */
.set-panel:has(.set-cols) { width: min(1080px, calc(100vw - 32px)); }
.set-cols {
  columns: 3 300px;
  column-gap: 0;
  column-rule: 1px solid var(--line-soft);
}
.set-cols > .set-group { break-inside: avoid; }
/* a short laptop window: the same panel a little tighter, still without a scrollbar */
@media (max-height: 720px) {
  .set-panel:has(.set-cols) .set-head { padding: 8px 18px 6px; }
  .set-panel:has(.set-cols) .set-group { padding: 2px 18px 4px; }
  .set-panel:has(.set-cols) .set-group h4 { margin: 6px 0 0; }
  .set-panel:has(.set-cols) .set-row { padding: 5px 0; }
}

.set-head {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 12px 18px 10px;
}

.set-head b {
  color: var(--text-strong);
  font-size: 15px;
}

.set-head span {
  color: var(--text-dim);
  font-size: 12px;
}

.set-group {
  padding: 6px 18px 8px;
  border-top: 1px solid var(--line-soft);
}

.set-group h4 {
  margin: 8px 0 2px;
  color: var(--text-dim);
  font-size: 11.5px;
  font-weight: var(--fw-bold);
  letter-spacing: 0.06em;
}

.set-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  padding: 9px 0;
}

.set-text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.set-text b {
  color: var(--text-strong);
  font-size: 13.5px;
  font-weight: var(--fw-semibold);
}

.set-text span {
  color: var(--text-dim);
  font-size: 11.5px;
  line-height: 1.45;
}

.set-switch {
  position: relative;
  flex: 0 0 auto;
  width: 40px;
  height: 24px;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: var(--line);
  cursor: pointer;
  transition: background-color 0.18s;
}

.set-switch i {
  position: absolute;
  top: 3px;
  left: 3px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
  transition: transform 0.18s;
}

.set-switch[aria-checked='true'] {
  background: var(--accent);
}

.set-switch[aria-checked='true'] i {
  transform: translateX(16px);
}

.set-switch:focus-visible,
.set-seg button:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.set-seg {
  display: inline-flex;
  padding: 3px;
  border-radius: 10px;
  background: var(--bg-soft);
}

.set-seg button {
  padding: 5px 12px;
  border: 0;
  border-radius: 7px;
  background: transparent;
  color: var(--text-muted);
  font-size: 12.5px;
  cursor: pointer;
}

.set-seg button[aria-checked='true'] {
  background: var(--bg-elevated);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
  color: var(--text-strong);
  font-weight: var(--fw-semibold);
}

/* 键盘皮肤: its own block under the switches — a grid of tiny keyboards,
   the locked ones greyed with a lock and the level that opens them */
.set-row.is-skins { flex-direction: column; align-items: stretch; gap: 8px; }
.set-swatches { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 4px; padding: 4px; }
.set-swatches button {
  position: relative;
  display: grid;
  justify-items: center;
  gap: 2px;
  padding: 5px 0 3px;
  font-size: 10.5px;
}
.set-swatches .kbt-mini { width: 38px; height: 23px; }
.set-swatches button[disabled] { cursor: not-allowed; }
.set-swatches button[disabled] .kbt-mini { opacity: 0.5; }
.set-swatches button svg {
  position: absolute;
  top: 3px;
  right: 3px;
  width: 11px;
  height: 11px;
  color: var(--text-dim);
}

.set-foot {
  position: sticky;
  bottom: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 18px;
  border-top: 1px solid var(--line-soft);
  background: var(--bg-elevated);
  color: var(--text-dim);
  font-size: 11.5px;
}

.set-foot kbd {
  display: inline-block;
  min-width: 18px;
  padding: 0 5px;
  border: 1px solid currentColor;
  border-bottom-width: 2px;
  border-radius: 5px;
  font-family: var(--font-mono);
  font-size: 10.5px;
  line-height: 16px;
  text-align: center;
}

@media (max-width: 600px) {
  .eng-tool.eng-tool-text span { display: none; }
  .eng-tool.eng-tool-text { width: 36px; padding: 0; justify-content: center; }
  .set-panel { position: fixed; top: calc(var(--bar-h) + 8px); right: 8px; left: 8px; width: auto; }
  .eng-toolbar-left .eng-seg { display: none; }
}

/* Focus mode: the passage and nothing else. */
.is-focused .practice-heading {
  display: none;
}

.practice-workspace #keyboardMount {
  -webkit-mask-image: linear-gradient(180deg, #000 calc(100% - 56px), transparent);
  mask-image: linear-gradient(180deg, #000 calc(100% - 56px), transparent);
}

/* ===================================================== round 7: light band
   The dial band now uses the site's own palette — the pale blue of the page
   bands, white card, blue structure — instead of a deep navy that no other
   page used. The dark version stays for dark mode, where it belongs. */

.rs-hero,
.rs-hero.is-best,
.rs-hero.is-fail {
  --rs-ink: var(--text-strong);
  --rs-dim: var(--text-muted);
  --rs-track: #e3edf7;
  --rs-tick: rgba(13, 118, 201, 0.22);
  border: 1px solid var(--accent-line);
  background:
    radial-gradient(90% 70% at 50% 0%, rgba(255, 255, 255, 0.95), transparent 70%),
    linear-gradient(180deg, #e3f0fc 0%, #f4f9fe 55%, #ffffff 100%);
  box-shadow: 0 18px 44px -22px rgba(13, 118, 201, 0.35);
}

.rs-hero.is-best {
  border-color: var(--gold-300);
  background:
    radial-gradient(90% 70% at 50% 0%, rgba(255, 255, 255, 0.9), transparent 70%),
    linear-gradient(180deg, #fdf3d8 0%, #fbf8f0 55%, #ffffff 100%);
}

.rs-hero.is-fail {
  border-color: var(--line);
  background: linear-gradient(180deg, #eef1f5 0%, #f7f8fa 55%, #ffffff 100%);
}

.rs-hero .rs-dial { --arc: #4ea3f0; }
.rs-hero .rs-dial.is-main { --arc: var(--accent); }
.rs-hero .rs-dial.is-best { --arc: #e0a132; }
.rs-hero .rs-dial.is-good { --arc: #57a95a; }
.rs-hero .rs-dial.is-bad { --arc: #e06a4f; }
.rs-hero .rs-dial.is-time { --arc: #9fb3c8; }

.rs-hero .rs-dial-fill {
  filter: none;
}

.rs-hero .rs-dial-target {
  stroke: var(--text-strong);
}

.rs-hero .rs-dial.is-main .rs-dial-read b {
  color: var(--accent-ink);
}

.rs-hero .rs-dial.is-best .rs-dial-read b { color: #b07a12; }

.rs-hero .rs-dial-sub { color: #b07a12; }

.rs-hero .rs-stars svg {
  color: #d9e2ec;
  filter: drop-shadow(0 2px 2px rgba(160, 124, 26, 0.18));
}

.rs-hero .rs-stars svg.is-on { color: #f1bc3f; }

.rs-hero .rs-title-aside {
  background: var(--accent-tint);
  color: var(--accent-ink);
}

.rs-hero .rs-dial-note em.is-ok { color: var(--green-ink); }
.rs-hero .rs-dial-note em.is-miss { color: var(--wrong-ink); }

.rs-hero .result-delta {
  background: rgba(13, 118, 201, 0.08);
  color: var(--text-muted);
}

.rs-hero .result-delta b { color: var(--text-strong); }
.rs-hero .result-delta.is-up { background: var(--green-soft); color: var(--green-ink); }
.rs-hero .result-delta.is-down { background: var(--red-soft, #fdecec); color: var(--wrong-ink); }

html[data-theme='dark'] .rs-hero,
html[data-theme='dark'] .rs-hero.is-best,
html[data-theme='dark'] .rs-hero.is-fail {
  --rs-ink: #ffffff;
  --rs-dim: rgba(255, 255, 255, 0.66);
  --rs-track: rgba(255, 255, 255, 0.14);
  --rs-tick: rgba(255, 255, 255, 0.28);
  border-color: rgba(255, 255, 255, 0.08);
  background: linear-gradient(165deg, #1b3b5f 0%, #15304f 55%, #10263f 100%);
}

html[data-theme='dark'] .rs-hero .rs-dial-target { stroke: #fff; }
html[data-theme='dark'] .rs-hero .rs-dial.is-main .rs-dial-read b { color: #fff; }
html[data-theme='dark'] .rs-hero .rs-stars svg { color: rgba(255, 255, 255, 0.18); }
html[data-theme='dark'] .rs-hero .result-delta { background: rgba(255, 255, 255, 0.1); color: var(--rs-dim); }
html[data-theme='dark'] .rs-hero .result-delta b { color: #fff; }

/* ===================================================== round 8: code practice
   A Chinese character over the letters that type it. The row gets taller so
   the label has its own band above the letters. */

.inview.has-ann {
  --type-pitch: 78px;
}

.typing-text .ann {
  position: absolute;
  z-index: 1;
  margin-top: 4px;
  transform: translateX(-50%);
  color: var(--text-muted);
  font-family: var(--font-sans);
  font-size: 16px;
  font-weight: var(--fw-semibold);
  line-height: 1;
  letter-spacing: 0.04em;
  white-space: nowrap;
  pointer-events: none;
}

/* ---- course v2: the lesson's steps (讲解 → 找键 → 练习 → 结算) ---- */
.lesson-steps {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 0 0 0 auto;
  padding: 0;
  list-style: none;
  order: 3;
}

.lesson-steps li {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 10px;
  border-radius: 999px;
  background: var(--bg-soft);
  color: var(--text-dim);
  font-size: 12px;
  font-weight: var(--fw-semibold);
}

.lesson-steps li + li::before {
  content: '';
  width: 10px;
  height: 1px;
  margin: 0 4px 0 -12px;
  background: var(--line);
}

.lesson-steps li i {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: currentColor;
  opacity: 0.5;
}

.lesson-steps li.is-on {
  background: var(--accent-tint);
  color: var(--accent-ink);
}

.lesson-steps li.is-on i { opacity: 1; }

.lesson-steps li.is-done {
  color: var(--green-ink);
}

.lesson-steps li.is-done i {
  opacity: 1;
  background: var(--green);
}

@media (max-width: 720px) {
  .lesson-steps { order: 5; width: 100%; margin: 4px 0 0; }
}

/* ============================================================ coach
   小一 teaching above the keyboard (assets/js/coach.js). */

.coach {
  width: 100%;
  max-width: 910px;
  margin: 0 auto;
  padding: 14px 20px 16px;
  border: 1px solid var(--accent-line);
  border-radius: 20px;
  background:
    radial-gradient(60% 120% at 0% 0%, rgba(124, 196, 255, 0.18), transparent 60%),
    var(--bg-elevated);
  box-shadow: 0 16px 40px -24px rgba(13, 118, 201, 0.45);
}

.coach.is-in { animation: coach-in 0.35s cubic-bezier(0.2, 0.9, 0.3, 1.1) both; }

@keyframes coach-in {
  from { opacity: 0; transform: translateY(10px) scale(0.98); }
  to { opacity: 1; transform: none; }
}

.coach-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  min-height: 20px;
}

.coach-dots { display: inline-flex; gap: 6px; }
.coach-dots i { width: 8px; height: 8px; border-radius: 50%; background: var(--line); transition: background-color 0.2s, width 0.2s; }
.coach-dots i.is-on { width: 22px; border-radius: 4px; background: var(--accent); }
.coach-dots i.is-done { background: var(--green); }

.coach-skip {
  border: 0;
  background: none;
  color: var(--text-dim);
  font-size: 12px;
  cursor: pointer;
}

.coach-skip:hover { color: var(--accent-ink); }

.coach-main {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: end;
  gap: 18px;
  min-height: 150px;
}

.coach-xy .xy { align-items: center; }
.coach-xy .xy-say { max-width: 440px; font-size: 15px; }

.coach-panel { min-width: 220px; }

.coach-task {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-end;
  gap: 10px;
  min-height: 48px;
}

.coach-task.is-pop { animation: coach-pop 0.3s cubic-bezier(0.3, 1.5, 0.5, 1); }
.coach-task.is-shake { animation: key-shake 0.25s ease-in-out; }

@keyframes coach-pop {
  from { opacity: 0.2; transform: translateY(6px); }
  to { opacity: 1; transform: none; }
}

.coach-count {
  color: var(--text-dim);
  font-family: var(--font-mono);
  font-size: 12px;
}

.coach-do {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  color: var(--text-strong);
  font-size: 16px;
  font-weight: var(--fw-bold);
}

.coach-finger {
  padding: 4px 10px;
  border-radius: 999px;
  background: var(--accent-tint);
  color: var(--accent-ink);
  font-size: 13px;
  font-weight: var(--fw-semibold);
}

.coach-hint { color: var(--text-dim); font-size: 12.5px; }
.coach-ok { color: var(--green-ink); font-size: 16px; font-weight: var(--fw-bold); }

.coach-key {
  display: inline-grid;
  place-items: center;
  min-width: 44px;
  height: 44px;
  padding: 0 10px;
  border: 1px solid var(--line);
  border-bottom-width: 3px;
  border-radius: 10px;
  background: var(--bg-elevated);
  color: var(--text-strong);
  font-family: var(--font-mono);
  font-size: 20px;
  font-weight: var(--fw-bold);
}

.coach-key.is-target {
  border-color: var(--accent);
  box-shadow: 0 0 0 4px var(--accent-tint);
  color: var(--accent-ink);
  animation: kb-spot-chip 1.1s ease-in-out infinite;
}

.coach-key.is-soft { min-width: 36px; height: 36px; font-size: 15px; color: var(--text-muted); }
.coach-key.is-done { border-color: var(--green); background: var(--green-soft); color: var(--green-ink); }

@keyframes kb-spot-chip {
  50% { transform: translateY(-3px); box-shadow: 0 0 0 7px rgba(13, 118, 201, 0.12); }
}

.coach-seq { display: inline-flex; gap: 6px; }

.coach-beat {
  position: relative;
  width: 36px;
  height: 36px;
}

.coach-beat i {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: var(--gold-400);
  animation: coach-beat var(--beat) ease-out infinite;
}

@keyframes coach-beat {
  0% { transform: scale(0.55); opacity: 1; }
  100% { transform: scale(1.1); opacity: 0.15; }
}

.coach-check {
  display: flex;
  flex-direction: column;
  gap: 6px;
  width: 100%;
  margin: 0;
  padding: 0;
  list-style: none;
}

.coach-check button {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 9px 12px;
  border: 1px solid var(--line-soft);
  border-radius: 12px;
  background: var(--bg-soft);
  color: var(--text-strong);
  font-size: 14px;
  text-align: left;
  cursor: pointer;
  transition: background-color 0.2s, border-color 0.2s;
}

.coach-check i {
  flex: 0 0 auto;
  width: 22px;
  height: 22px;
  border: 2px solid var(--line);
  border-radius: 7px;
  background: var(--bg-elevated);
}

.coach-check button.is-on { border-color: color-mix(in srgb, var(--green) 50%, transparent); background: var(--green-soft); }
.coach-check button.is-on i {
  border-color: var(--green);
  background: var(--green) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M5 12.5 10 17.5 19 7.5' fill='none' stroke='white' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 16px no-repeat;
  animation: kb-got 0.35s cubic-bezier(0.3, 1.6, 0.5, 1);
}

.stage-coaching .eng-screen { display: none; }

@media (max-width: 720px) {
  .coach { padding: 12px; }
  .coach-main { grid-template-columns: minmax(0, 1fr); }
  .coach-task { justify-content: flex-start; }
  .coach-xy .xy-say { max-width: 230px; font-size: 14px; }
}

@media (prefers-reduced-motion: reduce) {
  .coach.is-in, .coach-task.is-pop, .coach-key.is-target, .coach-beat i { animation: none; }
}

/* Coach layout, stacked: 小一 + bubble on top, the task centred under it.
   (Side by side squeezed the bubble whenever the task was wide.) */
.coach-main {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 10px;
  min-height: 0;
}

.coach-xy .xy { width: 100%; }
.coach-xy .xy-say { max-width: 560px; }
.coach-panel { min-width: 0; }
.coach-task { justify-content: center; min-height: 0; }
.coach-task:empty { display: none; }
.coach-check { max-width: 560px; margin: 0 auto; }
.coach-task > .coach-hint:last-child { width: 100%; text-align: center; }

/* ---- 小一陪练: perched on the top-right corner of the text card ----
   He sits on the card's edge with his legs over it, stands up and types
   along when you do. Sprite row 48 of 52 is his feet (the edge he sits on). */
.practice-workspace .eng-screen { position: relative; }
.has-perch .practice-heading { padding-right: 104px; }
.is-focused.has-perch .eng-screen { margin-top: 72px; }

.buddy {
  position: absolute;
  top: calc(var(--buddy-h, 78px) * -0.93);
  right: 22px;
  /* over the 中文输入 switch on the card's edge: what he says is never cut by it */
  z-index: 9;
  pointer-events: none;
}

.buddy .xy { pointer-events: auto; align-items: flex-end; }
/* One wide line fits in the gap between the toolbar and the card. */
.buddy .xy-say { width: max-content; max-width: min(440px, 52vw); margin-bottom: 4px; padding: 8px 13px 9px; font-size: 13px; line-height: 1.55; }
/* on a Chinese passage the bubble rides above the switch on the card's edge */
.has-zh .buddy .xy-say { margin-bottom: 20px; }

@media (max-width: 720px) {
  .buddy { display: none; }
  .has-perch .practice-heading { padding-right: 0; }
}

/* 小一 on the result band, bottom-left, his line above him in the margin
   beside the dials. */
.rs-hero { position: relative; }

.rs-buddy {
  position: absolute;
  left: 22px;
  bottom: 16px;
  z-index: 2;
}

/* He gets the band under the dials at every width. Reserving it only under
   1100px left the wide case with no room, so his bubble opened across the
   准确率 dial — 2565px² of overlap at 1440. The line now sits beside him
   instead of above him, which is what keeps it inside the band. */
.rs-hero:has(.rs-buddy) .rs-hero-in { padding-bottom: 96px; }
.rs-buddy .xy { flex-direction: row; align-items: flex-end; }
.rs-buddy .xy-say { width: 220px; font-size: 12.5px; line-height: 1.55; }
.rs-buddy .xy-say::before { left: -14px; bottom: 14px; transform: none; }
.rs-buddy .xy-say::after { left: -7px; bottom: 17px; transform: none; }

@media (max-width: 1100px) {
  .rs-hero-in { padding-bottom: 170px; }
}

@media (max-width: 720px) {
  .rs-buddy { transform: scale(0.85); transform-origin: left bottom; }
}


/* The perch has his speech bubble for the 键盘侠 moment; the banner would
   sit under the toolbar. */
.buddy .xy-banner { display: none; }

/* ======================================================= fit to the window
   practice-ui.js fit(): the stage fills the height under the toolbar and its
   content sits in the middle of it; the type and keyboard sizes come from one
   scale factor (--type-size / --type-pitch / --type-width set inline). */
.practice-workspace .eng-stage {
  justify-content: center;
  min-height: calc(100vh - 48px);
  min-height: calc(100dvh - 48px);
  /* the card may be wider than the keyboard (practice-ui.js --text-width) */
  max-width: calc(var(--text-width, var(--type-width, 910px)) + 80px);
  padding-top: calc(var(--bar-h, 64px) + 18px);
  padding-bottom: 18px;
}
.practice-workspace.is-prefit #stage { opacity: 0; }
.practice-workspace #stage { transition: opacity 0.18s; }
.practice-workspace #keyboardMount,
.practice-keyboard-label,
.coach { max-width: var(--type-width, 910px); }
.practice-workspace .eng-stage.hidden { display: none; }
@media (max-width: 700px) {
  .practice-workspace .eng-stage { min-height: 0; justify-content: flex-start; }
}
/* Laptop windows (up to 900px tall, which is most of them): the toolbar already
   names the lesson, so the title row goes and the passage + keyboard get the room. */
@media (min-width: 701px) and (max-height: 900px) {
  .practice-workspace .practice-heading { display: none; }
  .practice-workspace .eng-stage { padding-top: calc(var(--bar-h, 64px) + 10px); padding-bottom: 8px; }
  .practice-workspace .eng-hint { min-height: 30px; padding: 6px 0; }
  .practice-workspace .keyboard-mount { margin-top: 10px; }
  .practice-workspace #keyboardMount { padding-bottom: 12px; }
  .practice-workspace > .eng-footer { display: none; }
  .practice-workspace .eng-stage { min-height: 100vh; min-height: 100dvh; }
  /* 小一 sits on the card's corner: keep room for him under the toolbar. */
  .practice-workspace.has-perch .eng-screen { margin-top: 56px; }
}

/* ======================================================= 全拼 练码 lessons
   The Chinese character sits big over the pinyin that types it. The pinyin
   is also the hint, faded lesson by lesson (play.js codeMode):
     hint-full    pinyin shown
     hint-faint   pinyin faint — look only when stuck
     hint-demand  pinyin hidden; it comes back for the word you are on after
                  a two-second pause or a missed key */
.is-code .inview.has-ann { --type-pitch: calc(var(--type-size, 32px) * 3.1); }
.is-code .typing-text { font-size: calc(var(--type-size, 32px) * 0.82); }
.is-code .typing-text .ann {
  margin-top: 0;
  color: var(--text-strong);
  font-size: calc(var(--type-size, 32px) * 1.05);
  font-weight: 700;
  letter-spacing: 0.02em;
}
.is-code.hint-faint .typing-text .ch.is-pending { color: color-mix(in srgb, var(--text-dim) 40%, transparent); }
.is-code.hint-demand .typing-text .ch.is-pending { color: transparent; }
.is-code.hint-demand.show-hint .typing-text .ch.is-current,
.is-code.hint-demand.show-hint .typing-text .ch.is-current ~ .ch { color: var(--text-dim); transition: color 0.2s; }

/* 全拼 result: the spellings missed most (OOT.engine.missedSpellingsHtml). */
.rs-spell { display: flex; flex-direction: column; gap: 6px; margin: 0; padding: 0; list-style: none; }
.rs-spell li { display: flex; align-items: center; gap: 10px; }
.rs-spell b { min-width: 2.2em; color: var(--text-strong); font-size: 18px; }
.rs-spell code { flex: 1; color: var(--text); font: 600 15px var(--font-mono); letter-spacing: 0.04em; }
.rs-spell mark { padding: 0 1px; border-radius: 3px; background: var(--red-soft, #fde2e1); color: var(--red, #d63b35); }
.rs-spell span { color: var(--text-muted); font-size: 12px; }

/* ================================================= P4b: the practice heading
   One grid, two columns, one baseline. Left: where you are (课程 / 单元 ·
   第 n 关) over what this is (the title). Right: the lesson's steps as one
   connected track — the live state (准备就绪 / 正在练习) is the current step
   itself, so there is no second, differently aligned label beside it. */

.practice-heading {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: end;
  column-gap: 28px;
  row-gap: 12px;
  padding-left: 20px;
  padding-bottom: 20px;
}

.practice-heading .ph-main {
  display: flex;
  flex-direction: column;
  gap: 6px;
  min-width: 0;
}

.practice-heading .practice-eyebrow {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
  margin: 0;
  color: var(--text-muted);
  font-size: 13px;
  font-weight: var(--fw-semibold);
  letter-spacing: 0;
  line-height: 22px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.practice-eyebrow .pe-course { color: var(--accent-ink); font-weight: var(--fw-bold); }
.practice-eyebrow .pe-unit { overflow: hidden; text-overflow: ellipsis; }
.practice-eyebrow .pe-sep { flex: 0 0 auto; width: 5px; height: 5px; border-top: 1.5px solid var(--text-dim); border-right: 1.5px solid var(--text-dim); transform: rotate(45deg); opacity: 0.7; }
.practice-eyebrow .pe-num,
.practice-eyebrow .pe-round,
.practice-eyebrow .pe-draft {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  height: 22px;
  padding: 0 9px;
  border-radius: 999px;
  background: var(--accent-tint);
  color: var(--accent-ink);
  font-size: 12px;
  font-weight: var(--fw-bold);
  font-variant-numeric: tabular-nums;
}
.practice-eyebrow .pe-draft { background: var(--gold-100, #fff4d6); color: var(--gold-600, #9a6b00); }

.practice-heading .practice-heading-row { display: block; margin: 0; }

.practice-heading h1 {
  margin: 0;
  overflow: hidden;
  color: var(--text-strong);
  font-size: clamp(24px, 2.3vw, 30px);
  font-weight: 800;
  letter-spacing: -0.02em;
  line-height: 1.15;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.practice-heading .ph-side {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  height: 36px;
}

.practice-heading .practice-description { display: none; }

/* The state, when a page has no steps (测速): one quiet pill, same height
   as the step track so either sits on the same line. */
.practice-heading .practice-state {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  height: 36px;
  margin: 0;
  padding: 0 14px;
  border-radius: 999px;
  background: var(--bg-elevated);
  box-shadow: 0 0 0 1px var(--line-soft);
  color: var(--text-muted);
  font-size: 13px;
  font-weight: var(--fw-semibold);
  line-height: 1;
}
.practice-heading .practice-state::before { width: 7px; height: 7px; }
.ph-side:has(.lesson-steps:not([hidden])) .practice-state { display: none; }

.practice-heading .lesson-steps {
  counter-reset: step;
  display: flex;
  align-items: center;
  gap: 0;
  height: 36px;
  margin: 0;
  padding: 0 4px;
  border-radius: 999px;
  background: var(--bg-elevated);
  box-shadow: 0 0 0 1px var(--line-soft), 0 1px 2px rgba(0, 0, 0, 0.04);
  list-style: none;
}

.practice-heading .lesson-steps li {
  counter-increment: step;
  position: relative;
  display: flex;
  align-items: center;
  gap: 7px;
  height: 28px;
  margin: 0;
  padding: 0 12px 0 5px;
  border-radius: 999px;
  background: none;
  color: var(--text-dim);
  font-size: 13px;
  font-weight: var(--fw-semibold);
  line-height: 1;
  white-space: nowrap;
  transition: background 0.2s, color 0.2s;
}

.practice-heading .lesson-steps li + li { margin-left: 16px; }

.practice-heading .lesson-steps li + li::before {
  content: '';
  position: absolute;
  left: -13px;
  top: 50%;
  width: 10px;
  height: 2px;
  margin: -1px 0 0;
  border-radius: 2px;
  background: var(--line);
}

.practice-heading .lesson-steps li.is-done + li::before { background: var(--green); }

.practice-heading .lesson-steps li i {
  display: grid;
  place-items: center;
  flex: 0 0 auto;
  width: 19px;
  height: 19px;
  border-radius: 50%;
  background: var(--bg-soft);
  box-shadow: inset 0 0 0 1px var(--line-soft);
  color: var(--text-dim);
  font-size: 11px;
  font-style: normal;
  font-weight: var(--fw-bold);
  opacity: 1;
}

.practice-heading .lesson-steps li i::after { content: counter(step); }

.practice-heading .lesson-steps li.is-on {
  background: var(--accent-tint);
  color: var(--accent-ink);
}

.practice-heading .lesson-steps li.is-on i { background: var(--accent); box-shadow: none; color: #fff; }

.practice-heading .lesson-steps li.is-done { color: var(--green-ink); }
.practice-heading .lesson-steps li.is-done i { background: var(--green); box-shadow: none; color: #fff; }
.practice-heading .lesson-steps li.is-done i::after { content: '✓'; }

/* Typing now: the current step breathes. */
.practice-heading:has(.practice-state.is-active) .lesson-steps li.is-on i { animation: step-live 1.8s ease-out infinite; }

@keyframes step-live {
  0% { box-shadow: 0 0 0 0 rgba(13, 118, 201, 0.4); }
  70%, 100% { box-shadow: 0 0 0 7px rgba(13, 118, 201, 0); }
}

@media (max-width: 720px) {
  .practice-heading { grid-template-columns: minmax(0, 1fr); padding-left: 4px; }
  .practice-heading .ph-side { justify-content: flex-start; }
  .practice-heading h1 { font-size: 22px; }
}

@media (prefers-reduced-motion: reduce) {
  .practice-heading:has(.practice-state.is-active) .lesson-steps li.is-on i { animation: none; }
}

/* ---- the screen head never pushes past the card */
.practice-screen-head { min-width: 0; }
.practice-screen-head .eng-live { flex-shrink: 0; }
.ws-bars { width: auto; flex: 0 1 124px; min-width: 40px; }
.practice-screen-head:has(.ws-strip:not(.is-empty)) #screenHint { display: none; }

/* ---- hold: one key held, the rest typed with the other hand (coach.js) */
.coach-hold { position: relative; display: inline-flex; flex-direction: column; align-items: center; gap: 4px; }
.coach-hold-key { min-width: 92px; }
.coach-hold-tag {
  padding: 2px 8px 3px;
  border-radius: 6px;
  background: var(--bg-soft);
  color: var(--text-dim);
  font-size: 11px;
  font-style: normal;
  font-weight: var(--fw-bold);
  letter-spacing: 0.1em;
  transition: background 0.15s, color 0.15s;
}
.coach-hold.is-on .coach-hold-key {
  animation: none;
  transform: translateY(2px);
  border-bottom-width: 1px;
  background: var(--accent-tint);
  box-shadow: 0 0 0 4px rgba(13, 118, 201, 0.16);
}
.coach-hold.is-on .coach-hold-tag { background: var(--accent); color: #fff; }
.coach-hold.is-on .coach-hold-tag::after { content: '中'; }
.coach-hold.is-done .coach-hold-key { animation: none; border-color: var(--green); background: var(--green-soft); color: var(--green-ink); box-shadow: none; }
.coach-hold.is-done .coach-hold-tag { background: var(--green-soft); color: var(--green-ink); }
.coach-plus { color: var(--text-dim); font-size: 20px; font-weight: var(--fw-bold); align-self: flex-start; margin-top: 10px; }

/* The coach card keeps one size from scene to scene, so the heading and the
   keyboard around it do not jump every time 小一 says something new. */
.key-intro { width: 100%; }
.key-intro .coach { min-height: 244px; display: flex; flex-direction: column; }
.key-intro .coach-main { flex: 1; justify-content: center; }

/* ---- 词速着色, redesigned: a short line drawn under a finished word that
   stood out — green when it came out in one go, amber when it stalled.
   Ordinary words get nothing, the text itself never changes colour, and
   nothing appears ahead of the caret, so the eye stays on the next letter. */
.typing-text .word.ws-mark { position: relative; }
.typing-text .word.ws-mark::after {
  content: '';
  position: absolute;
  /* under the letters only (the trailing space is inside the word), and
     below the descenders, just above the ruled line */
  left: 3px;
  right: calc(var(--type-cell, 0.6em) + 3px);
  bottom: 4px;
  height: 3px;
  border-radius: 3px;
  background: var(--ws-mark);
  opacity: 0.75;
  transform-origin: left center;
  animation: ws-draw 0.36s cubic-bezier(0.2, 0.7, 0.3, 1) both;
  pointer-events: none;
}
.typing-text .word.ws-mark:has(+ br), .typing-text .word.ws-mark:last-child { right: 3px; }
/* fast in the site's speed blue (typed letters are already green), slow in amber */
.typing-text .word.ws-mark.is-slow { --ws-mark: #eab13b; }
@keyframes ws-draw {
  from { transform: scaleX(0); opacity: 0; }
  to { transform: scaleX(1); opacity: 0.75; }
}
.no-word-tint .typing-text .word.ws-mark::after { display: none; }
@media (prefers-reduced-motion: reduce) { .typing-text .word.ws-mark::after { animation: none; } }

/* ============================================ Chinese typography (2026-09)
   Two Chinese modes, redesigned together:

   .has-ann   pinyin / shuangpin: the character sits above the letters that
              type it, like ruby text. Letters use the site's Quicksand (not
              a monospace: a syllable reads as one shape), each word is at
              least as wide as its characters (engine.js pads it), a soft halo
              follows the word being typed, and finished words step back.
   .is-cjk    Chinese typed through an IME: a proper CJK face at a calm
              weight, generous spacing, no ruled lines, and no line that
              starts with 。 (engine.js keeps punctuation with its character).

   Both drop the full-height pink block for a wrong key: that is right for a
   single English letter but shouts across a Chinese line. */

:root {
  --font-cjk: 'PingFang SC', 'Hiragino Sans GB', 'Noto Sans SC', 'Source Han Sans SC', 'Microsoft YaHei', sans-serif;
}

.inview.has-ann .typing-text,
.inview.is-cjk .typing-text {
  background-image: none;
  isolation: isolate;
}

/* ---- pinyin over characters ---- */
.inview.has-ann .typing-text {
  font-family: var(--font-sans);
  font-weight: 600;
  letter-spacing: 0.01em;
  /* room for the card around the word you are on: the first and last word of
     a row used to lose its edge to the window's overflow */
  padding-left: 10px;
  padding-right: 10px;
}
/* The character over its letters sits in the middle of the row as a pair: the
   label cannot rise above its row, so the letters drop a little instead
   (engine.js rowTopOf takes it back out when it finds the row). */
.inview.has-ann { --ann-drop: 0.55em; }
.inview.has-ann .typing-text .ch { position: relative; top: var(--ann-drop); }
.inview.has-ann .typing-text .ch { min-width: 0; }
.inview.has-ann .typing-text .ch.is-sp { min-width: 0.42em; }
.inview.has-ann .typing-text .word { margin: 0 0.1em; }
.inview.has-ann .typing-text .ch.is-pending { color: var(--text-muted); }
.inview.has-ann .typing-text .ch.is-correct { color: color-mix(in srgb, var(--correct-ink) 70%, var(--text-dim)); }
.inview.has-ann .typing-text .ch.is-current { color: var(--accent); }

.typing-text .ann {
  font-family: var(--font-cjk);
  transition: color 0.18s, opacity 0.18s, transform 0.25s var(--ease);
}
.is-code .typing-text .ann,
.inview.has-ann .typing-text .ann {
  color: var(--text-strong);
  font-weight: 500;
  letter-spacing: 0.08em;
}
.inview.has-ann .typing-text .ann.is-done {
  color: color-mix(in srgb, var(--correct-ink) 65%, var(--text-dim));
  opacity: 0.72;
}
.inview.has-ann .typing-text .ann.is-now {
  color: var(--accent-ink);
  transform: translateX(-50%) translateY(-2px);
}
.typing-text .word-halo {
  position: absolute;
  z-index: -1;
  border-radius: 14px;
  background: linear-gradient(180deg, #f5faff, var(--accent-tint));
  box-shadow: 0 0 0 1.5px color-mix(in srgb, var(--accent) 45%, transparent),
    0 0 0 5px color-mix(in srgb, var(--accent) 9%, transparent),
    0 10px 20px -14px rgba(13, 118, 201, 0.7);
  opacity: 0;
  pointer-events: none;
  transition: left 0.18s var(--ease), width 0.18s var(--ease), top 0.18s var(--ease), height 0.18s var(--ease),
    opacity 0.2s, background-color 0.15s, box-shadow 0.15s;
}
/* a wrong key in the word: the card goes red until the letter is fixed */
.typing-text .word-halo.is-miss {
  background: #fff4f4;
  box-shadow: 0 0 0 1.5px color-mix(in srgb, var(--wrong) 55%, transparent),
    0 0 0 5px color-mix(in srgb, var(--wrong) 10%, transparent),
    0 10px 20px -14px rgba(197, 48, 48, 0.6);
}
html[data-theme='dark'] .typing-text .word-halo { background: rgba(63, 165, 240, 0.12); }
html[data-theme='dark'] .typing-text .word-halo.is-miss { background: rgba(252, 129, 129, 0.12); }
@media (prefers-reduced-motion: reduce) { .typing-text .word-halo { transition: opacity 0.2s; } }

/* wrong keys: coloured letter + a short bar under it, never a tall block */
.practice-workspace .inview.has-ann .typing-text .is-wrong,
.practice-workspace .inview.is-cjk .typing-text .is-wrong {
  background: linear-gradient(var(--wrong), var(--wrong)) center calc(50% + 0.72em) / 70% 2px no-repeat;
  border-radius: 0;
}
.practice-workspace .inview.has-ann .typing-text .ch.is-sp.is-wrong {
  background: linear-gradient(var(--wrong), var(--wrong)) center calc(50% + 0.72em) / 80% 3px no-repeat;
}

/* ---- Chinese through an IME ---- */
.inview.is-cjk .typing-text {
  font-family: var(--font-cjk);
  font-weight: 400;
  letter-spacing: 0.06em;
  line-break: strict;
}
.inview.is-cjk .typing-text .ch { min-width: 0; }
.inview.is-cjk .typing-text .ch.is-pending { color: var(--text-strong); }
.inview.is-cjk .typing-text .ch.is-correct { color: color-mix(in srgb, var(--correct-ink) 60%, var(--text-dim)); }
.inview.is-cjk .typing-text .ch.is-current { color: var(--accent-ink); }
/* rows tall enough for a character above its pinyin, with air above both */
.inview.has-ann { --type-pitch: 92px; }
.is-code .inview.has-ann { --type-pitch: calc(var(--type-size, 32px) * 3.3); }
.inview.has-ann .typing-text .ann { margin-top: 0; }
/* the word-speed mark sits under the letters too, not at the row's foot */
.inview.has-ann .typing-text .word.ws-mark::after { top: calc(50% + 0.8em + var(--ann-drop, 0px)); bottom: auto; right: calc(0.42em + 3px); }

/* 词汇 (vocabulary lessons and 词汇测速): the word being typed, its phonetics and meaning */
.gloss-bar { padding: 10px 19px 2px; min-height: 40px; }
.gloss-bar[hidden] { display: none; }

.gloss-bar { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.gloss-acts { display: inline-flex; gap: 6px; }
.gloss-btn { display: inline-flex; align-items: center; gap: 4px; height: 28px; padding: 0 10px; border: 1px solid var(--line-soft); border-radius: 8px; background: var(--bg-elevated); color: var(--text-muted); font: inherit; font-size: 12px; cursor: pointer; }
.gloss-btn svg { width: 16px; height: 16px; }
.gloss-btn:hover { color: var(--accent-ink); border-color: var(--accent); }
.gloss-toggle[aria-pressed='false'] { opacity: 0.7; }

/* ============ 模拟输入法 (ime-sim.js): the text, the pinyin being typed, the candidate box */
.typing-text:has(.ime-sim) { background-image: none; line-height: 1.3; font-family: var(--font-sans, system-ui); font-size: 16px; }
.ime-sim { padding: 10px 0 14px; }
/* the simulated input method lays out its own rows and never scrolls: the window follows it */
.inview:has(.ime-sim) { height: auto !important; }
.ime-line { display: flex; flex-wrap: wrap; gap: 10px 8px; font-size: 30px; font-weight: 600; line-height: 1.2; }
.ime-w { ruby-position: over; padding: 4px 6px 2px; border-radius: 10px; color: var(--text-dim); transition: color 0.15s, background 0.15s; }
.ime-w rt { font-size: 13px; font-weight: 500; font-family: var(--font-mono); color: var(--text-dim); letter-spacing: 0.02em; }
.ime-w.is-faint rt { opacity: 0.4; }
.ime-w.is-done { color: #2f9e5a; }
.ime-w.is-done rt { opacity: 0.5; }
.ime-w.is-now { color: var(--text-strong); background: var(--accent-tint); box-shadow: inset 0 -3px 0 var(--accent); }
.ime-w.is-now rt { color: var(--accent-ink); opacity: 1; }
.ime-comp { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin-top: 22px; min-height: 52px; }
.ime-buf { display: inline-flex; align-items: center; min-width: 90px; padding: 2px 6px; border-bottom: 2px solid var(--accent); color: var(--text-strong); font-family: var(--font-mono); font-size: 24px; }
.ime-buf i { color: var(--text-dim); font-style: normal; font-size: 15px; font-family: var(--font-sans, system-ui); }
.ime-caret { display: inline-block; width: 2px; height: 1em; margin-left: 2px; background: var(--accent); animation: ime-blink 1s steps(1) infinite; }
@keyframes ime-blink { 50% { opacity: 0; } }
.ime-cands { display: inline-flex; align-items: center; gap: 2px; padding: 6px 8px; border: 1px solid var(--line); border-radius: 12px; background: var(--bg-elevated);
  box-shadow: 0 10px 24px -12px rgba(0, 0, 0, 0.35); font-size: 24px; animation: ime-pop 0.12s ease-out; }
@keyframes ime-pop { from { transform: translateY(-4px); opacity: 0; } }
.ime-c { display: inline-flex; align-items: baseline; gap: 4px; padding: 3px 9px; border-radius: 8px; color: var(--text-strong); }
.ime-c em { color: var(--text-dim); font-size: 13px; font-style: normal; font-family: var(--font-mono); }
.ime-c:first-child { background: var(--accent-tint); }
.ime-c.is-bad { background: #ffe0e0; color: #c43b3b; animation: ime-shake 0.3s; }
@keyframes ime-shake { 25% { transform: translateX(-3px); } 75% { transform: translateX(3px); } }
.ime-pg { display: inline-flex; align-items: center; gap: 4px; margin-left: 8px; color: var(--text-dim); font-size: 12px; font-variant-numeric: tabular-nums; }
.ime-pg kbd { padding: 0 5px; border: 1px solid var(--line); border-radius: 5px; font-family: var(--font-mono); font-size: 11px; }
@media (prefers-reduced-motion: reduce) { .ime-caret, .ime-cands, .ime-c.is-bad { animation: none; } }

/* 手机、平板: while the device keyboard is up, the passage is all that shows */
.is-kb-open .practice-heading,
.is-kb-open .keyboard-mount,
.is-kb-open .practice-keyboard-label,
.is-kb-open #keyboardMount { display: none !important; }
.is-kb-open .eng-screen { margin-top: 0 !important; }
.is-touch .practice-resume b { font-size: 18px; }
@media (max-width: 560px) {
  .is-touch .engine { --type-size: 26px; --type-pitch: 50px; }
}

/* 课后小游戏 on the lesson result (play.js gameAfter) */
.rs-game { display: flex; align-items: center; gap: 12px; max-width: 520px; margin: 14px auto 0; padding: 10px 14px; border-radius: 14px; background: linear-gradient(100deg, #eef6ff, #f6efff); color: var(--text-strong); text-align: left; box-shadow: inset 0 0 0 1px rgba(80, 110, 200, 0.18); transition: transform .15s; }
.rs-game:hover { transform: translateY(-1px); }
.rs-game > svg { width: 22px; height: 22px; flex: 0 0 auto; color: #5b4fd8; }
.rs-game > svg:last-child { width: 16px; height: 16px; margin-left: auto; color: var(--text-dim); }
.rs-game span { display: grid; gap: 1px; }
/* 休息一下 (play.js restBreak): the same link, warmer */
.rs-game.is-rest { background: linear-gradient(100deg, #f0fbef, #fff8e6); box-shadow: inset 0 0 0 1px rgba(56, 161, 105, 0.22); }
.rs-game.is-rest > svg:first-child { color: #2f855a; }
/* 毕业 (play.js graduationHtml): the course is done, and what every day looks like from here */
.rs-grad { max-width: 560px; margin: 16px auto 0; padding: 18px 20px; border-radius: 18px; background: linear-gradient(120deg, #fff8e1, #eef6ff); box-shadow: inset 0 0 0 1px rgba(214, 158, 46, 0.28); text-align: left; }
.rs-grad-top { display: flex; align-items: center; gap: 14px; }
.rs-grad-cap { flex: none; font-size: 34px; line-height: 1; }
.rs-grad-top b { display: block; color: var(--text-strong); font-size: 18px; }
.rs-grad-top div > span { color: var(--text-muted); font-size: 13.5px; }
.rs-grad-lead { margin: 14px 0 8px; color: var(--text-strong); font-size: 14px; font-weight: 600; }
.rs-grad-steps { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; counter-reset: grad; }
.rs-grad-steps a { display: flex; align-items: center; gap: 12px; padding: 10px 12px; border-radius: 12px; background: var(--bg-elevated); color: var(--text-strong); text-decoration: none; box-shadow: 0 1px 2px rgba(15, 23, 42, 0.06); transition: transform 0.15s; }
.rs-grad-steps a:hover { transform: translateY(-1px); }
.rs-grad-steps a::before { counter-increment: grad; content: counter(grad); flex: none; width: 22px; height: 22px; border-radius: 50%; background: #d69e2e; color: #fff; font-size: 12px; font-weight: 700; line-height: 22px; text-align: center; }
.rs-grad-steps svg { flex: none; width: 20px; height: 20px; color: var(--accent); }
.rs-grad-steps span { display: grid; gap: 1px; }
.rs-grad-steps b { font-size: 14px; }
.rs-grad-steps small { color: var(--text-dim); font-size: 12.5px; }
[data-theme='dark'] .rs-grad { background: linear-gradient(120deg, #2a2412, #14233a); }
[data-theme='dark'] .rs-game.is-rest { background: linear-gradient(100deg, #12291d, #2a2412); }
.rs-game b { font-size: 14px; }
.rs-game small { color: var(--text-muted); font-size: 12.5px; }
[data-theme="dark"] .rs-game { background: rgba(120, 140, 255, 0.1); }

/* 背 (recite, tools/packs/culture.cjs): the text not yet typed is hidden — a short
   line under each character keeps the count, the punctuation keeps the shape of
   the lines, and the characters appear as they are typed. */
.is-recite .typing-text .ch.is-pending:not(.is-punct):not(.is-sp):not(.is-newline):not(.is-cue) {
  color: transparent;
  text-decoration: underline dotted color-mix(in srgb, var(--text-dim) 55%, transparent);
  text-underline-offset: 0.28em;
}
.is-recite .typing-text .ch.is-pending.is-current:not(.is-punct) {
  text-decoration-color: var(--accent);
}
/* a wrong one while reciting: the target stays hidden, what you typed sits in its place */
.is-recite .typing-text .ch.is-wrong[data-typed] { color: transparent; }
.is-recite .typing-text .ch.is-wrong[data-typed]::after { top: 50%; transform: translate(-50%, -50%); font-size: 1em; font-weight: inherit; }
.is-recite .kb-hands { display: none; }
/* the cue left showing (engine.js cueOf): readable, but quieter than the text typed */
.is-recite .typing-text .ch.is-pending.is-cue { color: color-mix(in srgb, var(--text-strong) 62%, transparent); }

/* 中文输入法: the pinyin being composed (engine.js showComp) — under the current
   character, in the gap before the next line, so the passage stays readable; the 选词 box opens just below it. */
.comp-chip {
  position: absolute;
  z-index: 6;
  padding: 1px 8px 2px;
  border: 1.5px solid var(--accent);
  border-radius: 8px;
  background: var(--bg-elevated);
  box-shadow: 0 6px 16px -8px rgba(13, 60, 120, 0.45);
  color: var(--accent-ink, var(--accent));
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: 0.46em;
  font-weight: 600;
  line-height: 1.3;
  letter-spacing: 0.02em;
  white-space: nowrap;
  text-decoration: underline;
  text-decoration-color: color-mix(in srgb, var(--accent) 55%, transparent);
  text-underline-offset: 3px;
  pointer-events: none;
}
.comp-chip[hidden] { display: none; }
.cursor.is-composing { opacity: 0.35; }

/* a wrong Chinese character: the one actually typed, small and red, under it (对照) */
.typing-text .ch[data-typed] { position: relative; }
.typing-text .ch[data-typed]::after {
  content: attr(data-typed);
  position: absolute;
  left: 50%;
  top: 92%;
  transform: translateX(-50%);
  color: var(--red, #d9483b);
  font-size: 0.6em;
  font-weight: 700;
  line-height: 1;
  pointer-events: none;
}

/* Chinese keeps its hands: the guide follows the 全拼 of the character you are on,
   letter by letter, then the space that picks it (engine.js nextKey); a spelling
   it cannot follow (简拼, 双拼, 五笔) leaves the hands resting on the home row. */

/* ============================================================ 键位地图
   assets/js/keymap.js: the keyboard as a map of what each key means, in the
   coach's `map` and `ask` scenes. Colours: 声母 purple, 韵母 blue (orange
   for 全拼's vowel letters), the three 翘舌 声母 gold; fingers as on the keyboard. */

.km-task { container-type: inline-size; }
.km {
  /* ten keys and the bottom row's stagger always fit the card */
  --km-gap: 6px;
  --km-key: min(54px, calc((100cqw - 26px - 9 * var(--km-gap)) / 10.8));
  --km-sm: #7b4fd6;
  --km-sm-bg: #f1eaff;
  --km-ym: #1f72c4;
  --km-ym-bg: #e6f2ff;
  --km-retro: #b7791f;
  --km-retro-bg: #fff3d6;
  --km-vow: #d96a1d;
  --km-vow-bg: #fff0e3;
  width: 100%;
  max-width: 680px;
  margin: 0 auto;
  padding: 12px 12px 10px;
  border-radius: 18px;
  background: var(--bg-elevated);
  border: 1px solid var(--line-soft);
  box-shadow: 0 10px 28px -20px rgba(13, 60, 120, 0.5);
}

.km-row { display: flex; gap: var(--km-gap); margin-left: calc(var(--stagger) * (var(--km-key) + var(--km-gap))); margin-bottom: var(--km-gap); }

.km-key {
  position: relative;
  flex: 0 0 var(--km-key);
  height: calc(var(--km-key) * 1.12);
  border-radius: 11px;
  background: #f4f6f9;
  border: 1.5px solid rgba(0, 0, 0, 0.06);
  box-shadow: 0 3px 0 rgba(0, 0, 0, 0.08);
  transition: transform 0.15s, opacity 0.2s, box-shadow 0.2s;
}

.km-cap { position: absolute; left: 7px; top: 4px; font: 800 18px/1 var(--font-mono, ui-monospace, monospace); color: var(--text-strong); }
.km-sm { position: absolute; right: 5px; top: 5px; padding: 1px 5px; border-radius: 6px; font: 800 11px/1.3 var(--font-mono, monospace); color: #fff; background: var(--km-retro); }
.km-ym { position: absolute; left: 0; right: 0; bottom: 5px; text-align: center; font: 700 11.5px/1.15 var(--font-mono, monospace); color: var(--km-ym); }
.km-ym.is-two { font-size: 10px; }
.km-note { position: absolute; left: 0; right: 0; bottom: 5px; text-align: center; font-size: 11px; font-weight: 700; color: var(--text-dim); }

.km-key.is-retro { background: var(--km-retro-bg); border-color: rgba(183, 121, 31, 0.35); }
.km-key.is-off { opacity: 0.35; }
.km-key.is-dim { opacity: 0.22; }
.km-xiaohe .km-key:not(.is-off):not(.is-retro), .km-abc .km-key:not(.is-off):not(.is-retro) { background: var(--km-ym-bg); }
.km-quanpin .km-key.is-sm { background: var(--km-sm-bg); }
.km-quanpin .km-key.is-sm .km-cap { color: var(--km-sm); }
.km-quanpin .km-key.is-ym { background: var(--km-vow-bg); }
.km-quanpin .km-key.is-ym .km-cap { color: var(--km-vow); }
.km-quanpin .km-key.is-v { outline: 2px dashed var(--km-vow); outline-offset: -4px; }

.km-fingers .km-key.km-f-pinky { background: #fde7f2; }
.km-fingers .km-key.km-f-ring { background: #eee7fd; }
.km-fingers .km-key.km-f-middle { background: #e3f0fd; }
.km-fingers .km-key.km-f-index { background: #e4f6ea; }
.km-bump { position: absolute; left: 50%; bottom: 9px; width: 14px; height: 3px; margin-left: -7px; border-radius: 2px; background: rgba(0, 0, 0, 0.35); }
.km-fingers .km-note { bottom: 4px; font-size: 10.5px; }
.km-fingers .km-bump { bottom: auto; top: 26px; }

.km-key.is-picked { transform: translateY(-4px) scale(1.06); box-shadow: 0 0 0 3px var(--accent), 0 8px 16px -8px rgba(13, 118, 201, 0.6); z-index: 1; opacity: 1; }
.km-key.is-answer { opacity: 1; animation: km-answer 1s ease-in-out infinite; box-shadow: 0 0 0 3px #f2b705; z-index: 1; }
.km-key.is-ok { animation: km-ok 0.6s ease-out; box-shadow: 0 0 0 3px var(--green); }
.km-key.is-bad { animation: key-shake 0.3s ease-in-out; box-shadow: 0 0 0 3px #e2554a; }

@keyframes km-answer { 50% { transform: translateY(-4px) scale(1.08); } }
@keyframes km-ok { 0% { transform: scale(1); } 40% { transform: translateY(-6px) scale(1.12); } 100% { transform: scale(1); } }

.km-legend { display: flex; flex-wrap: wrap; justify-content: center; gap: 6px 16px; margin: 6px 0 4px; font-size: 12.5px; color: var(--text-dim); }
.km-legend span { display: inline-flex; align-items: center; gap: 6px; }
.km-legend i { width: 14px; height: 14px; border-radius: 4px; display: inline-block; }
.km-legend i.km-sw-none { display: none; }
.km-sw-sm { background: var(--km-sm-bg); border: 1.5px solid var(--km-sm); }
.km-sw-ym { background: var(--km-ym-bg); border: 1.5px solid var(--km-ym); }
.km-quanpin .km-sw-ym { background: var(--km-vow-bg); border-color: var(--km-vow); }
.km-sw-v { background: var(--km-vow-bg); border: 1.5px dashed var(--km-vow); }
.km-sw-retro { background: var(--km-retro); }
.km-legend .km-f-pinky { background: #e2609f; } .km-legend .km-f-ring { background: #8a64e0; } .km-legend .km-f-middle { background: #1f86d8; } .km-legend .km-f-index { background: #3fae65; }

.km-card {
  min-height: 44px;
  margin-top: 6px;
  padding: 10px 14px;
  border-radius: 12px;
  background: rgba(13, 118, 201, 0.06);
  font-size: 15px;
  line-height: 1.6;
  text-align: center;
  color: var(--text);
}
.km-card.is-pop { animation: coach-pop 0.25s ease-out; }
.km-card kbd { padding: 1px 7px; border-radius: 6px; background: var(--bg-elevated); border: 1px solid var(--line); box-shadow: 0 2px 0 var(--line); font: 700 14px var(--font-mono, monospace); }
.km-card .km-k { font-size: 16px; padding: 2px 9px; }
.km-card .km-ex { margin-left: 4px; white-space: nowrap; }
.km-card .km-ex b { font-size: 18px; color: var(--text-strong); }
.km-sm-t { color: var(--km-sm); }
.km-xiaohe .km-sm-t, .km-abc .km-sm-t { color: var(--km-retro); }
.km-ym-t { color: var(--km-ym); }
.km-quanpin .km-ym-t { color: var(--km-vow); }
.km-card .km-f-pinky { color: #c43c83; } .km-card .km-f-ring { color: #6d48c9; } .km-card .km-f-middle { color: #1871bb; } .km-card .km-f-index { color: #2c8d4f; }

.km-progress { display: block; margin-top: 4px; font-size: 12.5px; color: var(--text-dim); }
.km-q { display: block; margin-bottom: 8px; font-size: 17px; font-weight: 800; color: var(--text-strong); text-align: center; }
.km-q b { padding: 1px 8px; border-radius: 7px; background: var(--km-ym-bg); color: var(--km-ym); font-family: var(--font-mono, monospace); }
.km-q b.is-sm { background: var(--km-retro-bg); color: var(--km-retro); }
.km-task { width: 100%; }

/* the map is the keyboard while it is up: the on-screen one under it would only repeat it */
body.is-keymap .keyboard-mount { display: none; }

@media (max-width: 720px) {
  .km { --km-gap: 3px; padding: 8px; }
  .km-cap { font-size: 13px; left: 4px; top: 3px; }
  .km-sm { font-size: 9px; padding: 0 3px; }
  .km-ym, .km-ym.is-two, .km-note { font-size: 8.5px; bottom: 3px; }
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .km { --km-sm-bg: #2c2245; --km-ym-bg: #142b45; --km-retro-bg: #3a2c12; --km-vow-bg: #3a2414; --km-ym: #7dbaf5; --km-sm: #b79cf2; }
  :root:not([data-theme="light"]) .km-key { background: #23262b; border-color: rgba(255, 255, 255, 0.06); }
  :root:not([data-theme="light"]) .km-fingers .km-key.km-f-pinky { background: #3a2230; }
  :root:not([data-theme="light"]) .km-fingers .km-key.km-f-ring { background: #2c2440; }
  :root:not([data-theme="light"]) .km-fingers .km-key.km-f-middle { background: #1b2c40; }
  :root:not([data-theme="light"]) .km-fingers .km-key.km-f-index { background: #1d3326; }
}
:root[data-theme="dark"] .km { --km-sm-bg: #2c2245; --km-ym-bg: #142b45; --km-retro-bg: #3a2c12; --km-vow-bg: #3a2414; --km-ym: #7dbaf5; --km-sm: #b79cf2; }
:root[data-theme="dark"] .km-key { background: #23262b; border-color: rgba(255, 255, 255, 0.06); }

/* ============================================================ milestones
   assets/js/milestones.js: a stop on the road to 60 WPM, and the rest break. */
.ms-overlay {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: grid;
  place-items: center;
  padding: 16px;
  background: rgba(10, 18, 36, 0.55);
  opacity: 0;
  transition: opacity 0.3s;
  overflow: hidden;
}
.ms-overlay.is-in { opacity: 1; }
.ms-card {
  position: relative;
  z-index: 1;
  width: min(440px, 100%);
  padding: 28px 26px 24px;
  border-radius: 24px;
  background: var(--bg-elevated);
  text-align: center;
  box-shadow: 0 30px 80px -30px rgba(0, 0, 0, 0.6);
  transform: translateY(14px) scale(0.96);
  transition: transform 0.35s cubic-bezier(0.2, 1.4, 0.4, 1);
}
.ms-overlay.is-in .ms-card { transform: none; }
.ms-eyebrow { margin: 0; font-size: 12.5px; font-weight: 800; letter-spacing: 0.08em; color: var(--accent-ink, var(--accent)); }
.ms-num { margin: 6px 0 2px; }
.ms-num b { font-size: 72px; line-height: 1; font-weight: 900; color: var(--text-strong); font-variant-numeric: tabular-nums; }
.ms-num small { margin-left: 6px; font-size: 18px; font-weight: 800; color: var(--text-dim); }
.ms-card h2 { margin: 4px 0 6px; font-size: 24px; color: var(--text-strong); }
.ms-line { margin: 0 0 14px; font-size: 15.5px; line-height: 1.7; color: var(--text); }
.ms-next { margin: 10px 0 18px; font-size: 14px; color: var(--text-dim); }
.ms-road { display: flex; justify-content: center; gap: 6px; }
.ms-road i { position: relative; flex: 1; max-width: 56px; height: 8px; border-radius: 4px; background: var(--line); }
.ms-road i b { position: absolute; left: 0; right: 0; top: 11px; font-size: 11px; font-style: normal; color: var(--text-dim); }
.ms-road i.is-done { background: #7be08f; }
.ms-road i.is-now { background: #ffd35a; box-shadow: 0 0 0 3px rgba(255, 211, 90, 0.35); }
.ms-road i.is-now b { color: var(--text-strong); font-weight: 900; }
.ms-road + .ms-next { margin-top: 26px; }
.ms-confetti { position: absolute; inset: 0; pointer-events: none; }
.ms-confetti i {
  position: absolute;
  top: -12px;
  width: 9px;
  height: 14px;
  border-radius: 2px;
  animation-name: ms-fall;
  animation-timing-function: cubic-bezier(0.3, 0.6, 0.6, 1);
  animation-fill-mode: both;
}
@keyframes ms-fall { to { transform: translateY(110vh) rotate(var(--r)); opacity: 0.9; } }
.ms-breathe { display: grid; place-items: center; height: 96px; }
.ms-breathe i { width: 56px; height: 56px; border-radius: 50%; background: radial-gradient(circle, #9fdcff, #5ec2ff); animation: ms-breathe 5s ease-in-out infinite; }
@keyframes ms-breathe { 50% { transform: scale(1.5); opacity: 0.7; } }
.ms-count { margin: 4px 0 16px; font-size: 15px; color: var(--text-dim); }
.ms-count b { font-size: 26px; color: var(--text-strong); font-variant-numeric: tabular-nums; }
@media (prefers-reduced-motion: reduce) {
  .ms-confetti { display: none; }
  .ms-breathe i { animation: none; }
  .ms-card { transition: none; }
}

/* ============================================================ 提速营
   預读 (engine opts.ahead): words beyond the next ones are veiled.
   节奏 (play.js lesson.beat): the caret pulses on each tick. */
.typing-text .word.is-veiled { filter: blur(6px); opacity: 0.35; transition: filter 0.2s, opacity 0.2s; }
.practice-workspace.is-beat .cursor { animation: beat-pulse var(--beat, 300ms) ease-out; }
@keyframes beat-pulse { 0% { box-shadow: 0 0 0 4px rgba(255, 196, 0, 0.55); } 100% { box-shadow: 0 0 0 0 rgba(255, 196, 0, 0); } }
@media (prefers-reduced-motion: reduce) { .practice-workspace.is-beat .cursor { animation: none; } .typing-text .word.is-veiled { transition: none; } }

/* 小一's line comes out of the perch to the left, over the step bar and the
   status pill; while he talks both step aside instead of being half covered.
   Guarded by the breakpoint that hides him: :has() matches the DOM, not
   visibility, so without the guard the pill went blank on a phone, where 小一
   is not drawn at all (2026-10-11). */
.practice-heading .lesson-steps,
.practice-heading .practice-state { transition: opacity 0.2s; }
@media (min-width: 721px) {
  .practice-workspace:has(.buddy .xy-say:not([hidden])) .practice-heading .lesson-steps,
  .practice-workspace:has(.buddy .xy-say:not([hidden])) .practice-heading .practice-state { opacity: 0; }
}
/* the first row's characters keep clear of the rule under the numbers */
.is-code .inview.has-ann { margin-top: 10px; }

/* 中文输入法 (engine.js placeInline): the input method composes in a real field
   placed by the engine, so its 选词 box opens right under the pinyin. Two layouts,
   练习设置 → 中文输入方式:

   对照 (.is-zh-pair) — the way Chinese typing sites have always done it: every
   row is the passage over a blank input line. You type on the line under the
   character you are on; what you put up stays there, right ones in ink, wrong
   ones in red, and the passage itself is never covered.

   字下拼音 (.is-zh-under) — the passage alone, with the pinyin in a small field
   just under the current character; the rows get a little more air for it. */
.practice-input.is-ime-inline { white-space: pre; }

/* ---- 对照 ----
   Every row of the passage with its own input field under it, the way a form
   pairs a label with its box: a rounded field, filled faintly; the one you are
   typing in is white with the accent outline and a soft focus ring (macOS text
   fields). What you type sits in it in the passage's own face and size, wrong
   ones red. In em throughout, so the pair keeps its shape at any size:
   passage 0.12–1.52em · field 1.62–3.12em · air to 3.4em (the candidate list floats over the next row, as it does in any editor). */
.inview.is-zh-pair .typing-text { --pair-pitch: 3.4em; line-height: var(--pair-pitch); }
/* room at the sides for the fields' rounded ends (the window clips at its edges) */
.inview.is-zh-pair { padding: 0 12px; }
.inview.is-zh-pair .typing-text .ch { box-sizing: border-box; height: var(--pair-pitch); padding-top: 0.12em; line-height: 1.4em; vertical-align: top; }
.inview.is-zh-pair .typing-text .ch.is-current { color: var(--accent-ink); }
.inview.is-zh-pair .cursor { display: none; }
.practice-workspace .inview.is-zh-pair .typing-text .is-wrong { background: none; color: var(--red, #d9483b); }
.typing-text .pair-field {
  position: absolute;
  z-index: -1;
  left: -0.36em;
  right: -0.36em;
  height: 1.5em;
  margin-top: 1.62em;
  border: 1px solid var(--line);
  border-radius: 0.32em;
  background: color-mix(in srgb, var(--text-strong) 3%, var(--bg-elevated));
  transition: background 0.15s, border-color 0.15s, box-shadow 0.15s;
  pointer-events: none;
}
.typing-text .pair-field.is-active {
  border-color: color-mix(in srgb, var(--accent) 70%, transparent);
  background: var(--bg-elevated);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 16%, transparent);
}
.inview.is-zh-pair .typing-text .ch[data-typed]::after {
  top: 1.62em;
  height: 1.5em;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-strong);
  font-size: 1em;
  font-weight: inherit;
  line-height: 1;
}
.inview.is-zh-pair .typing-text .ch.is-wrong[data-typed]::after { color: var(--red, #d9483b); }
/* the field itself: plain text in the box, a caret where the next character goes */
.practice-input.is-zh-pair,
.practice-input.is-zh-pair:focus,
.practice-input.is-zh-pair:focus-visible {
  z-index: 7;
  opacity: 1;
  padding: 0;
  border: 0;
  outline: none;
  box-shadow: none;
  background: transparent;
  color: var(--text-strong);
  caret-color: var(--accent);
}

/* ---- 字下拼音 ----
   The passage at its ordinary line spacing; while you compose, the blue line
   stays under the character you are on and the pinyin hangs from it in a small
   rounded box, underlined the way an input method marks what is being composed,
   with the candidate list just below. It may cover the next row, as an editor's does. */
/* the blue line under the character stays while you compose: the pinyin hangs from it */
.inview.is-zh-under .cursor.is-composing { opacity: 1; }
.practice-input.is-zh-under:focus,
.practice-input.is-zh-under:focus-visible { outline: none; }
.practice-input.is-zh-under.is-composing {
  z-index: 7;
  opacity: 1;
  box-sizing: border-box;
  padding: 0 0.45em;
  border: 0;
  border-radius: 0.5em;
  background: var(--bg-elevated);
  box-shadow: 0 0 0 0.5px rgba(0, 0, 0, 0.16), 0 2px 8px rgba(0, 0, 0, 0.12);
  color: var(--text-strong);
  font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "PingFang SC", system-ui, sans-serif !important;
  font-weight: 500;
  letter-spacing: 0.01em;
  text-decoration: underline;
  text-decoration-color: var(--accent);
  text-decoration-thickness: 1.5px;
  text-underline-offset: 0.22em;
  caret-color: var(--accent);
}
html[data-theme='dark'] .practice-input.is-zh-under.is-composing { box-shadow: 0 0 0 0.5px rgba(255, 255, 255, 0.2), 0 2px 10px rgba(0, 0, 0, 0.45); }

/* 中文输入方式 (practice-ui.js zhSwitch): a macOS segmented control sitting on the
   typing card's top edge, centred — always in sight, whatever the screen hides
   (the page heading goes on short screens and in 专注模式) — a grey track, the
   chosen segment a white thumb with a hairline shadow. */
.eng-screen.has-zh > .practice-screen-head { padding-top: 10px; }
.zh-switch { display: inline-flex; flex: none; align-items: center; gap: 2px; padding: 2px; border-radius: 9px; background: rgba(118, 118, 128, 0.12); white-space: nowrap; }
.eng-screen > .zh-switch { position: absolute; top: 0; left: 50%; z-index: 8; transform: translate(-50%, -50%); background: color-mix(in srgb, var(--bg-elevated) 55%, #e6e6ea); box-shadow: 0 0 0 1px var(--line); }
html[data-theme='dark'] .eng-screen > .zh-switch { background: #2c2c2e; }
.zh-switch-label { padding: 0 6px 0 8px; color: var(--text-muted); font-size: 12px; }
.zh-switch button {
  height: 24px;
  padding: 0 10px;
  border: 0;
  border-radius: 7px;
  background: transparent;
  color: var(--text-muted);
  font: inherit;
  font-size: 12.5px;
  font-weight: 500;
  cursor: pointer;
  transition: background 0.15s, color 0.15s, box-shadow 0.15s;
}
.zh-switch button:hover { color: var(--text-strong); }
.zh-switch button[aria-checked='true'] { background: var(--bg-elevated); color: var(--text-strong); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 0 0 0.5px rgba(0, 0, 0, 0.06); }
html[data-theme='dark'] .zh-switch { background: rgba(118, 118, 128, 0.24); }
html[data-theme='dark'] .zh-switch button[aria-checked='true'] { background: #636366; color: #fff; }

/* 快捷键 (combo.js): one card at a time — what to do, big, and its keys as
   keycaps the way a Mac draws them (white caps, a soft lower edge, system type);
   what comes next is a quiet list beside it. */
.inview.is-combo { height: auto !important; min-height: var(--type-viewport); overflow: visible; }
.inview.is-combo .typing-text { background-image: none; font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "PingFang SC", system-ui, sans-serif; line-height: 1.4; user-select: none; }
.cb-stage { display: grid; grid-template-columns: minmax(0, 1fr) 180px; gap: 24px; align-items: center; min-height: var(--type-viewport); padding: 8px 4px; }
.cb-card { display: flex; flex-direction: column; align-items: center; gap: 10px; padding: 22px 18px; border-radius: 18px; text-align: center; transition: background 0.2s; }
.cb-card.is-right { background: color-mix(in srgb, var(--correct-ink, #2f9e44) 9%, transparent); }
.cb-card.is-wrong { background: color-mix(in srgb, var(--red, #d9483b) 8%, transparent); animation: cb-shake 0.28s; }
@keyframes cb-shake { 25% { transform: translateX(-5px); } 75% { transform: translateX(5px); } }
.cb-count { margin: 0; color: var(--text-dim); font-size: 12.5px; font-weight: 500; letter-spacing: 0.02em; }
.cb-name { margin: 0; color: var(--text-strong); font-size: 30px; font-weight: 700; letter-spacing: -0.01em; }
.cb-note { margin: -4px 0 0; color: var(--text-muted); font-size: 14px; }
.cb-ask { margin: 2px 0 0; color: var(--text-muted); font-size: 13px; }
.cb-ask kbd { padding: 1px 6px; border-radius: 5px; background: var(--bg-soft); font-size: 12px; }
.cb-keys { display: flex; align-items: center; justify-content: center; gap: 8px; margin-top: 6px; transition: opacity 0.2s, filter 0.2s; }
.cb-keys.is-veiled { opacity: 0.12; filter: blur(6px); }
.cb-cap {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 52px; height: 52px; padding: 0 12px; box-sizing: border-box;
  border-radius: 10px; background: #fff; color: #1d1d1f;
  box-shadow: 0 0 0 0.5px rgba(0, 0, 0, 0.16), 0 2px 0 rgba(0, 0, 0, 0.12), 0 6px 14px -8px rgba(0, 0, 0, 0.25);
  font: 600 22px/1 -apple-system, BlinkMacSystemFont, "SF Pro Text", system-ui, sans-serif;
}
.cb-cap.is-wide { min-width: 64px; font-size: 18px; }
.cb-plus { color: var(--text-dim); font-size: 18px; font-style: normal; }
.cb-arrow { margin: 0 2px; color: var(--text-dim); font-size: 18px; font-style: normal; }
.cb-seq .cb-step { display: inline-flex; gap: 6px; border-radius: 12px; transition: opacity 0.15s, transform 0.15s; }
.cb-seq .cb-step.is-done { opacity: 0.35; }
.cb-seq .cb-step.is-now .cb-cap { box-shadow: 0 0 0 2px var(--accent), 0 2px 0 rgba(0, 0, 0, 0.12), 0 6px 14px -8px rgba(0, 0, 0, 0.25); }
.cb-choices { display: grid; gap: 8px; margin: 6px 0 0; padding: 0; list-style: none; }
.cb-choice { display: flex; align-items: center; gap: 14px; padding: 8px 14px 8px 10px; border-radius: 12px; background: var(--bg-soft); cursor: pointer; transition: background 0.15s, box-shadow 0.15s; }
.cb-choice:hover { box-shadow: inset 0 0 0 1px var(--line); }
.cb-choice > b { display: grid; place-items: center; width: 24px; height: 24px; border-radius: 7px; background: var(--bg-elevated); color: var(--text-muted); font-size: 13px; }
.cb-choice > span { display: flex; align-items: center; gap: 6px; }
.cb-choice .cb-cap { min-width: 40px; height: 40px; font-size: 17px; }
.cb-choice .cb-cap.is-wide { min-width: 52px; font-size: 14px; }
.cb-choice.is-right { background: color-mix(in srgb, var(--correct-ink, #2f9e44) 14%, var(--bg-elevated)); }
.cb-choice.is-wrong { background: color-mix(in srgb, var(--red, #d9483b) 12%, var(--bg-elevated)); }
.cb-next { align-self: stretch; display: flex; flex-direction: column; justify-content: center; gap: 8px; padding-left: 20px; border-left: 1px solid var(--line-soft); }
.cb-next > span { color: var(--text-dim); font-size: 12px; }
.cb-next ol { display: grid; gap: 8px; margin: 0; padding: 0; list-style: none; }
.cb-next li { overflow: hidden; color: var(--text-muted); font-size: 14px; white-space: nowrap; text-overflow: ellipsis; }
.cb-next li:first-child { color: var(--text-strong); }
html[data-theme='dark'] .cb-cap { background: #3a3a3c; color: #f5f5f7; box-shadow: 0 0 0 0.5px rgba(255, 255, 255, 0.14), 0 2px 0 rgba(0, 0, 0, 0.5); }
@media (max-width: 640px) {
  .cb-stage { grid-template-columns: 1fr; }
  .cb-next { display: none; }
  .cb-name { font-size: 24px; }
}

/* ------------------------------------------- 输入法体检 (assets/js/ime-check.js)
   一个真的会动手的提醒：让你按一下键，我们看键盘到底送出了什么，
   再决定告不告诉你「去切输入法」——以及按哪几个键切。 */
.set-act {
  display: flex; flex-direction: column; gap: 2px; width: 100%; margin-top: 8px;
  padding: 8px 10px; text-align: left; color: inherit; cursor: pointer;
  background: var(--bg-soft); border: 1px solid var(--line); border-radius: var(--radius-ctl);
}
.set-act:hover { background: var(--accent-tint); border-color: var(--accent-line); }
.set-act b { font-size: 13px; color: var(--text-strong); }
.set-act span { font-size: 12px; line-height: 1.35; color: var(--text-dim); }

/* 小一 / 小零 的声音选择 */
.set-row.set-voice { display: block; }
.set-voice .set-voice-row {
  display: flex; align-items: center; gap: 8px; margin-top: 8px;
}
.set-voice-who { flex: 0 0 34px; font-size: 13px; font-weight: 600; color: var(--text-strong); }
.set-voice select {
  flex: 1 1 auto; min-width: 0; max-width: 100%; padding: 6px 8px; font: inherit; font-size: 13px;
  color: var(--text-strong); background: var(--bg-soft);
  border: 1px solid var(--line); border-radius: var(--radius-ctl);
}
.set-voice select:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-soft); }

.ic-wrap {
  position: fixed; inset: 0; z-index: 1300; display: flex; align-items: center; justify-content: center;
  padding: 16px; background: rgba(16, 24, 40, 0.42);
}
.ic-card {
  display: flex; gap: 18px; width: min(680px, 100%); max-height: calc(100vh - 32px); overflow: auto;
  padding: 20px; background: var(--bg-elevated); border: 1px solid var(--line);
  border-radius: var(--radius-card-lg); box-shadow: var(--shadow-lg);
}
.ic-art { flex: 0 0 104px; display: flex; align-items: flex-start; justify-content: center; }
.ic-main { flex: 1 1 auto; min-width: 0; }
.ic-eyebrow { margin: 0 0 2px; font-size: 11px; letter-spacing: 0.08em; color: var(--text-dim); }
.ic-title { margin: 0 0 6px; font-size: 20px; line-height: 1.25; color: var(--text-strong); }
.ic-say { margin: 0 0 14px; font-size: 14px; line-height: 1.55; color: var(--text); }
.ic-say b { color: var(--text-strong); }
/* 这一门课对输入法的要求（全拼/双拼是模拟的、中文实战用自己的输入法）：
   安静的一行备注，压在「小一说话」下面，不抢体检本身。 */
.ic-course {
  margin: -8px 0 14px; padding: 6px 10px; font-size: 13px; line-height: 1.55;
  color: var(--text-muted); border-radius: 10px; background: var(--bg-soft);
}
.ic-field { display: block; margin-bottom: 10px; }
.ic-field-label { display: block; margin-bottom: 6px; font-size: 14px; color: var(--text-strong); }
.ic-field kbd, .ic-fix kbd {
  display: inline-block; min-width: 20px; padding: 1px 5px; font: inherit; font-size: 12px;
  text-align: center; color: var(--text-strong); background: var(--bg-sunken);
  border: 1px solid var(--line); border-bottom-width: 2px; border-radius: 4px;
}
.ic-input {
  width: 132px; padding: 9px 12px; font-size: 20px; font-weight: 600; text-align: center;
  color: var(--text-strong); background: var(--bg-soft);
  border: 2px solid var(--accent-line); border-radius: var(--radius-ctl);
}
.ic-input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.ic-state { margin: 0 0 10px; font-size: 14px; font-weight: 600; color: var(--text-muted); }
.ic-state .ic-mark { display: inline-block; min-width: 16px; margin-right: 6px; font-weight: 700; }
.ic-state.is-ok { color: var(--green-ink); }
.ic-state.is-bad { color: var(--wrong-ink); }
.ic-state.is-warn { color: #8a5a00; }
.ic-note {
  margin: 0 0 10px; padding: 8px 10px; font-size: 13px; line-height: 1.55; color: var(--text);
  background: var(--gold-soft); border: 1px solid var(--gold-200); border-radius: var(--radius-ctl);
}
.ic-fix {
  margin: 0 0 14px; padding: 10px 12px; font-size: 13px; line-height: 1.6; color: var(--text);
  background: var(--bg-soft); border: 1px solid var(--line-soft); border-radius: var(--radius-ctl);
}
.ic-fix ol { margin: 6px 0 0; padding-left: 20px; }
.ic-fix li { margin: 3px 0; }
.ic-acts { display: flex; gap: 8px; flex-wrap: wrap; }
@media (max-width: 600px) {
  .ic-card { flex-direction: column; gap: 10px; padding: 16px; }
  .ic-art { flex: 0 0 auto; }
}


/* 回合 (play.js applyRound): which round, what it asks, how the last one went */
.round-banner { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 12px; margin: 0 0 10px; padding: 8px 14px; border-radius: 12px; background: var(--accent-tint); color: var(--text-muted); font-size: 13px; }
.round-banner b { color: var(--accent-ink); font-size: 13.5px; }
.round-banner em { margin-left: auto; color: var(--text-dim); font-style: normal; font-variant-numeric: tabular-nums; }
/* B 盲打回合：键盘藏起来，凭手感打 */
.practice-workspace.is-blind #keyboardMount { visibility: hidden; }
/* 结算：每一回合一格 */
.rs-facts.rs-rounds { grid-template-columns: repeat(auto-fit, minmax(84px, 1fr)); }

/* 视频关（play.js startVideoLesson）：一张卡，视频占满宽度，下面一行跳过 */
/* 视频关：随窗口放大缩小——宽度用满内容区，但高度不超出一屏（上面留给顶栏和步骤条，
   下面留给按钮），16:9 不变形。旧写法封顶 880px，大屏上显得很小。 */
.vid-card { width: min(100%, max(320px, calc((100svh - 250px) * 16 / 9 + 30px))); margin: 0 auto; box-sizing: border-box; padding: 14px; border: 1px solid var(--line-soft); border-radius: 18px; background: var(--bg-elevated); }
.eng-stage.is-video { max-width: none; padding-top: 20px; }
.key-intro.is-video { max-width: none; margin-bottom: 0; }
.vid-player { display: block; width: 100%; aspect-ratio: 16 / 9; border-radius: 12px; background: #0f1724; }
.vid-missing { margin: 18px 6px; color: var(--text-muted); font-size: 14px; }
.vid-acts { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 12px; }
.vid-len { color: var(--text-dim); font-size: 13px; }
/* 游戏关的开场卡 */
.gl-card .ki-hint b { color: var(--accent-ink); letter-spacing: 0.06em; }
/* 连播：结算页底部一行倒计时 */
.autonext { display: flex; align-items: center; justify-content: center; gap: 12px; margin: 14px auto 0; color: var(--text-muted); font-size: 13.5px; }

/* 拼音纠音 (pyfix.js, play.js pyCorrect): only the Chinese shows; the pinyin is
   typed from memory. The letters not yet typed leave no trace at all — the
   dotted line 背 uses would count them, and an / ang would give themselves
   away — and every word keeps the same width for the same reason. */
.is-pyfix .typing-text .ch.is-pending:not(.is-sp):not(.is-newline) { color: transparent !important; text-decoration: none !important; }
.is-pyfix .typing-text .word { margin: 0 0.18em; }
/* a short blank under every word: the slot its pinyin is typed into */
.is-pyfix .inview.has-ann .typing-text .word {
  background: linear-gradient(var(--line), var(--line)) calc(50% - 0.21em) calc(50% + 0.78em + var(--ann-drop, 0px)) / calc(100% - 1.4em) 2px no-repeat;
}
.is-pyfix .inview.has-ann .typing-text .word:has(.is-current) {
  background-image: linear-gradient(var(--accent-line), var(--accent-line));
}
.is-pyfix .typing-text .ch.is-wrong { color: var(--red, #e5484d); }
.gloss-bar.py-fix { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px; padding: 10px 14px; border-radius: 12px; background: #fff4e5; color: #6b3d00; font-size: 14px; }
.py-fix-w { font-size: 20px; color: #3b2400; }
.py-fix-py { padding: 1px 8px; border-radius: 6px; background: #fff; color: #b35900; font: 700 16px var(--font-mono); letter-spacing: 0.04em; }
.py-fix-msg { flex: 1 1 260px; line-height: 1.55; }
[data-theme="dark"] .gloss-bar.py-fix { background: rgba(255, 170, 60, 0.12); color: #ffd9a8; }
[data-theme="dark"] .py-fix-w { color: #ffe9c9; }
[data-theme="dark"] .py-fix-py { background: rgba(0, 0, 0, 0.25); color: #ffc078; }
/* the 体检's result: the groups to practise as cards first, the clear ones as a row of tags */
.py-map { margin-top: 14px; padding: 16px 18px 18px; border: 1px solid var(--line-soft); border-radius: 16px; background: var(--bg-elevated); text-align: left; }
.py-map-head { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 6px 14px; }
.py-map-title { color: var(--text-strong); font-size: 17px; }
.py-map-score { color: var(--text-muted); font-size: 13px; }
.py-map-score i { font-style: normal; font-weight: 800; font-size: 16px; }
.py-map-score i.is-ok { color: #1f7a44; }
.py-map-score i.is-weak { color: #c92a2a; }
.py-map-sub { margin: 14px 0 8px; color: var(--text-dim); font-size: 12.5px; font-weight: 600; }
.py-map ul { margin: 0; padding: 0; list-style: none; }
.py-map-weak { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 168px), 1fr)); gap: 8px; }
.py-map-weak li { display: grid; grid-template-columns: 1fr auto; gap: 2px 8px; padding: 10px 12px; border: 1px solid #f5c2c2; border-radius: 12px; background: #fdf1f1; }
.py-map-weak b { color: var(--text-strong); font-size: 14.5px; }
.py-map-weak em { grid-row: 1 / span 2; grid-column: 2; align-self: center; padding: 2px 8px; border-radius: 99px; background: #fff; color: #c92a2a; font-size: 12px; font-style: normal; font-weight: 800; }
.py-map-weak small { color: var(--text-muted); font-size: 12px; }
.py-map-ok { display: flex; flex-wrap: wrap; gap: 6px; }
.py-map-ok li { padding: 4px 10px; border-radius: 99px; background: #e8f7ee; color: #1f7a44; font-size: 12.5px; font-weight: 600; }
[data-theme="dark"] .py-map-weak li { border-color: rgba(240, 80, 80, 0.35); background: rgba(240, 80, 80, 0.12); }
[data-theme="dark"] .py-map-weak em { background: rgba(0, 0, 0, 0.25); color: #ff8a8a; }
[data-theme="dark"] .py-map-ok li { background: rgba(50, 200, 120, 0.12); color: #7ddba3; }

/* 连播（play.js autoNext）：倒计时画在「下一关」按钮上，从左往右填满；左边一行小字说还剩几秒 */
.rs-bar .btn-gold { position: relative; overflow: hidden; }
.rs-bar .btn-gold.is-counting::after {
  content: ''; position: absolute; left: 0; bottom: 0; height: 4px; width: calc(var(--auto, 0) * 100%);
  background: rgba(120, 70, 0, 0.45); transition: width 0.1s linear;
}
.autonext-label { margin-right: 4px; color: var(--text-muted); font-size: 12px; white-space: nowrap; }
@media (max-width: 640px) { .autonext-label { display: none; } }

/* 拼写规则卡 (play.js showRule): the rule, then its examples as tiles —
   characters, the written pinyin, and what goes on the keyboard. */
.rule-card { display: block; max-width: 720px; padding: 24px 28px; }
.rule-title { margin-top: 2px; color: var(--text-strong); font-size: 24px; font-weight: var(--fw-bold); }
.rule-body { margin-top: 8px; color: var(--text-muted); font-size: 15px; line-height: 1.8; }
.rule-eg { display: grid; grid-template-columns: repeat(auto-fill, minmax(118px, 1fr)); gap: 10px; margin: 16px 0 0; padding: 0; list-style: none; }
.rule-eg li { display: flex; flex-direction: column; align-items: center; gap: 3px; padding: 12px 8px 10px; border: 1px solid var(--line-soft); border-radius: 14px; background: var(--bg); }
.rule-eg b { color: var(--text-strong); font-family: var(--font-cjk); font-size: 24px; font-weight: 600; letter-spacing: 0.06em; }
.rule-py { color: var(--accent-ink); font-size: 16px; font-weight: 600; }
.rule-kb { display: inline-flex; align-items: baseline; gap: 4px; margin-top: 4px; padding: 2px 8px; border: 1px solid var(--line); border-bottom-width: 2px; border-radius: 7px; background: var(--bg-elevated); color: var(--text-strong); font: 600 14px var(--font-mono); }
.rule-kb small { color: var(--text-dim); font: 500 11px var(--font-sans); }
.rule-note { margin-top: 12px; color: var(--text-dim); font-size: 13px; }
.rule-go { display: flex; align-items: center; gap: 10px; margin-top: 18px; color: var(--text-dim); font-size: 13px; }
@media (max-width: 560px) {
  .rule-card { padding: 18px; }
  .rule-eg { grid-template-columns: repeat(2, 1fr); }
}

/* 中文（字下拼音 / 中文输入）: the line under the character reads like English's — a clear bar right under
   the glyph; before the first character it breathes gently instead of fading almost away */
.inview.is-cjk .cursor { border-radius: 2px; }
.inview.is-cjk .cursor.is-blinking { animation-name: caret-pulse-soft; }
@keyframes caret-pulse-soft { 0%, 100% { opacity: 1; } 50% { opacity: 0.55; } }
/* 拼音（字在上、字母在下）: the word you are on stands out a little more — a lifted card, the
   character a touch larger — so the eye lands on it at once */
.inview.has-ann .typing-text .ann.is-now { transform: translateX(-50%) translateY(-2px) scale(1.08); }
/* 字下拼音: the ruled lines under each row, like English's exercise-book lines
   (对照 has its own fields, so it keeps none) */
.inview.is-cjk.is-zh-under .typing-text {
  background-image: repeating-linear-gradient(180deg, transparent 0, transparent calc(var(--type-pitch) - 1px),
    var(--rule, #d9d9d9) calc(var(--type-pitch) - 1px), var(--rule, #d9d9d9) var(--type-pitch));
}
