﻿/* Shared exercise-runner styles. Plain (non-scoped) because the markup spans
   several components (runner + per-type views). Loaded by the host via
   _content/LearnWordsAI.SharedUI.Exercises/css/exercises.css. Colors come from the
   theme tokens (tokens.css); only the fixed green/red verdict colors are literal. */

/* width:100% + border-box is what keeps the runner readable-width inside a FLEX host.
   `margin: 0 auto` on a flex item cancels the cross-axis stretch, so without an explicit
   width the runner collapses to fit-content — on the session screen (.session-page is a
   flex column) that meant a ~210px column of options on a 1920px display, while /lesson,
   whose wrapper is a plain block, looked right. */
.ex-runner { width: 100%; box-sizing: border-box; max-width: var(--w-readable); margin: 0 auto; padding: 8px 16px; }

/* Let the learner select & copy exercise text — e.g. an unknown word they want to look up
   via the "+" add-word panel (#610). iOS / Telegram WebView suppress text selection by
   default, so re-enable it explicitly on the text-bearing elements + answer options, the
   same way the reader keeps its prose selectable (HtmlChapterReader / ParallelText).
   Tap-to-answer is unaffected: a tap still fires the option's onclick; only a long-press
   or drag begins a selection. */
.ex-word, .ex-transcription, .ex-pos, .ex-hint-line, .ex-prompt, .ex-prompt-translation,
.ex-translation-main, .ex-note, .ex-sentence, .ex-sentence-tr, .ex-option,
.ex-example-text, .ex-example-tr, .ex-produce-answer, .ex-tiles-translation,
.ex-dictation-meaning, .ex-speak-heard, .ex-flashcard-face {
    user-select: text;
    -webkit-user-select: text;
    -webkit-touch-callout: default;
}

/* --- top bar --- */
.ex-topbar {
    display: flex;
    /* wrap lets the InfoHint card (flex-basis:100%) drop to its own line */
    flex-wrap: wrap;
    align-items: center;
    gap: 10px;
    margin-bottom: 18px;
}
.ex-badge {
    font-size: calc(12px * var(--ui-scale, 1));
    font-weight: 700;
    padding: 4px 10px;
    border-radius: 999px;
}
.ex-badge.learn { background: rgba(127,127,127,0.12); color: var(--text-secondary); }
.ex-badge.review { background: rgba(var(--accent-rgb), 0.14); color: var(--accent); }
.ex-counter {
    font-size: calc(12px * var(--ui-scale, 1));
    color: var(--hint);
    font-variant-numeric: tabular-nums;
}
/* --- shared word / prompt typography --- */
.ex-word { font-family: var(--font-display); font-size: calc(32px * var(--ui-scale, 1)); font-weight: 600; letter-spacing: -0.02em; text-align: center; color: var(--text); }
.ex-transcription { font-size: calc(16px * var(--ui-scale, 1)); color: var(--text-secondary); text-align: center; margin-top: 2px; }
.ex-pos { font-size: calc(13px * var(--ui-scale, 1)); color: var(--hint); text-align: center; font-style: italic; margin-top: 2px; }
.ex-hint-line { font-size: calc(13px * var(--ui-scale, 1)); color: var(--hint); text-align: center; margin: 14px 0 10px; }
.ex-prompt { margin-bottom: 18px; }
.ex-prompt-translation { font-size: calc(24px * var(--ui-scale, 1)); font-weight: 700; text-align: center; color: var(--text); }
.ex-translation-main { font-size: calc(22px * var(--ui-scale, 1)); font-weight: 700; text-align: center; color: var(--accent); margin: 8px 0; }
.ex-note { font-size: calc(14px * var(--ui-scale, 1)); color: var(--text-secondary); text-align: center; margin-top: 6px; }
/* #1021: glossed irregular forms strip on the flashcard back ("better — лучше · best — лучший") */
.ex-forms-strip { font-size: calc(13.5px * var(--ui-scale, 1)); color: var(--text-secondary); text-align: center; margin-top: 10px; }
.ex-forms-strip .form b { color: var(--text-primary); font-weight: 600; }

.ex-audio { display: flex; justify-content: center; margin: 12px auto 0; }

/* --- sentence / cloze --- */
.ex-sentence { font-size: calc(20px * var(--ui-scale, 1)); line-height: 1.6; text-align: center; }
.ex-sentence-tr { font-size: calc(14px * var(--ui-scale, 1)); color: var(--text-secondary); text-align: center; margin-top: 10px; }
.ex-blank { font-weight: 700; border-bottom: 2px solid var(--accent); padding: 0 14px; }
.ex-blank-filled { font-weight: 700; padding: 0 4px; }
.ex-blank-filled.ok { color: var(--success); border-bottom: 2px solid var(--success); }
.ex-blank-filled.bad { color: var(--destructive); border-bottom: 2px solid var(--destructive); }
/* Highlight of the target word inside example sentences / collocations.
   accent-soft (0.14 alpha) was too faint to see on the dark/OLED theme — the
   word looked unmarked. Use a stronger accent tint plus the accent ink color and
   a heavier weight so the marked word reads clearly on every theme. */
.ex-hl { background: rgba(var(--accent-rgb), 0.28); color: var(--accent-strong); font-weight: 600; border-radius: 3px; padding: 0 3px; }

/* --- options (MCQ) --- */
.ex-options { display: flex; flex-direction: column; gap: 10px; margin-top: 8px; }
.ex-option {
    background: var(--bg-secondary);
    border: 1px solid var(--card-border);
    border-radius: var(--radius-sm);
    padding: 14px 16px;
    font-size: calc(16px * var(--ui-scale, 1));
    cursor: pointer;
    text-align: center;
    color: var(--text);
    transition: all 0.12s ease;
}
.ex-option:hover:not(:disabled) { background: var(--card-hover); }
.ex-option.correct { background: rgba(var(--success-rgb),0.14); border-color: var(--success); }
.ex-option.wrong { background: rgba(var(--destructive-rgb),0.12); border-color: var(--destructive); }
.ex-option.dim { opacity: 0.45; }

/* --- type-in --- */
.ex-typein { display: flex; gap: 10px; margin-top: 18px; }
.ex-input {
    flex: 1 1 auto;
    background: var(--bg-secondary);
    border: 1px solid var(--card-border);
    border-radius: var(--radius-sm);
    padding: 13px 14px;
    font-size: calc(17px * var(--ui-scale, 1));
    color: var(--text);
    text-align: center;
}
.ex-input:focus { outline: none; border-color: var(--accent); }

/* --- buttons --- */
.ex-btn-primary {
    display: block;
    width: 100%;
    margin-top: 18px;
    background: var(--accent);
    color: var(--button-text);
    border: none;
    border-radius: var(--radius-sm);
    padding: 13px 18px;
    font-size: calc(16px * var(--ui-scale, 1));
    font-weight: 600;
    cursor: pointer;
}
.ex-btn-primary:disabled { opacity: 0.5; cursor: default; }
.ex-btn-ghost {
    background: none;
    border: 1px solid var(--card-border);
    border-radius: var(--radius-sm);
    padding: 13px 18px;
    font-size: calc(15px * var(--ui-scale, 1));
    cursor: pointer;
    color: var(--text);
    margin-top: 10px;
}
.ex-typein .ex-btn-primary { width: auto; margin-top: 0; }
.ex-continue { margin-top: 16px; }

/* A secondary "take as quiz" escape hatch that sits under the primary mic dock. Centered in
   its own row so it stops clinging to the left edge like a bare ghost button did (issue #851). */
.ex-alt-action { display: flex; justify-content: center; margin-top: 12px; }
.ex-alt-action > .ex-btn-ghost { margin-top: 0; }

/* Finish-session bar — replaces the old topbar ✕ (which hid behind the host's floating
   "+" button). Full-width, muted, and set apart from the exercise's own action buttons so
   it reads as "leave", not "answer". Issue #128. */
.ex-finish-bar {
    display: block;
    width: 100%;
    margin-top: 28px;
    padding: 12px 18px;
    background: var(--accent-soft);
    border: 1px solid var(--accent);
    border-radius: var(--radius-sm);
    color: var(--accent);
    font-size: calc(14px * var(--ui-scale, 1));
    font-weight: 600;
    font-family: inherit;
    cursor: pointer;
}
.ex-finish-bar:hover { background: rgba(var(--accent-rgb), 0.16); color: var(--accent-strong); border-color: var(--accent-strong); }

/* Report-exercise row (#610): the finish bar flex-fills next to a square 🚩 report button so a
   learner can flag a broken card without leaving the lesson. The scoped override zeroes the
   bar's own top margin since the row now owns that spacing (the bar keeps its 28px elsewhere). */
.ex-finish-row { display: flex; align-items: stretch; gap: 10px; margin-top: 28px; }
.ex-finish-row .ex-finish-bar { flex: 1 1 auto; margin-top: 0; }
.ex-report-btn {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0 16px;
    background: var(--accent-soft);
    border: 1px solid var(--accent);
    border-radius: var(--radius-sm);
    color: var(--accent);
    cursor: pointer;
}
.ex-report-btn:hover { background: rgba(var(--accent-rgb), 0.16); color: var(--accent-strong); border-color: var(--accent-strong); }

.ex-recall-actions, .ex-srs-actions { display: flex; gap: 12px; margin-top: 16px; }
.ex-recall-actions button, .ex-srs-actions button { flex: 1 1 0; margin-top: 0; }
.ex-btn-good {
    background: var(--success); color: #fff; border: none;
    border-radius: var(--radius-sm); padding: 13px 18px; font-size: calc(16px * var(--ui-scale, 1)); font-weight: 600; cursor: pointer;
}
.ex-btn-bad {
    background: var(--bg-secondary); color: var(--text); border: 1px solid var(--card-border);
    border-radius: var(--radius-sm); padding: 13px 18px; font-size: calc(16px * var(--ui-scale, 1)); font-weight: 600; cursor: pointer;
}

/* --- phrase self-rate grid --- */
.ex-rate-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-top: 16px; }
.ex-rate {
    border: 1px solid var(--card-border);
    border-radius: var(--radius-sm);
    padding: 13px;
    font-size: calc(15px * var(--ui-scale, 1));
    font-weight: 600;
    cursor: pointer;
    color: var(--text);
    background: var(--bg-secondary);
}
.ex-rate.again { border-color: var(--again); color: var(--again); }
.ex-rate.hard  { border-color: var(--hard); color: var(--hard); }
.ex-rate.good  { border-color: var(--good); color: var(--good); }
.ex-rate.easy  { border-color: var(--easy); color: var(--easy); }

/* --- type-the-word (produce) --- */
.ex-produce { text-align: center; }
.ex-dictation-meaning { font-size: calc(15px * var(--ui-scale, 1)); color: var(--text-secondary); text-align: center; margin: 10px 0 4px; }
.ex-produce-answer { margin-top: 18px; font-size: calc(24px * var(--ui-scale, 1)); font-weight: 800; }
.ex-produce-answer.ok { color: var(--success); }
/* The wrong branch now echoes the learner's OWN input — strike it through so it reads as
   "this is what you typed, and it's wrong"; the expected word shows below (the "Correct:" line). */
.ex-produce-answer.bad { color: var(--destructive); text-decoration: line-through; text-decoration-thickness: 2px; }

/* --- inline answer diff (type-in verdicts) ---
   A miss of one letter, struck through as a whole, reads as "all of this is wrong" and leaves the
   learner hunting for the letter. Core's AnswerDiff locates the difference; these marks show it in
   place — red on what was typed, green on what was expected.
   The --verdict-* pairs carry a solid fill WITH ink chosen to stay legible on it in every theme; a
   translucent wash off the *-rgb tokens was tried first and measured out near-grey on dark, which
   is exactly the letter the learner is supposed to spot.
   Only shown when a diff exists at all: two answers with nothing in common fall back to the plain
   struck-through rendering above (AnswerDiffResult.TooDifferent). */
.ex-diff { border-radius: 4px; padding: 0 2px; }
.ex-diff.miss { background: var(--verdict-bad-bg); color: var(--verdict-bad-ink); }
.ex-diff.want { background: var(--verdict-ok-bg); color: var(--verdict-ok-ink); }
/* A dropped word left no glyphs to mark, so the hole itself is drawn where it belonged. It holds no
   text, so it needs an explicit height (an empty inline-block collapses to nothing and the mark is
   invisible) and margins on both sides — the surrounding spaces went missing with the word. */
.ex-diff--gap {
    display: inline-block;
    width: 0.9em;
    height: 0.85em;
    margin: 0 0.22em;
    vertical-align: baseline;
}
/* With the difference marked in place the blanket strike-through is noise — worse, it crosses out
   the very letter the learner is meant to read. The whole-answer red goes too: it would drown the
   one red run that means something. */
.ex-produce-answer.bad.has-diff { color: var(--text); text-decoration: none; }
/* Same reason on the "✗ Correct: …" line, whose text is red end to end (.ex-feedback.bad): green
   on red is unreadable, and the tint is what carries the meaning here. */
.ex-answer-diff { color: var(--text); font-weight: 800; }

/* --- sentence-tiles (stage 6 production via tiles) --- */
.ex-tiles { text-align: center; }
.ex-tiles-translation { font-size: calc(17px * var(--ui-scale, 1)); color: var(--text-secondary); margin: 10px 0 16px; }
.ex-tiles-built {
    /* Holds .ex-tile chips, which size off --ui-scale — the drop zone grows with them
       so a scaled-up sentence doesn't immediately wrap to a second row. */
    min-height: calc(56px * var(--ui-scale, 1));
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    align-items: center;
    justify-content: center;
    padding: 10px;
    border: 1px dashed var(--card-border);
    border-radius: var(--radius-sm);
    background: var(--bg-secondary);
}
/* The listening slot prompts with the audio pill instead of the translation, and
   .ex-audio carries no bottom margin — without this the tray sits flush against
   the Listen button. The translation path already spaces itself (margin-bottom). */
.ex-audio + .ex-tiles-built { margin-top: 16px; }
.ex-tiles-built.ok { border-color: var(--success); }
.ex-tiles-built.bad { border-color: var(--destructive); }
.ex-tiles-placeholder { color: var(--hint); font-size: calc(14px * var(--ui-scale, 1)); }
.ex-tiles-pool {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    justify-content: center;
    margin-top: 16px;
}
.ex-tile {
    background: var(--bg-secondary);
    border: 1px solid var(--card-border);
    border-radius: var(--radius-sm);
    padding: 10px 14px;
    font-size: calc(16px * var(--ui-scale, 1));
    color: var(--text);
    cursor: pointer;
    transition: background 0.12s ease, opacity 0.12s ease;
}
.ex-tile:hover:not(:disabled) { background: var(--card-hover); }
.ex-tile.used { opacity: 0.25; cursor: default; }
.ex-tile.placed { background: rgba(var(--accent-rgb),0.10); border-color: var(--accent); }
.ex-tile.placed.tile-ok { background: rgba(var(--success-rgb),0.14); border-color: var(--success); color: var(--success); }
.ex-tile.placed.tile-bad { background: rgba(var(--destructive-rgb),0.14); border-color: var(--destructive); color: var(--destructive); }
.ex-tiles-actions { display: flex; gap: 10px; margin-top: 16px; }
.ex-tiles-actions .ex-btn-ghost, .ex-tiles-actions .ex-btn-primary { flex: 1 1 0; margin-top: 0; }
.ex-tiles-correct { margin-top: 12px; font-size: calc(15px * var(--ui-scale, 1)); color: var(--text-secondary); }

/* --- matching pairs (multi-lemma SRS refresher) --- */
.ex-matching { text-align: center; }
.ex-matching-hint { font-size: calc(13px * var(--ui-scale, 1)); color: var(--hint); margin: 4px 0 14px; }
.ex-matching-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 10px 16px;
    margin-top: 8px;
}
.ex-matching-col { display: flex; flex-direction: column; gap: 8px; }
.ex-match-tile {
    background: var(--bg-secondary);
    border: 1px solid var(--card-border);
    border-radius: var(--radius-sm);
    padding: 12px 14px;
    font-size: calc(16px * var(--ui-scale, 1));
    color: var(--text);
    cursor: pointer;
    text-align: center;
    transition: background 0.12s ease;
}
.ex-match-tile:hover:not(:disabled) { background: var(--card-hover); }
.ex-match-tile.selected { background: rgba(var(--accent-rgb),0.14); border-color: var(--accent); }
.ex-match-tile.paired { background: rgba(127,127,127,0.10); opacity: 0.75; cursor: default; }
.ex-match-tile.ok { background: rgba(var(--success-rgb),0.16); border-color: var(--success); }
.ex-match-tile.bad { background: rgba(var(--destructive-rgb),0.14); border-color: var(--destructive); }
.ex-match-tile .ex-match-pair-no {
    display: inline-block;
    font-size: calc(11px * var(--ui-scale, 1));
    font-weight: 700;
    color: var(--accent);
    margin-right: 6px;
}

/* --- speaking --- */
.ex-speak { text-align: center; }
/* Centred + side padding so the "heard: …" line never bleeds to the screen edge.
   In SpeakView it sits inside the centred .ex-speak; in PhraseRunner it renders
   directly under .ex-body (no centring parent), where it used to touch the edge. */
.ex-speak-heard { margin-top: 14px; padding: 0 16px; text-align: center; font-size: calc(15px * var(--ui-scale, 1)); color: var(--text-secondary); font-style: italic; }

/* "Something off?" — a low-key text link under a speak result; opens nothing, just sends the
   clip + context for triage (issue #439 follow-up). Deliberately quiet so it doesn't compete
   with Next/Retry. */
.ex-speak-report { display: block; margin: 10px auto 0; padding: 4px 8px; background: none; border: none; cursor: pointer; font-size: calc(13px * var(--ui-scale, 1)); color: var(--text-secondary); text-decoration: underline; opacity: 0.75; }
.ex-speak-report:hover { opacity: 1; }
.ex-speak-report-sent { margin-top: 10px; text-align: center; font-size: calc(13px * var(--ui-scale, 1)); color: var(--text-secondary); }

/* Per-word highlight in the phrase speak verdict: the target phrase is shown with mispronounced /
   mismatched words in red so a tolerated near-miss (≥70%) still flags what slipped (#851). */
.ex-speak-phrase-line { margin-top: 12px; padding: 0 16px; text-align: center; font-size: calc(15px * var(--ui-scale, 1)); color: var(--text); }
.ex-word-bad { color: var(--destructive); font-weight: 700; }

/* The mirror mark inside "heard: …": a word the learner ADDED that the target does not have.
   Struck through as well as coloured — the "heard" line is quiet secondary text where a hue
   change alone is easy to miss, and a strike reads as "this shouldn't be here" without needing
   a legend. Extras never fail the attempt (the scorer only measures target coverage), so this
   is the ONLY signal that an inserted article was noticed at all. */
.ex-word-extra {
    color: var(--destructive);
    font-weight: 700;
    text-decoration: line-through;
    text-decoration-thickness: 1.5px;
}

/* Same highlight inside the big target headline, where the whole line is already accent-coloured
   and --destructive sits one hue away from it (#e07a4e vs #e08a7f on NIGHT) — colour alone is
   invisible there. The wavy underline carries the signal on every theme, and for anyone who
   can't separate the two warm hues at all. */
.ex-translation-main .ex-word-bad {
    text-decoration: underline wavy var(--destructive);
    text-decoration-thickness: 2px;
    text-underline-offset: 5px;
}

/* --- feedback --- */
.ex-feedback { margin-top: 18px; font-size: calc(16px * var(--ui-scale, 1)); font-weight: 600; text-align: center; }
.ex-feedback.ok { color: var(--success); }
.ex-feedback.bad { color: var(--destructive); }
/* A wrong verdict reads as two sentences: "✗ Incorrect." carries the failure in red, and the
   correct answer after it is plain body text. Painting the whole line red made the answer itself
   look like the mistake — exactly the string the learner is supposed to take away. The inner
   per-letter diff marks (.ex-answer-diff) keep their own colours on top of this. */
.ex-verdict-answer { margin-inline-start: 0.35em; }
.ex-feedback.bad .ex-verdict-answer { color: var(--text); font-weight: 500; }
/* Secondary line under the matching-pairs score: lists the canonical mappings of
   the pairs the user missed (issue #179). Lighter weight so the score reads first. */
.ex-matching-correct { margin-top: 6px; font-size: calc(14px * var(--ui-scale, 1)); font-weight: 400; color: var(--text-secondary); }

/* --- flashcard --- */
.ex-card-stack { display: flex; flex-direction: column; align-items: stretch; }
/* Both min-heights scale together — the face is absolutely positioned (inset: 0), so the
   card's own box is the only thing reserving room for the 32px .ex-word inside it. Left
   fixed, a step-2 word + transcription + POS line overflowed a 160px card. */
.ex-flashcard { perspective: 1200px; cursor: pointer; min-height: calc(160px * var(--ui-scale, 1)); }
.ex-flashcard-inner {
    position: relative;
    width: 100%;
    min-height: calc(160px * var(--ui-scale, 1));
    transition: transform 0.5s;
    transform-style: preserve-3d;
}
.ex-flashcard.flipped .ex-flashcard-inner { transform: rotateY(180deg); }
.ex-flashcard-face {
    position: absolute;
    inset: 0;
    backface-visibility: hidden;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 4px;
    background: var(--bg-secondary);
    border: 1px solid var(--card-border);
    border-radius: var(--radius-lg);
    padding: 24px;
}
.ex-flashcard-back { transform: rotateY(180deg); }

/* "Tap to see translation" CTA — replaces the tiny grey "нажми, чтобы перевернуть"
   hint that didn't suggest what was on the back. The chip pulses gently so the
   action is unmistakable. */
.ex-flip-cta {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    margin-top: 16px;
    padding: 8px 14px;
    border-radius: 999px;
    background: rgba(var(--accent-rgb), 0.12);
    color: var(--accent);
    font-size: calc(13px * var(--ui-scale, 1));
    font-weight: 600;
    line-height: 1;
    border: 1px solid rgba(var(--accent-rgb), 0.3);
    animation: ex-flip-cta-bob 2.4s ease-in-out infinite;
}

.ex-flip-cta-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 18px;
    height: 18px; /* skip(c): spinning icon glyph, square by construction */
    animation: ex-flip-cta-spin 3.2s linear infinite;
}

@keyframes ex-flip-cta-bob {
    0%, 100% { transform: translateY(0); }
    50%      { transform: translateY(-2px); }
}

@keyframes ex-flip-cta-spin {
    to { transform: rotate(360deg); }
}

.ex-flashcard.flipped .ex-flip-cta { display: none; }

@media (prefers-reduced-motion: reduce) {
    .ex-flip-cta, .ex-flip-cta-icon { animation: none; }
}

.ex-examples { margin-top: 16px; }
.ex-examples-title { font-size: calc(12px * var(--ui-scale, 1)); font-weight: 700; color: var(--hint); text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 6px; }
/* Row = text on the left, the +learn button / "изучается" status docked right and vertically
   centred so the action column lines up across every example (#711). */
.ex-example { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 8px 0; border-bottom: 1px solid var(--card-border); }
.ex-example-body { min-width: 0; flex: 1 1 auto; }
.ex-example-text { font-size: calc(15px * var(--ui-scale, 1)); color: var(--text); }
.ex-example-tr { font-size: calc(13px * var(--ui-scale, 1)); color: var(--text-secondary); margin-top: 2px; }
/* "+ learn" opt-in on a reference-only example phrase (#682) — a small accent pill. */
.ex-example-add-btn {
    flex-shrink: 0;
    font-size: calc(12px * var(--ui-scale, 1));
    font-weight: 600;
    padding: 3px 10px;
    border-radius: 999px;
    background: var(--accent-soft);
    color: var(--accent-strong);
    border: none;
    cursor: pointer;
    white-space: nowrap;
}
.ex-example-add-btn:disabled { opacity: 0.5; cursor: default; }
.ex-example-added { flex-shrink: 0; font-size: calc(12px * var(--ui-scale, 1)); font-weight: 600; color: var(--text-secondary); white-space: nowrap; }

/* --- state screens --- */
.ex-state { text-align: center; padding: 48px 16px; }
.ex-empty p { color: var(--text); font-size: calc(17px * var(--ui-scale, 1)); margin: 4px 0; }
/* !important on both of these: .ex-empty p above is more specific than a single class, so the
   ramp values would lose to its 17px otherwise. */
.ex-state-title {
    font-family: var(--font-display);
    font-size: var(--type-title-l-size) !important;
    line-height: var(--type-title-l-line);
    font-weight: var(--type-title-l-weight);
    letter-spacing: var(--type-title-l-track);
    margin: 0 0 10px !important;
}
.ex-state-sub { color: var(--text-secondary) !important; font-size: calc(14px * var(--ui-scale, 1)) !important; max-width: 52ch; margin-left: auto !important; margin-right: auto !important; text-wrap: pretty; }
.ex-state-emoji { font-size: 44px; /* skip(c): state emoji */ margin-bottom: 12px; }
.ex-spinner {
    width: 32px; height: 32px; /* skip(c): loading spinner, no text inside */ margin: 0 auto;
    border: 3px solid var(--card-border);
    border-top-color: var(--accent);
    border-radius: 50%;
    animation: ex-spin 0.8s linear infinite;
}
@keyframes ex-spin { to { transform: rotate(360deg); } }

/* ============================================================
   MOBILE LESSON LAYOUT (≤640px) — "one DOM, two ergonomics"
   ------------------------------------------------------------
   Desktop (>640px) is untouched: the wrappers below are either display:contents
   (.ex-verdict, .ex-dock--inline) or plain in-flow flex rows (.ex-dock), so every
   runner/view renders exactly as before. At ≤640 the SAME markup re-flows to a
   thumb-zone layout (mirrors ReaderChrome's single 640px breakpoint):
     • action buttons pin to a fixed bottom DOCK (.ex-dock / the action rows),
     • the answer verdict rises as a colour-coded bottom SHEET (.ex-verdict),
     • finish ✕ / report 🚩 / add "+" move into the lesson TOP BAR,
     • the type-in field goes full-width — the old "field + Check" row that ran
       off the edge of narrow screens no longer exists on mobile.
   Verdict colours come from the per-theme --verdict-* tokens (tokens.css §7).
   ============================================================ */

/* --- action dock. Desktop = ordinary in-flow flex row, identical to the old
       .ex-tiles-actions / .ex-recall-actions rows the views already used. --- */
.ex-dock { display: flex; gap: 10px; margin-top: 16px; }
.ex-dock > .ex-btn-primary,
.ex-dock > .ex-btn-ghost,
.ex-dock > .ex-btn-good,
.ex-dock > .ex-btn-bad,
.ex-dock > .lw-mic { flex: 1 1 0; margin-top: 0; }

/* --- type-in dock: a zero-box wrapper on desktop so the Check button stays in
       the SAME flex row as the input (the protected "field + Check" layout). It
       becomes a real fixed dock only at ≤640 (media block below). --- */
.ex-dock--inline { display: contents; }
.ex-dock--inline > .ex-btn-primary { flex: 0 0 auto; }

/* Square 🔊 replay is a MOBILE-only duplicate of the content AudioClip (dictation) —
   desktop keeps the single audio pill in the body, so hide it above the breakpoint. */
.ex-dock-square { display: none; }

/* --- verdict wrapper: transparent on desktop (feedback + Next render in flow,
       exactly as before); a colour-coded bottom sheet at ≤640. --- */
.ex-verdict { display: contents; }

/* --- mobile-only top-bar affordances: hidden on desktop, where finishing lives in
       .ex-finish-row and add-word is the host's floating "+" launcher. --- */
.ex-top-close, .ex-top-report, .ex-top-add { display: none; }

@media (max-width: 640px) {
    /* The runner is a full-height flex column so the step content can sit VERTICALLY CENTERED
       between the top bar and the fixed dock (design §5/§6) instead of clinging to the top.
       The bottom padding still reserves room so a tall card's last line never hides under the
       fixed dock/sheet. */
    .ex-runner {
        box-sizing: border-box; /* so min-height:100dvh INCLUDES the dock padding (no overflow/scroll) */
        min-height: 100dvh;
        display: flex;
        flex-direction: column;
        padding: 8px 16px calc(var(--ex-dock-h) + 44px + var(--safe-bottom) + var(--lesson-dock-lift, 0px));
    }
    /* Managed mode (MixedRunner) nests a child runner's own .ex-runner inside the shared one.
       The inner one just fills the outer column — it must NOT claim a second viewport height
       or re-reserve the dock padding (the outer already did). */
    .ex-runner .ex-runner { min-height: 0; flex: 1 1 auto; padding: 0; }
    /* Body grows to fill the gap; its inner wrapper takes auto block-margins so it centers,
       yet a card taller than the gap still scrolls (margin:auto collapses to 0 with no spare
       space, unlike justify-content:center which would clip the top). The fixed dock/verdict
       are out of flow, so they don't affect this centering. */
    /* overflow-x:hidden is deliberate: overflow-y:auto alone makes the browser compute
       overflow-x to auto, and the fixed dock (a DOM child here) then inflates the scroll
       width into a phantom horizontal scrollbar. Clipping x is safe — the dock/verdict are
       position:fixed (containing block = viewport), so they are NOT clipped by this. */
    .ex-body { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; overflow: hidden auto; }
    .ex-body-in { margin-block: auto; width: 100%; }

    /* ---------- top bar: ✕ · chip · counter · 🚩 · + ---------- */
    /* min-height scales so the counter/chip (both --ui-scale sized) keep their vertical
       breathing room at step 2; the icon buttons below stay fixed — they are touch
       targets already at the 44px minimum, and their glyphs don't grow. */
    .ex-topbar { flex: 0 0 auto; gap: 10px; margin-bottom: 6px; min-height: calc(54px * var(--ui-scale, 1)); }
    .ex-counter { margin-left: auto; }
    .ex-top-close, .ex-top-report, .ex-top-add {
        display: inline-flex; align-items: center; justify-content: center;
        flex: 0 0 auto; border: none; background: none; cursor: pointer;
        padding: 0; -webkit-tap-highlight-color: transparent;
    }
    /* The three top-bar circles stay fixed: they hold an icon, not text, and 44px is already
       the touch-target floor — scaling them would only push the counter off a narrow screen. */
    .ex-top-close { width: 44px; height: 44px; /* skip(c): circular icon touch target */ margin-left: -12px; border-radius: 50%; color: var(--text-secondary); }
    .ex-top-report { width: 40px; height: 40px; /* skip(c): circular icon touch target */ border-radius: 50%; color: var(--text-secondary); }
    .ex-top-add {
        width: 36px; height: 36px; /* skip(c): circular icon touch target */ border-radius: 50%;
        background: var(--accent); color: var(--button-text);
        box-shadow: 0 4px 12px -4px rgba(var(--accent-rgb), 0.7);
    }
    .ex-top-close:active, .ex-top-report:active { background: var(--card-hover); }
    .ex-top-add:active { transform: scale(0.94); }

    /* The desktop finish/report bar is redundant on mobile (moved to the top bar);
       the host's floating "+" would collide with the top-bar "+" on the lesson route. */
    .ex-finish-row, .ex-finish-bar { display: none; }
    /* Anchored on body, not .tg-app: that wrapper only exists in the web host, and the dock is
       mounted OUTSIDE .app-shell in both hosts, so body is the one ancestor they share. Same
       reason as the tab-bar rule below. */
    body:has(.lesson-mixed) .fab-dock { display: none; }

    /* The lesson is a focus surface: hide the host's bottom tab bar so the fixed action dock /
       verdict sheet own the thumb zone. Without this they sit UNDER the tab bar, which stacks
       above them at --shell-z-chrome (50 > dock 20 / verdict 30) and swallows "Check"/"Next".
       The lesson keeps its own exit (top-bar ✕), same rationale as the immersive Reader. Also
       drop the content's tab-bar bottom padding so the runner fills a clean 100dvh.

       Scoped to .app-shell, NOT .tg-app: that wrapper is mounted only by WebApp.Client's
       MainLayout, so the MAUI host never matched it and shipped the exact bug this rule
       exists to prevent — the tab bar covering the dock. .app-shell is rendered by the
       shared AppShell component, so both hosts (and any future one) get it. The .fab-dock
       rule above stays on .tg-app: that dock is mounted OUTSIDE AppShell and is web-only. */
    .app-shell:has(.lesson-mixed) .app-shell__tabbar { display: none; }
    .app-shell:has(.lesson-mixed) .app-shell__content { padding-bottom: 0; }

    /* ---------- action dock: pin to the thumb zone ---------- */
    .ex-dock,
    .ex-dock--inline,
    .ex-tiles-actions,
    .ex-recall-actions,
    .ex-rate-grid {
        position: fixed;
        left: 0; right: 0;
        /* --lesson-dock-lift clears a tab bar the host still shows (a session block that is not
           focus mode). Zero on /lesson and in focus mode, where the bar is hidden outright. */
        bottom: calc(var(--kb-inset, 0px) + var(--lesson-dock-lift, 0px));
        margin: 0;
        padding: 10px 16px calc(14px + var(--safe-bottom));
        gap: 10px;
        z-index: 20;
    }
    .ex-dock, .ex-dock--inline { display: flex; }
    .ex-dock--inline > .ex-btn-primary { flex: 1 1 0; }
    /* 4 ratings across the dock. minmax(0, 1fr), NOT a bare 1fr: `1fr` floors each column at
       its content's min-content width, and these four labels ("Снова / Трудно / Хорошо /
       Легко") don't fit four-across on a 320px screen — the track then overflowed the fixed
       dock and body's overflow-x:clip silently cut "Легко" off the right edge. Measured at
       320px: 4 columns wanted 377px of content plus gaps against 288px of usable width, at
       the DEFAULT interface size; the regulator only made an existing clip more obvious.
       minmax(0,…) lets the columns actually divide the dock and the labels wrap instead. */
    .ex-rate-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
    .ex-dock-square { display: flex; }

    /* Every button that lives in a dock gets the 56px thumb target + shared shape. */
    .ex-dock > button,
    .ex-tiles-actions > button,
    .ex-recall-actions > button,
    .ex-rate-grid > .ex-rate {
        height: var(--ex-dock-h); min-height: var(--ex-dock-h);
        margin: 0; padding: 0 16px; border-radius: 14px;
        font-size: calc(17px * var(--ui-scale, 1));
        display: flex; align-items: center; justify-content: center; gap: 8px;
    }
    /* Four labels share one dock, so a rating gets roughly a quarter of the width that the
       rules above budget 16px of side padding for. At 320px that left ~31px of text box and
       clipped the longest label ("Хорошо") inside its own button. Trim the padding and let a
       label wrap to a second line rather than being cut. */
    .ex-rate-grid > .ex-rate {
        /* Four labels share one dock, so a rating gets ~a quarter of the width the rules above
           budget 16px of side padding for. At 320px that left ~31px of text box and clipped the
           longest label ("Хорошо") inside its own button — at the DEFAULT interface size, before
           the regulator was involved at all.

           The cap is what keeps the regulator from making it worse: four-across on a 320px
           screen has no room for a 30%-larger label, so the rating tier stops growing once a
           quarter of the viewport becomes the binding constraint. It is the ONE place in the
           lesson where the regulator is deliberately bounded, and it beats the alternatives —
           clipping the label, or breaking "Хорошо" across two lines mid-word. Everything wider
           (the phone in the picture, any tablet) resolves to the plain scaled size. */
        font-size: min(calc(14px * var(--ui-scale, 1)), 4.2vw);
        padding: 0 6px;
        line-height: 1.15;
    }
    .ex-dock-square { flex: 0 0 var(--ex-dock-h); width: var(--ex-dock-h); padding: 0; }

    /* ---------- type-in field goes full width (fixes the off-screen Check bug) ---------- */
    .ex-typein { display: block; margin-top: 18px; }
    .ex-input { width: 100%; height: calc(58px * var(--ui-scale, 1)); border-radius: 14px; font-size: calc(19px * var(--ui-scale, 1)); }

    /* ---------- verdict sheet: rises from the bottom, colour-coded ---------- */
    .ex-verdict {
        display: block;
        position: fixed; left: 0; right: 0;
        bottom: calc(var(--kb-inset, 0px) + var(--lesson-dock-lift, 0px));
        z-index: 30; margin: 0;
        padding: 16px 18px calc(16px + var(--safe-bottom));
        border-radius: 20px 20px 0 0;
        box-shadow: var(--shadow-3);
        animation: ex-verdict-rise 0.28s cubic-bezier(0.32, 0.72, 0.33, 1);
    }
    .ex-verdict.ok  { background: linear-gradient(rgba(var(--success-rgb), 0.15), rgba(var(--success-rgb), 0.05)), var(--bg-elevated); border-top: 1px solid rgba(var(--success-rgb), 0.45); }
    .ex-verdict.bad { background: linear-gradient(rgba(var(--destructive-rgb), 0.14), rgba(var(--destructive-rgb), 0.05)), var(--bg-elevated); border-top: 1px solid rgba(var(--destructive-rgb), 0.45); }
    /* The feedback line becomes the sheet headline. The ✓ / ✗ glyph is already baked into
       the verdict strings themselves (e.g. "✓ Верно", "✗ Правильно: …"), so we must NOT add
       a CSS ::before here — that double-printed the glyph at ≤640 ("✓ ✓ …", issue #851). */
    .ex-verdict .ex-feedback { margin-top: 0; text-align: left; font-size: calc(17px * var(--ui-scale, 1)); }
    .ex-verdict .ex-matching-correct { text-align: left; }
    /* The "Next" pill fills the sheet width and takes the verdict colour (decision #3). */
    .ex-verdict .ex-continue { width: 100%; height: var(--ex-dock-h); margin-top: 14px; border-radius: 14px; font-size: calc(17px * var(--ui-scale, 1)); }
    .ex-verdict.ok  .ex-continue { background: var(--verdict-ok-bg);  color: var(--verdict-ok-ink); }
    .ex-verdict.bad .ex-continue { background: var(--verdict-bad-bg); color: var(--verdict-bad-ink); }
    /* SRS retry/relearn (the wrong-review verdict) flows as a button row inside the sheet. */
    .ex-verdict .ex-srs-actions { margin-top: 14px; }
    .ex-verdict .ex-srs-actions > button { height: var(--ex-dock-h); margin: 0; border-radius: 14px; }

    /* ---------- bigger touch targets in the answer content ----------
       The min-heights track --ui-scale alongside the font-size on the same rule:
       these plates hold the answer text, so a fixed box just tightened around a
       larger glyph instead of growing with it. */
    .ex-option { min-height: calc(56px * var(--ui-scale, 1)); border-radius: 14px; font-size: calc(17px * var(--ui-scale, 1)); }
    .ex-tile { min-height: calc(48px * var(--ui-scale, 1)); border-radius: 12px; }
    .ex-match-tile { min-height: calc(52px * var(--ui-scale, 1)); }

    @media (prefers-reduced-motion: reduce) {
        .ex-verdict { animation: none; }
    }
}

@keyframes ex-verdict-rise {
    from { transform: translateY(105%); }
    to   { transform: translateY(0); }
}
