/* ============================================================================
   Seriela design system — the single stylesheet (every account, light + dark).

   - default palette: the light theme (:root)
   - data-theme="dark": the dark palette (soft charcoal)
   The user picks light/dark in the account menu or Settings.

   The rules (INFO_DEV.md §The design system, checked by src/design.test.ts): tokens on :root,
   plain named classes, no inline styles; outside the token blocks every colour,
   font size, line-height, radius, margin, padding and gap is a var(--…).

   Contents: tokens, base, the app shell, page shapes, cards, buttons, forms,
   chips, alerts, the sound button, toast, dialogs, empty + done, the deck, the
   Discover hub, the word card, training and lessons, utilities, public pages,
   thumbs. No CDN: the Nunito font is self-hosted (top of file).

   The layers (INFO_DEV.md §CSS; the statement below names their order): tokens ·
   base (the reset, elements only) · components (everything with a class) ·
   platform (reduced motion's last word). Every rule sits in one, wrapped where it
   stood: a rule's layer is its own `@layer` block, wherever that block is in the
   file. The components stay one layer on purpose — earlier, more specific rules
   beat later ones across every section (a shell menu over the button base below
   it, a card's last child over a landing paragraph), so a finer split rewrites
   those rules, with a computed-style A/B to prove it (U4 §4).
   ========================================================================== */

@layer tokens, base, components, platform;

@layer tokens {
/* Self-hosted Nunito (variable, weights 500–900) — no CDN. Files live in
   static/assets/fonts/ (SIL OFL 1.1, see the OFL.txt next to them); if an exotic
   subset char misses, the system UI font is used. The families come from the
   Google Fonts CSS: latin, latin-ext, cyrillic, cyrillic-ext — Cyrillic was added
   2026-10-16, before that Russian rendered in the fallback font. */
@font-face {
    font-family: 'Nunito';
    font-style: normal;
    font-weight: 500 900;
    font-display: swap;
    src: url('/assets/fonts/nunito-latin.woff2') format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
    font-family: 'Nunito';
    font-style: normal;
    font-weight: 500 900;
    font-display: swap;
    src: url('/assets/fonts/nunito-latin-ext.woff2') format('woff2');
    unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
    font-family: 'Nunito';
    font-style: normal;
    font-weight: 500 900;
    font-display: swap;
    src: url('/assets/fonts/nunito-cyrillic.woff2') format('woff2');
    unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
    font-family: 'Nunito';
    font-style: normal;
    font-weight: 500 900;
    font-display: swap;
    src: url('/assets/fonts/nunito-cyrillic-ext.woff2') format('woff2');
    unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}

/* ---------- tokens ---------- */
/* One accent hue, two calibrations: light and dark use the SAME indigo and
   only re-calibrate it for the surround. Easy on the eyes (2026-10-26): no
   pure white and no pure black — light is warm paper with ink text, dark is a
   soft charcoal with off-white text; every text pair clears 4.5:1 (the muted
   one 5:1+), the accent fills carry their label at 6:1. */
:root {
    /* Cool, a lavender-tinted white (his pick, 2026-11-03, the theme overhaul;
       warm #f4f2ee before); --bg-2 is the page's deeper top, the cards white. */
    --bg: #f3f2fa;
    --bg-2: #e8e6f6;
    --card: #ffffff;
    --text: #25272c;
    --muted: #5d616b;
    --line: #e1dfee;
    --accent: #5154c8;
    --accent-strong: #3e40ad;
    --accent-soft: #e9e9fa;
    --fire-1: #ff922b;
    --fire-2: #f03e3e;
    --good: #2f8a57;
    --good-soft: #e0f2e7;
    --bad: #c43d3d;
    --bad-soft: #fbe7e6;
    /* the same two as *text*: --good/--bad are surfaces (a fill, a border, a
       dot) — a label on them or on its soft tint needs its own step, or axe
       reads 3.7:1 (.alert) and 2.9:1 (.opt.right) */
    --good-text: #1f6a40;
    --bad-text: #a33030;
    /* the third status: "this worked, but read it" — anything that needs
       attention without the red of an error. */
    --warn-soft: #fbefe0;
    --warn-text: #8a4510;
    /* The rewards (the theme overhaul, 2026-11-04): gold for points and levels,
       sky for a score; each soft tint carries its text at 4.5:1+ (gold 4.9, sky
       5.3), a finish's tiles and the level pill. Mint is --good-soft, lavender
       --accent-soft, the streak's fire --warn-soft with --fire-2. */
    --gold-soft: #fdf1d3;
    --gold-text: #8f5f00;
    --sky-soft: #e2f0fd;
    --sky-text: #1b64a6;
    /* a progress bar's empty track: --accent-soft vanished on the page's
       lavender (Discover's round under the deck) */
    --accent-track: #dcdcf6;
    /* a chart's empty track on a pastel hub card: the card's white on light, the
       page's charcoal on dark (--line vanished on the tints) */
    --hub-track: var(--card);
    /* Shadows: a surface's (cards, panels) and the primary button's: raised on
       an edge --raise deep, a darker indigo, with its glow (his pick, 2026-11-03). */
    --shadow: 0 8px 24px rgba(37, 39, 44, .07);
    --raise: 4px;
    --accent-edge: #2c2e8c;
    --shadow-accent: 0 var(--raise) 0 var(--accent-edge), 0 8px 18px rgba(var(--accent-rgb), .22);
    /* Snappy's rim: none on light; on dark a thin light line so his navy outline
       doesn't sink into the charcoal (his call, 2026-11-03). */
    --mascot-rim: none;
    /* Motion (his call, 2026-11-03: the slides read too fast; one place to tune).
       A press is quick, an entry unhurried, an exit a little quicker than its
       entry, a reaction (a pop, a right or wrong answer) the slowest. design.test
       holds every animation and transition to these. */
    --dur-press: .12s;    /* a press, a hover, a colour, a caret */
    --dur-fade: .18s;     /* a picture fading in once it has loaded (Snappy) */
    --dur-in: .3s;        /* a block or a screen coming in: a slide-down, a menu, a sheet */
    --dur-out: .22s;      /* …and going (lib/conceal.ts waits for it) */
    --dur-swipe: .35s;    /* a deck card thrown off (SwipeDeck.svelte's 350 ms) */
    --dur-react: .45s;    /* Snappy's pop, "not quite", the green of a right answer */
    --dur-fill: .8s;      /* a ring filling */
    --dur-flourish: 1s;   /* a nudge, a burst: once, to catch the eye */
    --ease-out: cubic-bezier(.2, .7, .3, 1);     /* entries: fast start, soft landing */
    --ease-in: cubic-bezier(.5, 0, .75, 0);      /* exits: soft start, gone */
    --ease-spring: cubic-bezier(.2, 1.6, .4, 1); /* a pop, past its size and back */
    --ease-bounce: cubic-bezier(.3, 1.4, .5, 1); /* a slider or a ring overshooting a touch */
    /* Roundness: two steps and the pill (2026-10-30; there were six steps 2px
       apart, plus a few raw ones). */
    --radius-md: 14px;     /* controls, rows and insets: buttons, fields, options, word rows, alerts */
    --radius-lg: 22px;     /* cards, panels, dialogs, the deck card */
    --radius-pill: 999px;  /* chips, tags, switches, tracks, dots */
    /* Type weights - the three the app uses (Nunito is loaded 500-900). One
       step lighter than they were (700/800/900): every line of text was bold,
       which tires the eye on a long list (2026-10-26). Body text is 500, and
       since 2026-11-06 it has a name: --fw-regular (600) is the *label and
       secondary* weight, a step above what it labels (U1 §1.3). */
    --fw-body: 500;
    --fw-regular: 600;
    --fw-bold: 700;
    --fw-black: 800;
    /* The type scale: every font size is one of these seven steps (there were
       27 different sizes), or one of the two display sizes. */
    --fs-xs: .75rem;     /* 12 small caps labels, meta */
    --fs-sm: .875rem;    /* 14 secondary text, chips, small buttons */
    --fs-md: 1rem;       /* 16 body, buttons, fields */
    --fs-lg: 1.125rem;   /* 18 a word in a list, emphasis */
    --fs-xl: 1.25rem;    /* 20 card titles (h2) */
    --fs-2xl: 1.5rem;    /* 24 page-level headings, prompts */
    --fs-3xl: 2rem;      /* 32 the largest heading */
    --fs-display: clamp(2rem, 10vw, 2.9rem);  /* the deck word, the milestone number */
    --fs-icon: 4.5rem;                        /* a finish's big icon */
    /* Line-heights: icons and numbers sit on 1, headings and display on 1.2,
       reading text on 1.5. */
    --lh-none: 1;
    --lh-tight: 1.2;
    --lh-body: 1.5;
    --tracking-caps: .05em;   /* the small-caps labels */
    --tracking-tight: -.5px;  /* a display word: a touch closer */
    /* Colour roles that must hold in both themes: text on a filled surface
       that is *not* the accent (the destructive commit, the fire banner) */
    --on-danger: #fff;
    /* the accent as channels: the soft glow under a button */
    --accent-rgb: 81, 84, 200;
    /* the scrim behind the native <dialog> */
    --scrim: rgba(37, 39, 44, .45);
    /* the landing's drawn TV screen: dark in both themes, white text */
    --screen: #1d1e24;
    --on-screen: #fff;
    --nav-h: 64px;
    --bar-h: 52px;          /* the phone's app bar (the toast hangs below it) */
    --side-w: 13.5rem;      /* the desktop sidebar, open */
    --side-w-min: 4.25rem;  /* …and collapsed (icons only) */
    /* layout: the page's max width and the width of a page's side column */
    --app-max: 960px;       /* every app page's container */
    --form-max: 680px;      /* a focused single-column page (a form, a session) */
    --auth-max: 28rem;      /* log in and sign up: one short card (448) */
    --aside-w: 21rem;
    /* The screen a session fills, on a phone: the six hand sums this replaces
       spelled the same arithmetic twice, once in vh and once in svh (U2 §1).
       --screen-h-kb is the keyboard-up variant (no bottom nav, dvh: the visual
       viewport is what the keyboard shrinks). Both read --bar-h at use time, so
       the phone's 48 and a wide screen's 0 need no second copy. */
    --screen-h: calc(100svh - var(--bar-h) - var(--nav-h) - env(safe-area-inset-top) - env(safe-area-inset-bottom) - 2 * var(--space-4));
    --screen-h-kb: calc(100dvh - var(--bar-h) - env(safe-area-inset-top) - 2 * var(--space-4));
    /* The spacing scale: every margin, padding and gap is one of these steps,
       picked by role (2026-10-30; the ad-hoc .3/.45/.6/.7/.85/.9/1.1rem values
       read as random gaps from one component to the next). */
    --space-1: .25rem;      /* a label and its control */
    --space-2: .5rem;       /* inside a cluster: chips, small buttons, a menu */
    --space-3: .75rem;      /* between blocks inside a card; buttons side by side */
    --space-4: 1rem;        /* between cards; around a section's hairline */
    --space-5: 1.5rem;      /* between the page's columns */
    --space-6: 2rem;        /* around a finish (.done) */
    --space-7: 3rem;        /* an empty card; a page's foot */
    --gap: var(--space-4);      /* the page rhythm: between blocks */
    --gap-lg: var(--space-5);   /* …and between the page's columns */
    --card-pad: var(--space-4); /* every card surface, phone and desktop (his call) */
    /* Controls: three heights; a phone raises everything tappable to --touch.
       Borders: 1px for surfaces and hairlines, --control-border for controls. */
    --control-h-lg: 52px;   /* the answer bar, quiz options */
    --control-h: 46px;      /* buttons, fields */
    --control-h-sm: 36px;   /* small buttons, chips, switch options, menu rows */
    --touch: 44px;
    --control-border: 2px;
    /* The accent fill, painted under the border too: a 2px transparent border
       over a padding-box gradient showed the gradient's far end as a 1px seam
       on both sides of every primary button (2026-10-26). Anything filled with
       the accent that is not a chart uses it. */
    --accent-fill: linear-gradient(135deg, var(--accent), var(--accent-strong)) border-box;
    --on-accent: #fff;  /* text on the primary gradient */
    /* the one font stack (self-hosted Nunito + system fallbacks) */
    --font: 'Nunito', system-ui, -apple-system, 'Segoe UI', sans-serif;
    /* …and the one fixed-width stack: the CSV sample and the scramble's letters
       had two (ui-monospace and the generic, which is Courier on iOS), so the same
       job wore two faces (U1 §1.1). */
    --font-mono: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace;
    /* the chrome's own surface: top bar, side panel and the bottom nav are the
       same colour (tuned here, one place) */
    --nav-bg: var(--card);
    /* controls (the switch, the chips): the track they sit on, an option that is
       off, and an option that is on */
    --control-bg: var(--bg);
    --control-fg: var(--muted);
    --control-on-fg: var(--on-accent);
    /* ---------- the caret (one shape, every disclosure in the app) ----------
       Same geometry twice: `--caret-icon` is painted as a background (native
       <select> can't hold an element) and `--caret-mask` is masked with
       currentColor for .caret, so the element carets take the text colour of
       wherever they sit. Change the path once, here. */
    --caret-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%235d616b' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
    --caret-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
    /* The browser's own controls (checkboxes, the file picker, scrollbars) follow
       this, so they match the theme the account picked, not the OS. */
    color-scheme: light;
}
/* Dark — soft charcoal surfaces and the SAME indigo at its dark-mode step:
   light enough to sit on the charcoal, with dark text on its fill. */
:root[data-theme="dark"] {
    color-scheme: dark;
    --bg: #1b1c21;
    --bg-2: #202127;
    --card: #26282f;
    --text: #e4e5ea;
    --muted: #a4a8b4;    /* 5.36:1 on the card — headroom matters here: at
                            #a8b3c4 axe read the app bar's course pills at
                            4.48:1, under the 4.5 floor (2026-10-17) */
    --line: #363842;
    --accent: #8b8ef0;
    /* The accent is text as often as it is a surface (links, the current
       destination, tag chips on --accent-soft, .btn-soft labels): it must clear
       4.5:1 as text in the dark surround. */
    --accent-strong: #aeb0ff;   /* 5.3:1 on --accent-soft · 5.2:1 on the card */
    /* indigo, not slate — at #454e62 every chip, pill and tag lost the accent
       hue in dark and the two themes read as different designs (2026-10-17) */
    --accent-soft: #33345a;
    --good: #6cbf8a;
    --good-soft: #1f3329;
    --bad: #e07a7a;
    --bad-soft: #3b2326;
    --good-text: #93d8ab;  /* 6.1:1 on the card, 5.7:1 on --good-soft */
    --bad-text: #f3a7a7;   /* 4.9:1 on the card, 5.8:1 on --bad-soft */
    --warn-soft: #3a2d20;
    --warn-text: #f6c592;  /* 8.4:1 on --warn-soft, 8.9:1 on the card */
    --gold-soft: #3d3220;
    --gold-text: #f5cf7a;  /* 8.4:1 on --gold-soft, 9.9:1 on the card */
    --sky-soft: #22384d;
    --sky-text: #a6d2fa;   /* 7.6:1 on --sky-soft, 9.3:1 on the card */
    --accent-track: var(--accent-soft);
    --hub-track: var(--bg);
    --scrim: rgba(0, 0, 0, .6);
    --shadow: 0 10px 30px rgba(0, 0, 0, .35);
    --accent-edge: #5558c4;
    --mascot-rim: drop-shadow(1px 0 0 rgba(255, 255, 255, .2)) drop-shadow(-1px 0 0 rgba(255, 255, 255, .2)) drop-shadow(0 1px 0 rgba(255, 255, 255, .2)) drop-shadow(0 -1px 0 rgba(255, 255, 255, .2));
    --on-accent: #15162a;  /* the dark accent is light — dark text on it */
    --caret-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23a4a8b4' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
    --on-danger: #1b1c21;  /* 5.9:1 on the dark --bad */
}

}
@layer base {
/* ---------- base ---------- */
* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
/* Scrollbars thin, in the theme's line colour (his call, 2026-11-01): the page's
   and the inner boxes'. Phones draw their own overlay bar. */
* { scrollbar-width: thin; scrollbar-color: var(--line) transparent; }
html, body { margin: 0; font-family: var(--font); overflow-x: clip; }
}
@layer components {
/* SvelteKit's mount point (app.html): no box of its own, so a page's shell is
   the body's child and its layout is unchanged. */
.app-root { display: contents; }
}
@layer base {
/* html carries the base colour too: body's gradient is viewport-fixed, so a
   page taller than the viewport (and any overscroll) used to end in a black
   band in dark — visible in every full-page capture (2026-10-17). */
html { background: var(--bg); -webkit-text-size-adjust: 100%; text-size-adjust: 100%; touch-action: manipulation; }
/* No rubber band, desktop or phone (his call, 2026-10-31): the page stops at its
   edge, as an app does. It also keeps a sideways swipe on the deck from becoming
   iOS's back/forward gesture, and turns the browser tab's pull-to-refresh off. */
html { overscroll-behavior: none; }
/* `hidden` is how a page hides a block, and it must win over any component
   that sets its own `display` (a button, .alert-choice, .row-actions). */
[hidden] { display: none !important; }

}
@layer components {
/* ---------- icons (Lucide) ---------- */
svg.lucide { width: 1.15em; height: 1.15em; stroke-width: 2.4; vertical-align: -.18em; flex-shrink: 0; } /* a long label never squeezes its icon */
.fire-burst svg.lucide { width: 14px; height: 14px; }
.mainnav a .ico svg.lucide { width: 1.35rem; height: 1.35rem; }
}
@layer base {
body {
    color: var(--text);
    font-weight: var(--fw-body); line-height: var(--lh-body);   /* reading text: medium weight, room between lines */
    background: linear-gradient(180deg, var(--bg-2) 0%, var(--bg) 40%) fixed;
    min-height: 100vh;
    min-height: 100dvh;
    -webkit-font-smoothing: antialiased;
}
button { font: inherit; color: inherit; }
a { color: var(--accent-strong); }
img { max-width: 100%; }
h1, h2, h3 { margin: 0 0 var(--space-3); line-height: var(--lh-tight); }
/* The ladder (U1 §4): display · h1 · h2 · h3 · body · body-sm · caption · numeric ·
   mono — one rung per job, every size a token that already existed. A page's title
   outranks a card's: the public pages write <h1 class="h2"> (8 of them) and were
   landing at a card title's 20 px, so the rung follows the level, not the class. */
h1 { font-size: var(--fs-2xl); font-weight: var(--fw-black); }
@media (min-width: 900px) { h1 { font-size: var(--fs-3xl); } }
h2 { font-size: var(--fs-xl); font-weight: var(--fw-black); }
h3 { font-size: var(--fs-lg); font-weight: var(--fw-bold); }
}
/* …and the rungs as classes, a heading's look without its level: base holds
   elements only, so the class half of each rung is a primitive. */
@layer components {
.h1 { font-size: var(--fs-2xl); font-weight: var(--fw-black); }
h1.h2 { font-size: var(--fs-2xl); font-weight: var(--fw-black); }
@media (min-width: 900px) { .h1 { font-size: var(--fs-3xl); } }
.h2 { font-size: var(--fs-xl); font-weight: var(--fw-black); }
.h3 { font-size: var(--fs-lg); font-weight: var(--fw-bold); }
}
@layer base {
/* .h1/.h2/.h3: a heading's look without its level — the outline stays h1 → h2 → h3
   (axe heading-order) while a section label keeps the small size. */
/* A page's title is the h1 rung (.h1, --fs-2xl); a heading never demotes
   itself to .h2/.h3 for a smaller look. Cleaned in the 2026-11-06 type pass:
   privacy, terms, login, signup, verify, the check's two states, +error and
   the tool pages' title (ToolPage). design.test.ts holds the rule. */
small { color: var(--muted); font-size: var(--fs-sm); }
:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }

}
@layer components {
/* ---------- app shell ----------
   Two shells from one page: a phone gets the app bar and the fixed bottom nav; a
   wide screen has no top bar at all — everything (brand, destinations, the
   account) lives in the collapsible left panel. Sizes are tokens: --bar-h (the
   app bar), --nav-h (the bottom bar), --side-w / --side-w-min (the panel). */
.appbar {
    position: sticky; top: 0; z-index: 40;
    display: flex; align-items: center; gap: var(--space-2);
    padding: calc(env(safe-area-inset-top) + var(--space-2)) calc(env(safe-area-inset-right) + var(--space-4)) var(--space-2) calc(env(safe-area-inset-left) + var(--space-4));
    background: var(--nav-bg);
    backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
    border-bottom: 1px solid var(--line);   /* a hairline: the bar needs an edge */
}
.shell { display: block; }
.sidenav { display: none; }        /* phones: the top bar + the bottom nav */
.mainnav-bottom { display: block; }/* …and this is that bottom bar */
.brand {
    display: inline-flex; align-items: center; gap: var(--space-2);
    font-size: var(--fs-lg); font-weight: var(--fw-black); text-decoration: none;
    color: inherit; white-space: nowrap;
}
/* the mark rounds its own corners (icon.source.svg) */
.brand .logo { display: block; }
.appbar-right { display: flex; align-items: center; gap: var(--space-2); margin-left: auto; }
/* The account menu (UserMenu.svelte): a <details> whose trigger is plain text,
   like the brand: name + a caret, no pill, no background. */
.user-menu, .word-menu { position: relative; }
.user-menu > summary {
    list-style: none; cursor: pointer; user-select: none;
    display: inline-flex; align-items: center; gap: var(--space-1);
    padding: var(--space-1) 0; border-radius: var(--radius-md);
    background: none; box-shadow: none; color: var(--text);
    font-weight: var(--fw-black); font-size: var(--fs-lg); line-height: var(--lh-tight);
}
.user-menu > summary::-webkit-details-marker, .word-menu > summary::-webkit-details-marker { display: none; }
.user-menu > summary .menu-name { max-width: 9rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.user-menu > summary .caret { width: .8em; height: .8em; color: var(--muted); }
.user-menu[open] > summary .caret { color: var(--accent-strong); }
/* the panel: a card, one row per action; a word card's ⋯ menu (WordCard.svelte)
   is the same panel with its rows closer together */
.user-menu-panel, .word-menu-panel {
    position: absolute; right: 0; top: calc(100% + var(--space-2)); z-index: 60;
    display: flex; flex-direction: column; align-items: stretch;
    background: var(--card); border: 1px solid var(--line); border-radius: var(--radius-lg); box-shadow: var(--shadow);
}
.user-menu-panel { gap: var(--space-3); padding: var(--card-pad); min-width: 18.5rem; }
.word-menu-panel { gap: var(--space-1); padding: var(--space-2); min-width: 13rem; }
.user-menu:not([open]) .user-menu-panel { display: none; }
/* A word card's ⋯ menu (WordCard.svelte). */
.word-menu > summary { list-style: none; }
.word-menu:not([open]) .word-menu-panel { display: none; }
/* A dropdown at the screen's foot opens upwards (dropdown.ts). */
.user-menu.is-up > .user-menu-panel, .word-menu.is-up > .word-menu-panel { top: auto; bottom: calc(100% + var(--space-2)); }
.menu-item--danger, .menu-item--danger svg.lucide { color: var(--bad); }
/* A menu entry is a row, not a button: Settings is a link (it goes somewhere),
   Install and Log out are buttons (they do something here) — one look: icon +
   label, the row tints on hover. */
.menu-item {
    display: flex; align-items: center; text-decoration: none; cursor: pointer;
    width: 100%; min-height: var(--control-h-sm); justify-content: flex-start; gap: var(--space-2);
    padding: var(--space-2); border: 0; border-radius: var(--radius-md);
    background: none; box-shadow: none; color: var(--text);
    font-size: var(--fs-md); font-weight: var(--fw-bold);
}
.menu-item svg.lucide { color: var(--muted); }
/* The switches first, then Install, Settings and Log out close together, apart
   from the switches by space, no divider (his calls, 2026-11-01 and 11-03) */
.menu-links { display: flex; flex-direction: column; gap: var(--space-1); padding-top: var(--space-2); }
/* A labelled field: the small caps label, then its control — the switches
   (language, theme, goal), the chip rows, Add word's language pickers. */
.pref-group, .chips-row { display: flex; flex-direction: column; gap: var(--space-1); }
.pref-group + .pref-group, .chips-row + .chips-row { margin-top: var(--space-3); }
/* The caption rung's one caps recipe: a field's or a filter row's name. Only
   these two wear it (U1 §6 pick 3). */
.pref-label, .chips-row > .chips-label {
    padding: 0 var(--space-1); color: var(--muted);
    font-size: var(--fs-xs); font-weight: var(--fw-black); text-transform: uppercase; letter-spacing: var(--tracking-caps);
}
/* A slide-down's trigger is a control, not a label: body, 16/700 (U1 §6 pick 3 —
   every disclosure in the app was 12 px all caps, and the landing's FAQ had to opt
   out of it, which is the proof the recipe was misassigned). */
.collapse > summary {
    padding: var(--space-2) 0; color: var(--text);
    font-size: var(--fs-md); font-weight: var(--fw-bold);
}
/* ---------- the switch (segmented) ----------
   Two or three options out of which exactly one is on: a single thumb slides
   to the tapped option (Segmented.svelte moves it, then applies the choice).
   The theme, the languages, the goal, the landing's two ways in. */
.segmented {
    position: relative; display: grid; grid-auto-flow: column; grid-auto-columns: 1fr;
    padding: var(--space-1); background: var(--control-bg);
    border: var(--control-border) solid var(--line); border-radius: var(--radius-pill);
}
.segmented-thumb {
    position: absolute; top: var(--space-1); bottom: var(--space-1); left: var(--space-1);
    width: calc((100% - 2 * var(--space-1)) / var(--seg-count, 2));
    transform: translateX(calc(var(--seg-index, 0) * 100%));
    background: var(--accent-fill);
    border-radius: var(--radius-pill); z-index: 0;
    transition: transform var(--dur-in) var(--ease-bounce);
}
.segmented-opt {
    position: relative; z-index: 1;
    display: inline-flex; align-items: center; justify-content: center; gap: var(--space-1);
    width: 100%; min-width: 0; min-height: var(--control-h-sm); padding: 0 var(--space-2);
    background: none; border: 0; box-shadow: none; border-radius: var(--radius-pill);
    color: var(--control-fg); font: inherit; font-size: var(--fs-sm); font-weight: var(--fw-bold);
    white-space: nowrap; cursor: pointer;
}
/* A three-option switch in Russian («Сложно · 100», «Системная»): the widest
   label sets the track, and at 320 it has no shrink path — so the option takes
   two lines instead of clipping (U8 ✗6, the .grid-2 .btn precedent). Scoped to
   320, the one width U8 measured it failing at: at 360 the row is green and a
   wrap there would be a new regression. */
@media (max-width: 320px) { .segmented-opt { white-space: normal; } }
/* A three-option switch in the account menu (the language, the theme): Russian's
   «Английский» is 82 px, more than a third of the panel's width, so the panel is
   as wide as the switch needs and the option gives up its side padding — never a
   smaller type, a wrap or a cut (his rule, 2026-11-01; the 2026-11-06 wave's ru
   overflow). 18.5rem still fits a 320 px screen with its margins. */
.user-menu-panel .segmented-opt { padding: 0; }
/* …and the track gives back its own inset too: Russian's «Английский» needs
   every pixel of the cell (85 in 83, 2026-11-07). The thumb follows the track
   it now fills. */
.user-menu-panel .segmented { padding: 0; }
.user-menu-panel .segmented-thumb {
    top: 0; bottom: 0; left: 0;
    width: calc(100% / var(--seg-count, 2));
}

.segmented-opt svg.lucide { width: .95rem; height: .95rem; }
.segmented-opt[aria-current="true"] { color: var(--control-on-fg); }

/* one nav list (NavList.svelte): vertical in the desktop side panel, a fixed
   bottom bar on phones (two renders — only the visible one is displayed) */
.mainnav ul { list-style: none; margin: 0; padding: 0; display: flex; }
.mainnav a {
    display: flex; align-items: center; gap: var(--space-2);
    text-decoration: none; color: var(--muted); font-weight: var(--fw-bold);
}
.mainnav a[aria-current="page"] { color: var(--accent-strong); }

@media (min-width: 900px) {
    /* No top bar: the left panel holds the brand, the destinations and the
       account (its foot — where the menu opens upwards). Only the app shell
       has that panel — the public pages (`<body class="public">`) have nothing
       to replace the bar, so on a wide screen they would lose their brand,
       language and theme switches altogether (2026-10-17). */
    body:not(.public) .appbar { display: none; }
    .mainnav-bottom { display: none; }
    /* a phone on its side has the notch at one edge: the panel grows by the inset */
    .shell { display: grid; grid-template-columns: calc(var(--side-w) + env(safe-area-inset-left)) minmax(0, 1fr); align-items: start; }
    .sidenav {
        display: flex; flex-direction: column; gap: var(--space-2);
        position: sticky; top: 0; z-index: 30; height: 100dvh;
        background: var(--nav-bg);
        padding: var(--space-4) var(--space-2) var(--space-3) calc(env(safe-area-inset-left) + var(--space-2)); border-right: 1px solid var(--line);
        /* no overflow clipping: the account menu opens out of the panel and must
           be able to leave it (2026-10-16) — the destinations fit by design */
        overflow: visible;
    }
    .side-head { display: flex; align-items: center; gap: var(--space-2); padding: 0 var(--space-1) var(--space-2); }
    .side-head .brand { flex: 1; min-width: 0; font-size: var(--fs-xl); }
    html.nav-collapsed .side-head { flex-direction: column; gap: var(--space-2); }
    /* Collapsed, the panel shows icons only — but the labels are clipped, not
       display:none: a hidden label took the link's accessible name with it, so a
       collapsed panel read "link, link, link, link" to a reader (U7 §2.3),
       including the brand's. Same recipe as .sr-only. */
    html.nav-collapsed .brand-text,
    html.nav-collapsed .nav-label,
    html.nav-collapsed .side-foot .menu-name {
        position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
        overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
    }
    html.nav-collapsed .side-foot .user-menu > summary { justify-content: center; }
    .mainnav { flex: 1; }
    .mainnav ul { flex-direction: column; gap: var(--space-1); }
    .mainnav a { padding: var(--space-2); border-radius: var(--radius-md); }
    .mainnav a[aria-current="page"] { background: var(--accent-soft); }
    .side-foot { display: flex; flex-direction: column; gap: var(--space-2); margin-top: auto; }
    /* the account menu at the bottom of the panel: it opens upwards, to the right */
    .side-foot .user-menu { width: 100%; }
    .side-foot .user-menu > summary { width: 100%; justify-content: space-between; }
    .side-foot .user-menu-panel {
        top: auto; bottom: calc(100% + var(--space-2)); left: 0; right: auto;
    }
    /* the collapse toggle: a caret, nothing else — it points at where the panel
       will go (left = collapse, right = open) */
    .nav-toggle {
        display: inline-flex; align-items: center; justify-content: center;
        width: var(--control-h-sm); height: var(--control-h-sm); padding: 0; flex-shrink: 0; cursor: pointer;
        background: var(--card); border: var(--control-border) solid var(--line); border-radius: var(--radius-pill);
        color: var(--muted); box-shadow: none;
    }
    .nav-toggle .caret { width: .8em; height: .8em; transform: rotate(90deg); }
    html.nav-collapsed .nav-toggle .caret { transform: rotate(-90deg); }
    html.nav-collapsed .mainnav a { justify-content: center; }
    html.nav-collapsed { --side-w: var(--side-w-min); }
}
@media (max-width: 899px) {
    /* PWA chrome: the inset above the app bar and below the bottom nav is the
       same surface as the bars themselves, not the page gradient. */
    html { background: var(--nav-bg); }
    .appbar {
        /* backdrop-filter would make the appbar the containing block of the
           fixed bottom nav; keep the bar opaque here instead (its --nav-bg is
           the base rule's). */
        backdrop-filter: none;
        -webkit-backdrop-filter: none;
    }
    /* the hairline is its edge: no shadow (2026-10-30) */
    .mainnav-bottom {
        position: fixed; left: 0; right: 0; bottom: 0; z-index: 40;
        background: var(--nav-bg); border-top: 1px solid var(--line);
        padding: 0 env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
    }
    .mainnav ul { width: 100%; max-width: 560px; margin: 0 auto; }
    .mainnav li { flex: 1; min-width: 0; }
    .mainnav a {
        flex-direction: column; justify-content: center; gap: var(--space-1); min-width: 0;
        height: var(--nav-h); font-size: var(--fs-xs);
    }
    /* One line each: a label on two lines ("Добавить слово") lifted its icon
       above the other four. The longest tab takes the room it needs and the
       others give theirs — every Russian label fits down to 360px (2026-10-30).
       At 320 the row's min-content is wider than the bar, so a label is cut with
       an ellipsis instead of running past the edge with no sign (U8 ✗5). */
    .mainnav a .nav-label { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
    .mainnav a .ico { font-size: var(--fs-xl); line-height: var(--lh-none); }
    .mainnav a[aria-current="page"] .ico { transform: scale(1.12); }
    /* Phones: compact cluster — the account menu only. Keep the top inset in
       the padding: this rule used to replace the base padding outright, so the
       app bar stopped painting the PWA status-bar strip (2026-10-20). */
    .appbar { padding: calc(env(safe-area-inset-top) + var(--space-2)) calc(env(safe-area-inset-right) + var(--space-3)) var(--space-2) calc(env(safe-area-inset-left) + var(--space-3)); }
    .user-menu-panel { min-width: 18.5rem; }
}
/* The content column. On a phone it is the block flow, centred by its own
   margins; inside the desktop grid those margins must go — an auto margin on a
   grid item makes it shrink-to-fit its content, which is how every page ended up
   a different width (add word 373px, discover 1018px, 2026-10-16). */
.app-main {
    width: 100%; max-width: var(--app-max); margin: 0 auto;
    padding: var(--space-4) calc(env(safe-area-inset-right) + var(--space-4)) calc(var(--nav-h) + env(safe-area-inset-bottom) + var(--space-4)) calc(env(safe-area-inset-left) + var(--space-4));
}
/* The page's last block keeps no margin: on a phone the gap above the nav is the
   gap under the bar (his note, 2026-11-04: it was 2.5rem against 1rem). */
.app-main > :last-child, .app-main > :last-child > :last-child { margin-bottom: 0; }
@media (min-width: 900px) {
    .app-main { margin: 0; justify-self: center; padding: var(--space-5) calc(env(safe-area-inset-right) + var(--space-5)) var(--space-7) var(--space-5); }
    /* no top bar on a wide screen: the toast sits near the top edge */
    :root { --bar-h: 0px; }
    /* …but a public page keeps its bar (only the app's top bar hides), so 0 put the
       landing's section links' anchors and every toast *under* a ~62 px bar
       (U2 §1, a bug). Its height, from its own padding and control: 8 + 46 + 8. */
    html:has(> body.public) { --bar-h: calc(2 * var(--space-2) + var(--control-h)); }
}
/* ---------- moving between screens ----------
   A view transition (the root layout starts one for each move to another
   screen): the bars stay put, the old screen goes at once and the new one
   rises in. Not a crossfade: the two screens' heights and scroll positions
   differ, so the old one would slide or ghost over the new. The root swaps
   without blending (plus-lighter over an unanimated pair would wash it out).
   The phone's bottom bar is a layer of its own, above the screen: in the
   root's, the new screen rose over it and it showed only once the move ended
   (his bug, 2026-11-04). It swaps at once, the new tab lit. */
.app-main { view-transition-name: screen; }
.mainnav-bottom { view-transition-name: tabbar; }
::view-transition-group(tabbar), ::view-transition-new(tabbar) { animation: none; }
::view-transition-old(tabbar) { display: none; }
::view-transition-old(root), ::view-transition-new(root) { animation: none; mix-blend-mode: normal; }
::view-transition-group(screen) { animation: none; }
::view-transition-old(screen) { animation: none; opacity: 0; }
::view-transition-new(screen) { animation: ws-screen-in var(--dur-in) var(--ease-out) both; mix-blend-mode: normal; }
@keyframes ws-screen-in { from { opacity: 0; transform: translateY(10px); } }

/* ---------- the page shapes ----------
   A page is one of three things:
     .page-focus  one narrow column (a form, a session, a lesson) — --form-max
     .page-grid   a main column and a side column — --app-max, --aside-w
     .card-grid   a set of independent cards, auto-fit
   Everything sits in the one container above; a page never sizes itself. On a
   phone the main column leads (`order`, below) so the primary content is read
   first; the desktop placement is explicit (it ignores both DOM order and
   `order`). Every shape starts at the container's top-left corner: a focus
   column centred on the screen (Add word, a session) sat apart from every
   other page (2026-10-26). There is no visible page title — the navigation
   already says where you are; each page keeps an .sr-only h1 for the outline. */
.page-focus { max-width: var(--form-max); }
.page-grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--gap); }
.page-grid > .col-main, .page-grid > .col-side { min-width: 0; }
/* the grid's gap is the space between the columns' cards: a last card's own
   margin doubled it on a phone (2026-10-26) */
.page-grid > .col-main > :last-child, .page-grid > .col-side > :last-child { margin-bottom: 0; }
.page-grid > .col-side { display: flex; flex-direction: column; }
/* One column, content first: the side cards (the dictionary's filters, the
   training tag picker) are written before the main column for the desktop's
   explicit placement, but stacked that way on a phone they pushed the word
   list / the exercise buttons below the fold (walk finding 4, 2026-10-16). */
.page-grid > .col-main { order: -1; }
/* …unless the side card picks what the main column shows (Lessons' course
   card: his call, 2026-11-01): then it leads. */
.page-grid--side-first > .col-main { order: 0; }
/* Settings: one card per row until there is room for two. The old auto-fit
   split the cards at 768px, halving every control (the Delete All button was
   clipped inside its form) — 2026-10-19. Two columns are columns, not rows: in
   a grid every card started level with its neighbour, so a short card left a
   hole under it — the cards now pack down each column, one gap everywhere
   (2026-10-26). */
/* minmax(0, …): a card's widest unbreakable content (a long word, a <pre>)
   can't widen the column, and so the page, on a phone (2026-10-31). */
.card-grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--gap); align-items: start; }
.card-grid > .card { margin: 0; }
/* Settings' two columns, as a grid, at the shell's own wide step — the CSS
   `columns: 2` this replaces was the app's only column-major layout, so the DOM
   order and the visual order disagreed (U2 §2). */
@media (min-width: 900px) {
    .card-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
/* A focused page fills the phone's shell: it used to stop at 680 and sit left,
   leaving a dead band of 56 px at 768 and 187 px at 899 (U8 ✗3). Above the step
   the 680 column stands, top-left, as it has since 2026-10-26. */
@media (max-width: 899px) {
    .page-focus { max-width: 100%; }
}
@media (min-width: 1024px) {
    /* 1024, not the shell's 900: at 900 the panel takes 216 and the aside 336, so
       the main column would be ~276 — narrower than a 320 phone's. Measured in U8
       §5 as the one band with no rule of its own; the CTO's word decides it. */
    .page-grid {
        grid-template-columns: minmax(0, 1fr) var(--aside-w);
        gap: var(--gap-lg); align-items: start;
    }
    /* A page with no side column (the Dictionary) has nothing for the second
       track: it kept an empty band on its right, the list 552 of 912 px (walk
       2026-11-05). Discover, Training, Lessons and Add word all have one. */
    .page-grid:not(:has(> .col-side)) { grid-template-columns: minmax(0, 1fr); }
    .page-grid > .col-main { grid-area: 1 / 1; }
    .page-grid > .col-side { grid-area: 1 / 2; }
}

/* ---------- cards ---------- */
.card {
    background: var(--card); border-radius: var(--radius-lg);
    box-shadow: var(--shadow); padding: var(--card-pad); margin: 0 0 var(--gap);
}
.card > :last-child { margin-bottom: 0; }
/* A page's heading block (title + the action that belongs to it) — one rhythm,
   so nothing sits flush against what follows. Its parts: the h1 (the visible title
   on a public page, .sr-only in the app), one state line, at most one small action
   at the right, --gap below; never a card (U2 §2). */
.page-head { display: flex; flex-direction: column; gap: var(--space-3); margin-bottom: var(--gap); }
.page-head > h1, .page-head > h2 { margin: 0; }
/* the state line (Review's count, a filter's result): the head's gap does its spacing */
.page-head > .caption { margin: 0; }
/* The dictionary's one box (search or add), its filters' line under it (his
   call, 2026-11-01: Add word's line). */
.page-head > form { margin: 0; }
.page-head > .collapse { margin: 0; }
/* Add word's search layout there (his ask, 2026-11-04): the head's gap spaces
   its parts, so their own top margins go; its languages' line on the same step. */
.page-head > .mt-3 { margin-top: 0; }
/* …in one row when it is a state line and its one small action (Review's count and
   Undo: the `.caption-row` it was, DES-2 2026-11-08) */
.page-head--row { flex-direction: row; align-items: center; justify-content: space-between; gap: var(--space-2); }
/* the way back (a lesson's) keeps its own width in the column */
.page-head > .plain-link { align-self: flex-start; }
.page-head > form > .collapse { margin-top: var(--space-3); }
/* The pager: back at the left, on at the right, the page between. */
.pager { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); margin-top: var(--space-4); }
.muted { color: var(--muted); }
/* content for assistive tech only: a page's h1 when the design shows no title */
.sr-only {
    position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
    overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
/* The skip link (2026-11-04): every page's first Tab stop, above the screen
   until focused; it moves focus to the page's main, which shows no ring. */
.skip-link {
    position: fixed; z-index: 80; left: var(--space-3); top: calc(env(safe-area-inset-top) + var(--space-2));
    padding: var(--space-2) var(--space-3); border-radius: var(--radius-md);
    background: var(--card); color: var(--accent-strong); border: 1px solid var(--line); box-shadow: var(--shadow);
    font-weight: var(--fw-bold); text-decoration: none;
    transform: translateY(-200%);
}
.skip-link:focus { transform: none; }
main:focus { outline: none; }
.center { text-align: center; }
/* Buttons side by side: --space-3 apart (chips and small clusters: --space-2). */
.row-actions { display: flex; gap: var(--space-3); flex-wrap: wrap; align-items: center; }
/* A long button label wraps inside its cell instead of overflowing the grid:
   the Backups card's "Create & Download New Backup" ran past the card edge,
   clipped by the neighbouring card (his known list, 2026-10-16). */
.grid-2, .answer-actions, .dialog-actions { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: var(--space-3); }
.grid-2 .btn { white-space: normal; }
@media (max-width: 600px) {
    .grid-2 { grid-template-columns: 1fr; }
    /* a joined button's label gives its room to the field on a phone: the icon
       stays, the words stay for assistive tech */
    .join .btn-label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
}
/* The training modes: equal tiles, the count under the name. */
.mode-grid { grid-auto-rows: 1fr; }
.mode-grid > .btn { flex-direction: column; gap: 0; }
.mode-count { font-size: var(--fs-sm); font-weight: var(--fw-regular); }

/* ---------- buttons ---------- */
button, .btn {
    display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2);
    min-height: var(--control-h); padding: var(--space-2) var(--space-4); border-radius: var(--radius-md);
    border: var(--control-border) solid transparent;
    cursor: pointer; text-decoration: none;
    font-weight: var(--fw-bold); font-size: var(--fs-md); white-space: nowrap;
    background: var(--accent-fill);
    color: var(--on-accent); box-shadow: var(--shadow-accent);
    transition: transform var(--dur-press) var(--ease-out), filter var(--dur-press) var(--ease-out), box-shadow var(--dur-press) var(--ease-out);
    user-select: none; -webkit-user-select: none;
}
/* The press (his ask, 2026-11-03): everything tappable sinks a touch and eases back.
   The rule below was dead — the base button's own transition out-ranked it (U9
   §2.11) — so the press now names every trigger: buttons, rows, a slide-down's
   summary and the scramble's letter tiles (spans with role="button", which were
   tappable and silent, U9 §2.8). */
:where(button, .btn, .chip-btn, .menu-item, a.card) { transition: transform var(--dur-press) var(--ease-out), filter var(--dur-press) var(--ease-out), box-shadow var(--dur-press) var(--ease-out); }
:where(summary, .letter-tile, .letter-slot) { transition: transform var(--dur-press) var(--ease-out); }
:where(button, .btn, .chip-btn, .menu-item, a.card, summary, .letter-tile, .letter-slot):active { transform: scale(.97); }
/* Disabled is a state, not a faded primary: half-opacity left the scramble
   Check at 2.4:1 on the card (2026-10-19). A neutral surface keeps the label
   readable while still looking unavailable. */
button[disabled], .btn[disabled] {
    opacity: 1; background: var(--bg-2); color: var(--muted);
    border-color: var(--line); box-shadow: none; pointer-events: none;
}
.btn-soft { background: var(--accent-soft); color: var(--accent-strong); box-shadow: none; }
.btn-line { background: transparent; border-color: var(--line); color: var(--text); box-shadow: none; }
/* Bare: the icon alone, no border, no fill, still a button's hit area and focus
   ring; on hover the accent tint of a menu row (the word card's ⋯, his call 2026-11-03). */
.btn-bare { background: transparent; border-color: transparent; color: var(--muted); box-shadow: none; }
.btn-block { width: 100%; }
.btn-sm { min-height: var(--control-h-sm); padding: var(--space-1) var(--space-3); font-size: var(--fs-sm); line-height: var(--lh-tight); }
/* The label half of an icon+label button: it may shrink (the account menu's
   entries are the long ones) instead of pushing the icon out of the row. One
   line, so a long label is cut with an ellipsis rather than wrapped (2026-11-04). */
.btn-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.btn-danger {
    background: transparent; border-color: var(--bad); color: var(--bad-text); box-shadow: none;
}
/* The filled weight: the button that actually commits a destructive action
   (the confirm dialog's OK), so a row action and a commitment never look alike. */
.btn-danger-fill { background: var(--bad); border-color: var(--bad); color: var(--on-danger); box-shadow: none; }
/* The raised primary sinks onto its edge when pressed (his pick, 2026-11-03);
   the flat ones above, and a button joined to a field, keep the 97% press. */
.btn:not(.btn-soft, .btn-line, .btn-bare, .btn-danger, .btn-danger-fill, .join .btn, .landing-end .btn):active {
    transform: translateY(var(--raise)); box-shadow: 0 0 0 var(--accent-edge);
}

}
@layer base {
/* ---------- forms ---------- */
label { display: block; font-weight: var(--fw-regular); }
input:where(:not([type="radio"], [type="checkbox"])), select, textarea {
    width: 100%; min-height: var(--control-h); padding: var(--space-2) var(--space-3);
    border-radius: var(--radius-md); border: var(--control-border) solid var(--line);
    background: var(--card); color: var(--text); font: inherit;
}
/* A field takes the accent border on any focus, and the global ring when the
   keyboard has it: only the mouse's focus comes without the outline (U7 §2.9 —
   this was the one place the app's focus ring was off). Under the ring the border
   goes back to the hairline: accent border plus accent ring read as two. */
input:focus, select:focus, textarea:focus { border-color: var(--accent); }
input:focus:not(:focus-visible), select:focus:not(:focus-visible), textarea:focus:not(:focus-visible) { outline: none; }
input:focus-visible, select:focus-visible, textarea:focus-visible { border-color: var(--line); }
/* A box grows taller, never wider (his call, 2026-10-31). */
textarea { resize: vertical; }
/* Chrome's grip is square: its tip poked out of the rounded corner (his bug,
   2026-11-03). Hidden; the corner still drags. */
textarea::-webkit-resizer { background: transparent; }
}
@layer components {
/* A field joined to its button: one control, one continuous outline, no seam
   (the dictionary's search, the quick add above the word list). The field
   keeps the left corners, the button the right — the same radius — and the
   two borders that would meet in the middle are dropped. */
.join { display: flex; align-items: stretch; margin: 0; }
.join input { border-radius: var(--radius-md) 0 0 var(--radius-md); border-right-width: 0; margin: 0; }
.join button { border-radius: 0 var(--radius-md) var(--radius-md) 0; margin: 0; flex-shrink: 0; box-shadow: none; }
/* a field whose button comes and goes (a tag's rename check) is whole while alone */
.join button svg.lucide { width: 1.1em; height: 1.1em; }
}
@layer base {
/* Every <select> uses the token chevron, with room to breathe on the right —
   the native arrow sat hard against the border (2026-10-16). */
select {
    appearance: none; -webkit-appearance: none; -moz-appearance: none;
    padding-right: calc(var(--space-3) + var(--space-6));
    background-image: var(--caret-icon);
    background-repeat: no-repeat;
    background-position: right var(--space-3) center;
    background-size: 1.05rem 1.05rem;
}
select::-ms-expand { display: none; }
}
@layer components {
/* a select inside a row of controls keeps its share of the line */
.row-actions select { width: auto; flex: 1 1 8rem; }
}
@layer base {
/* The file picker (Settings → Import): the field, and in it a soft small
   button instead of the browser's own. */
input[type="file"] { padding: var(--space-1); }
input[type="file"]::file-selector-button {
    min-height: calc(var(--control-h) - 2 * var(--control-border) - 2 * var(--space-1));
    margin-right: var(--space-3); padding: 0 var(--space-3);
    border: 0; border-radius: var(--radius-md); cursor: pointer;
    background: var(--accent-soft); color: var(--accent-strong);
    font: inherit; font-size: var(--fs-sm); font-weight: var(--fw-bold);
}
input[type="radio"], input[type="checkbox"] { margin: 0 var(--space-2) 0 0; accent-color: var(--accent-strong); }
fieldset { border: 0; padding: 0; margin: 0 0 var(--space-3); }
legend { font-weight: var(--fw-bold); margin-bottom: var(--space-2); }

}
@layer components {
/* ---------- the caret element ----------
   Used by every disclosure (the account menu trigger, the slide-downs): it is
   masked with the token shape and painted with currentColor, so it inherits the
   text colour and size of its context — one caret, all languages, any theme. */
.caret {
    display: inline-block; width: .7em; height: .7em; flex-shrink: 0;
    background: currentColor; vertical-align: middle;
    -webkit-mask: var(--caret-mask) center / contain no-repeat;
    mask: var(--caret-mask) center / contain no-repeat;
    transition: transform var(--dur-press) var(--ease-out);
}
.user-menu[open] > summary .caret, details[open] > summary .caret { transform: rotate(180deg); }

/* ---------- sections inside a card ---------- */
/* One card per page: a section is separated by a line, never a nested card, so
   the page keeps the card's own width and padding (2026-10-16). */
.section { margin-top: var(--space-4); padding-top: var(--space-4); border-top: 1px solid var(--line); }
/* A slide-down: the summary is the small caps label with a caret that flips
   when open; the body is plain content of the surrounding card. Add word's
   languages and follow-up, the Dictionary's filters, Settings' CSV guide. No
   divider above it (his call, 2026-11-01). */
.collapse { margin: var(--space-4) 0 0; }
.collapse > summary { display: flex; align-items: center; gap: var(--space-2); list-style: none; }
.collapse > summary::-webkit-details-marker { display: none; }
.collapse > summary .caret { width: .8em; height: .8em; }
.collapse-body { margin-top: var(--space-3); }
}
@layer base {
summary { cursor: pointer; font-weight: var(--fw-bold); }

}
@layer components {
/* ---------- small bits ---------- */
/* vertical rhythm for a column of blocks (a form, a card's content, a list) */
.stack > * + * { margin-top: var(--space-3); }
/* …and a section keeps its own step around the hairline */
.stack > .section, .stack > .collapse { margin-top: var(--space-4); }
/* Chips are the app's small labels: a number that matters, a tag. A system
   tag (Mastered, Top 1000) is filled. The ghost weight is information only — a
   rank, a hint: no fill, a hairline, muted text. Interactive chips are
   .chip-btn, not a chip. */
.chip, .chip-ghost {
    display: inline-block; padding: var(--space-1) var(--space-2); border-radius: var(--radius-pill);
    font-weight: var(--fw-bold); font-size: var(--fs-sm); line-height: var(--lh-tight);
}
/* one look for every tag, built-in or the learner's own (his call, 2026-11-01) */
.chip { background: var(--accent-soft); color: var(--accent-strong); }
.chip-ghost { border: 1px solid var(--line); background: transparent; color: var(--muted); font-size: var(--fs-xs); }
/* ---------- chips: tap-to-choose (one component) ----------
   Every multi-choice in the app is a .chips row of .chip-btn buttons (or
   links): tags on a word, tags in bulk, a filter, a language. A chip that is
   on carries aria-pressed (a toggle) or aria-current (one of a few: our rule is
   five options or fewer become chips, more stay a <select>). */
.chips { display: flex; flex-wrap: wrap; gap: var(--space-2); }
/* a few chips sharing the row equally, like a switch's options (Settings'
   I'm learning, his call 2026-11-01) */
.chips--fill { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); }
.chips .chip-btn {
    display: inline-flex; align-items: center; justify-content: center; gap: var(--space-1);
    min-width: 0; max-width: 100%; min-height: var(--control-h-sm); padding: var(--space-1) var(--space-3); border-radius: var(--radius-pill);
    border: var(--control-border) solid var(--line); background: var(--card); color: var(--control-fg);
    font: inherit; font-size: var(--fs-sm); font-weight: var(--fw-bold); box-shadow: none; line-height: var(--lh-tight);
    cursor: pointer; white-space: nowrap; transition: border-color var(--dur-press) var(--ease-out), color var(--dur-press) var(--ease-out), transform var(--dur-press) var(--ease-out);
    text-decoration: none;   /* a chip is also a link (a language view, a filter) */
}
.chips .chip-btn[aria-pressed="true"], .chips .chip-btn[aria-current="true"] {
    background: var(--accent-fill);
    border-color: transparent; color: var(--on-accent);
}
/* half on: some of the selected words have the tag (the Dictionary's bulk Tags) */
.chips .chip-btn[aria-pressed="mixed"] { background: var(--accent-soft); border-color: var(--accent); color: var(--accent-strong); }
.chips .chip-btn svg.lucide { width: .9rem; height: .9rem; }
/* A learner's tag can be longer than the row: the chip stays inside it and its
   label is cut with a visible ellipsis, never clipped mid-word with no sign
   (the Dictionary's filters, the word card's picker, the bulk dialog; walk
   2026-11-05). The label needs its own box — text-overflow does not reach the
   anonymous text item of a flex chip. (min-width/max-width ride the chip above.) */
.chips .chip-btn > .chip-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chips-empty, .hub-cheer, .wcard-def { color: var(--muted); font-size: var(--fs-sm); font-weight: var(--fw-regular); }
/* Long unbroken text breaks instead of running out of its box (U8 ✗4): the
   definition is the longest thing a learner writes, and .says carries a search
   box's text. Only the term and a row had this, so a 60-character string left
   every card at every width, cut by `overflow-x: clip` with no sign. */
.wcard-def, .flash .def, .flash .forms, .says, .practice-cta > span { overflow-wrap: anywhere; }
/* A boxed notice in a form: Add word's "Did you mean" (.alert-choice). Errors
   are toasts (.toast.error), never a box (his call, 2026-10-31). */
.alert {
    margin: 0 0 var(--space-3); padding: var(--space-3) var(--space-4); border-radius: var(--radius-md);
    background: var(--bad-soft); color: var(--bad-text); border: 1px solid var(--bad);
}
/* A chat turn (Add word's follow-up): the user's message on the accent fill,
   the answer on its soft tint. */
.msg {
    padding: var(--space-2) var(--space-3); border-radius: var(--radius-md);
    background: var(--accent-soft); text-align: left;
}
.msg--user { background: var(--accent-fill); color: var(--on-accent); text-align: right; }
/* A scrolling transcript (the follow-up history). */
.scroll-box {
    max-height: 12rem; overflow-y: auto; padding: var(--space-2);
    background: var(--bg-2); border-radius: var(--radius-md);
}
/* The add-word suggestion reads as one sentence — "Did you mean: [word]?" —
   with the refusal beside it; the row wraps in units, so the "?" can no longer
   end up alone on a line (2026-10-17). */
.alert-choice, .wcard-title { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); }
.alert-choice .suggest-q { white-space: nowrap; }
.text-success { color: var(--good-text); }
.text-danger { color: var(--bad-text); }
}
@layer base {
table { width: 100%; border-collapse: collapse; }
}
@layer components {
/* The analytics table on a phone: the day, its pageviews and its visitors; the
   sign-ups and the first words stay in the tiles and the API, because five
   columns spill past a 360 px edge (2026-11-07). */
@media (max-width: 640px) {
    .table-days th:nth-child(n + 4), .table-days td:nth-child(n + 4) { display: none; }
}
/* A real ad name is long: 34 characters widened the three columns a phone keeps
   and `overflow-x: clip` cut the numbers off with no scroll and no sign (SW-UI-2,
   2026-11-07). `anywhere` shrinks min-content, so the table fits its card again;
   the day column never breaks at its hyphen (`2026-` / `11-06`). */
.table-days th, .table-days td { overflow-wrap: anywhere; }
/* the same name as a card's heading on the pass line and the walk */
.cell-name { overflow-wrap: anywhere; }
.table-days th[scope='row'] { white-space: nowrap; }
}
@layer base {
th, td { text-align: left; padding: var(--space-2); border-bottom: 1px solid var(--line); }
}
@layer components {
/* the CSV guide in Settings: the example block must never wrap a column,
   so it scrolls sideways on a phone instead of folding the sample; a long
   column name (passed_term_to_definition) breaks anywhere. */
.csv-guide table { margin: var(--space-2) 0; }
.csv-guide th, .csv-guide td { padding: var(--space-1) var(--space-2) var(--space-1) 0; }
.csv-guide td:first-child { overflow-wrap: anywhere; }
.csv-sample {
    margin: var(--space-2) 0; padding: var(--space-2) var(--space-3); overflow-x: auto;
    border: 1px dashed var(--line); border-radius: var(--radius-md);
    font-family: var(--font-mono);
    font-size: var(--fs-sm); line-height: var(--lh-body); color: var(--muted);
}
/* A list of things with their own actions (Settings: tags): one row per
   item, a hairline between rows, the actions at the right edge and the rows
   flush with the card's padding — the table cells inset them (2026-10-27).
   A long name with no spaces wraps anywhere. */
.item-list { list-style: none; margin: 0; padding: 0; }
.item-list > li { display: flex; align-items: center; gap: var(--space-2); padding: var(--space-2) 0; }
.item-list > li:first-child { padding-top: 0; }
.item-list > li:last-child { padding-bottom: 0; }
/* a built-in tag stays on its hint's first line when the hint wraps */
.item-list > li > .chip { align-self: flex-start; }
.item-list form { margin: 0; }
.item-main { flex: 1; min-width: 0; overflow-wrap: anywhere; }
}
@layer base {
hr { border: 0; border-top: 1px solid var(--line); margin: var(--space-4) 0; }

}
@layer components {
/* ---------- sound button (one component, three sizes) ----------
   The icon is always ~42% of the circle, so every size reads the same:
     .sound.sm     40px — inline next to a sentence or a list row
                           (example sentences, lesson dialogue, word rows)
     .sound        --control-h-lg (52px) — a word's own audio in a focused view
                           (deck card, a training prompt)
     .sound-md     the field height — beside Add word's 46px field
   `padding: 0` matters: the base button padding used to squash the icon. */
.sound {
    --sound-size: var(--control-h-lg);
    width: var(--sound-size); height: var(--sound-size);
    border-radius: 50%; flex-shrink: 0; padding: 0;
    display: inline-flex; align-items: center; justify-content: center;
    border: 0; cursor: pointer; box-shadow: none; min-height: 0;
    font-size: calc(var(--sound-size) * .42);
    background: var(--accent-soft); color: var(--accent-strong);
    transition: transform var(--dur-press) var(--ease-out);
}
.sound:active { transform: scale(.9); }
.sound.sm { --sound-size: 40px; }
.sound.sound-md { --sound-size: var(--control-h); }
.sound svg.lucide { width: 1em; height: 1em; }

/* The tutor's card opens with what it is (its title was the only visible one, the audit 2026-11-04). */
.tutor-intro h2 { margin-bottom: var(--space-1); }
.tutor-intro p { margin: 0; }

/* ---------- the consent gate ---------- */
/* The Meta pixel's card (his call, 2026-11-07): the toast's family — soft, raised,
   one row on a desktop and wrapped at 360 px — under the public bar, never a
   modal. It is not `pointer-events: none` like the toast: it takes the two taps
   it asks for. The toast sits above it (z 60), so a rare message over the card
   still reads. */
.consent {
    position: fixed; left: 50%; z-index: 55;
    top: calc(env(safe-area-inset-top) + var(--bar-h) + var(--space-2));
    transform: translateX(-50%);
    display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: var(--space-2) var(--space-3);
    width: min(528px, calc(100vw - var(--space-6)));
    background: var(--card); color: var(--text); border: 1px solid var(--line);
    padding: var(--space-2) var(--space-2) var(--space-2) var(--space-3);
    border-radius: var(--radius-md); box-shadow: var(--shadow);
}
.consent-text { flex: 1 1 14rem; min-width: 0; margin: 0; font-size: var(--fs-sm); font-weight: var(--fw-regular); line-height: var(--lh-body); }
.consent .row-actions { flex: 0 1 auto; }

/* ---------- toast ---------- */
/* What glides in glides out (U9 §2.5): the exit owns the base rule — out, on the
   exit's curve — and the shown state carries the enter, so no property is animated
   blind by `transition: all`. */
.toast {
    position: fixed; left: 50%; z-index: 60;
    top: calc(env(safe-area-inset-top) + var(--bar-h) + var(--space-2));
    transform: translate(-50%, -20px); opacity: 0; pointer-events: none;
    display: flex; align-items: center; gap: var(--space-3);
    width: min(528px, calc(100vw - var(--space-6)));
    background: var(--card); color: var(--text); border: 1px solid var(--line);
    padding: var(--space-2) var(--space-2) var(--space-2) var(--space-3); border-radius: var(--radius-md); font-weight: var(--fw-bold);
    box-shadow: var(--shadow); transition: transform var(--dur-out) var(--ease-in), opacity var(--dur-out) var(--ease-in);
}
/* Phones: the bar is a little shorter (it shrank 2026-10-16). */
@media (max-width: 899px) {
    :root { --bar-h: 48px; }
}
/* The toast's icon: round and tinted, like a finish's tiles; the accent, or red for an error. */
.toast-ico {
    display: grid; place-items: center; flex-shrink: 0; width: var(--control-h-sm); height: var(--control-h-sm);
    border-radius: var(--radius-pill); background: var(--accent-soft); color: var(--accent);
}
.toast-msg { flex: 1; white-space: normal; overflow-wrap: anywhere; min-width: 0; }
/* Shown, it still lets the page through: it sits over the top of a phone screen,
   and a toast that waits for a hand (an action) would otherwise hold a control
   under it forever (2026-11-05: the review's Undo, unclickable). Only its own
   button takes a tap; a focus on it still holds the countdown. */
.toast.show { transform: translate(-50%, 0); opacity: 1; transition: transform var(--dur-in) var(--ease-out), opacity var(--dur-in) var(--ease-out); }
.toast button { pointer-events: auto; }
/* An error (every error is a toast): red in its icon and its edge, never grey, never a
   solid red block (his call, 2026-11-04: too harsh). */
.toast.error { border-color: var(--bad); }
.toast.error .toast-ico { background: var(--bad-soft); color: var(--bad-text); }
.toast button {
    background: none; border: 0; color: var(--accent); font-weight: var(--fw-black); cursor: pointer;
    padding: var(--space-2); white-space: nowrap; flex-shrink: 0;
    box-shadow: none; min-height: 0; font-size: var(--fs-md);
}
/* Inside an open sheet the toast is the dialog's last child (ui/Toast.svelte
   moves it there): the modal's top layer already lifts it over the scrim, its
   card paints before it, so the z-index above only matters in the page.
   There it also takes its own taps: a press on the message would otherwise reach
   the backdrop and close the sheet, throwing away the tags it was warning about. */
.toast.in-sheet { pointer-events: auto; }

}
@layer base {
/* ---------- dialogs: the confirmation, the bulk tags, the milestone ---------- */
dialog {
    border: 0; padding: 0; background: transparent; color: inherit;
    width: min(560px, calc(100vw - var(--space-6))); max-height: calc(100dvh - 4rem);
}
dialog::backdrop { background: var(--scrim); backdrop-filter: blur(4px); }
}
@layer components {
dialog .card { margin: 0; }
/* Every dialog enters like the milestone (U9 §3, 2026-11-08): the scrim fades in and
   the card pops. The five open through Sheet.svelte, each with its card first inside. */
dialog[open]::backdrop { animation: ws-fade var(--dur-in) var(--ease-out); }
dialog[open] > .card { animation: ws-pop var(--dur-react) var(--ease-spring); }
/* the milestone (Milestone.svelte): a finish (.done), + confetti */
.milestone { width: min(380px, calc(100vw - var(--space-7))); }
.milestone-num { font-size: var(--fs-display); font-weight: var(--fw-black); color: var(--accent-strong); line-height: var(--lh-none); }
@keyframes ws-pop { from { transform: scale(.6); opacity: 0; } }
@keyframes ws-fade { from { opacity: 0; } }
.confetti {
    position: fixed; inset: 0; width: 100vw; height: 100vh; margin: 0; padding: 0;
    border: 0; background: transparent; pointer-events: none;
}

/* ---------- empty and loading ----------
   A card with nothing to show yet: the spinner while it loads, then a muted
   line and, where there is one, the action that fills it (full width). */
.empty { text-align: center; color: var(--muted); padding-block: var(--space-7); }
.empty > p { margin: 0; }
.empty > * + * { margin-top: var(--space-4); }

/* ---------- a finish (.done) ----------
   The end of something: a deck round, the whole deck, Review, a lesson, a
   training, a milestone. The big icon, the title, the text, then full-width
   actions. */
.done { text-align: center; }
.card.done { padding-block: var(--space-6); }
.done .big { font-size: var(--fs-icon); line-height: var(--lh-none); margin-bottom: var(--space-3); }
/* The mascot (Mascot.svelte): a hop over a finish, sleepy on an empty screen, waving over
   sign-up, on the couch under the landing's TV; one size per place, never stretched. */
.mascot { display: block; width: 7rem; height: 7rem; object-fit: contain; margin: 0 auto var(--space-3); filter: var(--mascot-rim); }
/* Unseen till his picture has loaded, then a quick fade (his note, 2026-11-04: half
   drawn for a split second looked broken). */
.mascot:not(.is-loaded) { opacity: 0; }
.mascot.is-loaded { transition: opacity var(--dur-fade) var(--ease-out); }
/* Peeking: on the top corner of the block it belongs to, out of the layout, so
   showing it moves nothing (Discover's deck till it is touched); the block it
   sits on is a .peek-host. */
.peek-host { position: relative; }
.mascot-peek { position: absolute; z-index: 2; top: -4rem; right: var(--space-2); width: 5rem; height: auto; margin: 0; pointer-events: none; animation: ws-pop var(--dur-react) var(--ease-spring); }
/* in Discover's top card, beside its rank: the label keeps clear of him */
.meta .mascot-peek { top: calc(-1 * var(--space-2)); right: 0; width: 3.75rem; }
.meta.has-peek { padding-right: calc(var(--space-7) + var(--space-3)); }
.empty .mascot { width: 5.5rem; height: 5.5rem; margin-bottom: 0; }
/* A screen on its way (Loading.svelte): his face bobs where the spinner was. */
.loading .mascot { animation: ws-bob 1.2s ease-in-out infinite; }
@keyframes ws-bob { 50% { transform: translateY(-.5rem); } }
.auth-main > .mascot, .keep-sheet .mascot { width: 6rem; height: 6rem; margin: 0; animation: ws-pop var(--dur-react) var(--ease-spring); }
/* Snappy says it (Mascot.svelte with children): a speech bubble beside him, its tail
   at him, the words the page's own (an empty screen's hint, a milestone's line). */
.snappy-says { display: flex; align-items: center; justify-content: center; gap: var(--space-3); margin: 0 auto var(--space-3); }
.snappy-says .mascot { margin: 0; flex-shrink: 0; }
.says {
    position: relative; margin: 0; max-width: 16rem; min-width: 0; padding: var(--space-2) var(--space-3);
    background: var(--accent-soft); color: var(--text); border-radius: var(--radius-md);
    font-weight: var(--fw-bold); line-height: var(--lh-tight); text-align: left;
    animation: ws-pop var(--dur-react) var(--ease-spring) var(--dur-press) backwards;
}
.says::before { content: ''; position: absolute; top: 50%; left: -.35rem; width: .75rem; height: .75rem; background: inherit; transform: translateY(-50%) rotate(45deg); }
.big svg.lucide { width: 1em; height: 1em; stroke-width: 1.9; }

/* ---------- training helpers ---------- */
/* The prompt: one rung for "what you answer" (U1 §6 pick 4 — the quiz's 24/800
   won). The box carries no size of its own: the prompt inside sets it, so
   Listening's definition and a quiz's question are the same thing to read. */
.question { text-align: center; padding: var(--space-4); }
.letter-tile, .letter-slot, .letter-empty {
    display: inline-flex; align-items: center; justify-content: center; vertical-align: top;
    width: 2.5rem; height: 2.5rem; margin: var(--space-1);
    font-family: var(--font-mono); font-size: var(--fs-2xl); font-weight: var(--fw-regular);
}
.letter-tile { border: var(--control-border) solid var(--accent); border-radius: var(--radius-md); cursor: pointer; }
.letter-slot { border-bottom: var(--control-border) solid var(--accent); cursor: pointer; }
.letter-slot.is-empty { border-bottom-style: dashed; border-bottom-color: var(--line); cursor: default; }
.letter-empty { border: var(--control-border) dashed var(--line); border-radius: var(--radius-md); }
/* A letter and a slot are real <button>s (his gate, 2026-11-06): the button's
   own skin comes off, so the tile keeps its one look. */
.letter-tile, .letter-slot {
    background: none; color: inherit; padding: 0; min-height: 0; box-shadow: none;
    font-weight: var(--fw-regular);
}

/* ---------- the deck (Discover, Review) ---------- */
/* progress hero: the ring and its line */
.ring { position: relative; width: 88px; height: 88px; flex-shrink: 0; }
.ring svg, .mini-ring svg { width: 100%; height: 100%; transform: rotate(-90deg); }
.ring .track { stroke: var(--accent-soft); }
.ring .bar { stroke: url(#grad); stroke-linecap: round; transition: stroke-dashoffset var(--dur-fill) var(--ease-bounce); }
.ring .inner { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; }
.ring .count { font-size: var(--fs-2xl); font-weight: var(--fw-black); line-height: var(--lh-none); }
.ring .of { font-size: var(--fs-xs); color: var(--muted); font-weight: var(--fw-bold); }
.hero-text { flex: 1; min-width: 0; }
.hero-text .goal { font-weight: var(--fw-black); line-height: var(--lh-tight); }
/* On Review the goal line IS the page's h1 (walk finding 5,
   2026-10-16); on Discover the same class is a caption under the score ring
   and keeps its small size. */
.hero-section .goal { font-size: var(--fs-md); }
.stats { display: flex; gap: var(--space-2); margin-top: var(--space-2); flex-wrap: wrap; }
.coverage {
    display: flex; align-items: center; gap: var(--space-2);
    font-weight: var(--fw-bold); font-size: var(--fs-sm); line-height: var(--lh-tight);
}
.coverage .ico { font-size: var(--fs-xl); line-height: var(--lh-none); color: var(--accent-strong); }
.coverage .pct { color: var(--accent-strong); font-weight: var(--fw-black); }
/* The cards share one grid cell, so the deck is as tall as its tallest card:
   a card shows all of itself, never scrolls inside (a 360 × 800 phone cut the
   example off, 2026-10-31). At least the space left between the progress area
   and the action buttons. */
.deck {
    display: grid; position: relative; margin-bottom: var(--space-6); touch-action: pan-y;
    /* Stable viewport units: 100dvh changes as mobile browser chrome hides and
        shows while scrolling, resizing the deck under the user's thumb. svh is
        the same at both scroll positions, so the card content stays put. The
        534px reserve leaves room for the two cards peeking under the deck, the
        round counter and the action row above the fixed bottom nav (2026-10-20). */
    min-height: clamp(292px, calc(100vh - 534px - env(safe-area-inset-top) - env(safe-area-inset-bottom)), 430px);
    min-height: clamp(292px, calc(100svh - 534px - env(safe-area-inset-top) - env(safe-area-inset-bottom)), 430px);
}
/* The yesterday chip adds a second stats row on a phone; give the card back
   that height so the buttons never sit under the bottom nav (2026-10-21). */
body.has-yesterday .deck {
    min-height: clamp(280px, calc(100vh - 560px - env(safe-area-inset-top) - env(safe-area-inset-bottom)), 430px);
    min-height: clamp(280px, calc(100svh - 560px - env(safe-area-inset-top) - env(safe-area-inset-bottom)), 430px);
}
.flash {
    position: relative; grid-area: 1 / 1;
    background: var(--card); border-radius: var(--radius-lg); box-shadow: var(--shadow);
    padding: var(--card-pad); display: flex; flex-direction: column;

    touch-action: pan-y; user-select: none; -webkit-user-select: none;
    -webkit-user-drag: none;
    transition: transform var(--dur-swipe) var(--ease-out), opacity var(--dur-swipe) var(--ease-out);
    will-change: transform;
}
/* The stack. The cards waiting behind the one in play carry their own content,
   so a swipe already shows what comes next. They stay non-interactive and
   unscrollable: only the top card moves and takes input. */
.flash.behind { transform: translateY(16px) scale(.955, .97); opacity: 1; pointer-events: none; }
.flash.behind-2 { transform: translateY(32px) scale(.91, .94); opacity: 1; pointer-events: none; }
/* The waiting cards are previews: no compositing layer of their own. Their
   content shows once the top card moves: at rest, the strip of a waiting card
   under the top one showed a cut-off line of its text (2026-10-30). */
.flash.preview { overflow: hidden; will-change: auto; }
.flash.preview > * { visibility: hidden; }
.deck:has(.flash.dragging) .flash.preview > * { visibility: visible; }
.flash.dragging { transition: none; }
/* A thrown card leaves on the exit's easing (U9 §2.7: the app's only exit without one) */
.flash.out-right { transform: translate(130%, -40px) rotate(25deg); opacity: 0; transition: transform var(--dur-swipe) var(--ease-in), opacity var(--dur-swipe) var(--ease-in); }
.flash.out-left { transform: translate(-130%, -20px) rotate(-20deg); opacity: 0; transition: transform var(--dur-swipe) var(--ease-in), opacity var(--dur-swipe) var(--ease-in); }
.flash .meta { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); margin-bottom: var(--space-2); flex-shrink: 0; }
/* Never let the word block shrink under its content (2026-10-21): with a short
   card and a two-line example, centred overflow rode up over the rank badge. */
.flash .word-row { flex: 1 0 auto; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; gap: var(--space-2); min-height: 84px; padding: var(--space-1) 0; }
.flash .word { font-size: var(--fs-display); font-weight: var(--fw-black); letter-spacing: var(--tracking-tight); word-break: break-word; line-height: var(--lh-tight); }
.flash .forms, .flash .def { color: var(--muted); font-weight: var(--fw-regular); font-size: var(--fs-md); }
/* The meaning shows straight away (2026-10-16): the deck is a "do you know it"
   pass, so the card answers itself. No rules and no inset surface (2026-10-20/21):
   the dashed line over the meaning and the solid line over the example were two
   competing dividers; spacing alone does that job now. */
.flash .answer {
    margin-top: auto; padding-top: var(--space-3);
    display: flex; flex-direction: column; align-items: center;
    text-align: center; flex-shrink: 0;
}
/* One line, one style: the main gloss and its alternatives are the same kind
   of information (2026-10-20). */
.flash .answer .es { font-size: var(--fs-2xl); font-weight: var(--fw-black); color: var(--accent-strong); line-height: var(--lh-tight); }
/* The two gestures, while the card is still new: each one sits over the button
   below that does the same thing (left = to the dictionary, right = I know it),
   so the direction of the swipe is the direction of the buttons. */
.flash .cues {
    display: flex; justify-content: space-between; gap: var(--space-2); flex-shrink: 0;
    margin: 0 0 var(--space-3); font-size: var(--fs-sm); font-weight: var(--fw-bold); color: var(--muted);
}
.flash .cues .know { color: var(--accent-strong); }
.flash .cues .cue { white-space: nowrap; }
/* A line with its sound (the deck's example, a lesson's grammar example and
   dialogue): plain content, separated by spacing alone. */
.example {
    display: flex; align-items: center; gap: var(--space-2); width: 100%;
    text-align: left;
}
.flash .example { margin-top: var(--space-3); }
.example .en { font-weight: var(--fw-bold); }
.example .tr { color: var(--muted); font-size: var(--fs-sm); }
/* the word's definition (Review): under the word and its sound, centred */
.flash .word-row > .def { max-width: 32rem; }
.flash .similar { color: var(--muted); font-weight: var(--fw-regular); font-size: var(--fs-sm); text-align: center; margin-top: var(--space-2); }
.stamp {
    position: absolute; top: var(--space-6); padding: var(--space-1) var(--space-3); border-radius: var(--radius-md);
    font-weight: var(--fw-black); font-size: var(--fs-xl); opacity: 0; pointer-events: none; border: var(--control-border) solid;
}
.stamp.know { right: var(--space-5); color: var(--fire-2); border-color: var(--fire-2); transform: rotate(12deg); }
.stamp.later { left: var(--space-5); color: var(--accent-strong); border-color: var(--accent-strong); transform: rotate(-12deg); }
.actions { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-3); }
/* three verdicts (Review): the swipe's two ends at the sides, Delete between */
.actions--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
/* One label size across the card: these were 15.2px while every other action
   was 16, and nowrap clipped the longest (2026-10-19). */
.actions .btn { padding-inline: var(--space-2); font-size: var(--fs-md); white-space: normal; }
/* A 360 px phone: each verdict on one line, in every language (2026-10-31).
   Delete keeps only its bin (its name stays for screen readers), I know it
   drops its tick, and the two ends share what is left. The tutor's Listen |
   Slow | Next drop their icons the same way (2026-11-01). */
@media (max-width: 400px) {
    .actions--3 { grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); }
    .actions--3 .btn-danger { padding-inline: var(--space-3); }
    /* The fields whose placeholders were cut at 360 px in Russian (his call,
       2026-11-05): the input takes the row, its button below it, label and all. */
    .join--stack { flex-direction: column; }
    .join--stack input { border-radius: var(--radius-md); border-right-width: var(--control-border); }
    .join--stack button { border-radius: var(--radius-md); }
    .join--stack .btn-label { position: static; width: auto; height: auto; clip: auto; white-space: normal; }
    .wide-only {
        position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
        overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
    }
}
/* The round counter sits between the card and the buttons (2026-10-20), so the
   action row has breathing room instead of touching the card's bottom edge. */
.swipe-hint { text-align: center; color: var(--muted); font-size: var(--fs-xs); font-weight: var(--fw-regular); margin: 0 0 var(--space-2); }
.meter + .swipe-hint { margin-top: var(--space-2); } /* the round's blocks over it */
/* Swipe/tap grammar belongs to a thumb. A mouse sees the buttons and the
   keyboard shortcuts; the round counter stays (it is not a gesture). */
@media (hover: hover) and (pointer: fine) {
    .gesture-hint, .flash .cues { display: none; }
}
.practice-cta { display: flex; align-items: center; gap: var(--space-3); text-decoration: none; color: inherit; }
.practice-cta .ico { font-size: var(--fs-3xl); }
/* A lesson's row: its icon in the finish tiles' round tint (his pick, 2026-11-04). */
.practice-cta .tile-ico { font-size: var(--fs-md); }
.practice-cta .go { margin-left: auto; font-size: var(--fs-2xl); color: var(--accent-strong); }
/* the swipe hint: the deck card nudges left and right now and then (and the
   landing's Add word try-it, until it is touched) */
.hint-nudge { animation: hint-nudge var(--dur-flourish) ease-in-out; }
@keyframes hint-nudge {
    20% { transform: translateX(-10px) rotate(-1.2deg); }
    50% { transform: translateX(10px) rotate(1.2deg); }
    80% { transform: translateX(-4px) rotate(-.6deg); }
}
.fire-burst { position: fixed; pointer-events: none; z-index: 70; font-size: var(--fs-3xl); animation: burst var(--dur-flourish) ease-out forwards; }
@keyframes burst { to { transform: translate(var(--dx), var(--dy)) scale(.4); opacity: 0; } }

/* ---------- Discover hub: one card per page, each with a mini chart ---------- */
/* The hero: today's score, one prominent section on the accent-tinted
   surface, with the week as the same chart component. The deck's coverage and
   counts are a card of their own below it (his call, 2026-10-31). */
.hero-section {
    padding: var(--card-pad); border-radius: var(--radius-lg); margin-bottom: var(--gap);
    background: linear-gradient(160deg, var(--card) 55%, var(--accent-soft));
    border: 1px solid var(--line); box-shadow: var(--shadow);
}
/* ring on the left, chart on the right */
.score-top { display: flex; align-items: center; gap: var(--space-4); }
.score-ring { display: flex; flex-direction: column; align-items: center; gap: var(--space-2); flex-shrink: 0; }
.score-ring .ring { width: 96px; height: 96px; }
.score-bars { margin-top: var(--space-2); }
.score-bars .mini-bars i { background: var(--accent-soft); }
.score-bars .mini-bars i b { background: var(--accent); }
.score-bars .mini-bars i:last-child b { background: linear-gradient(180deg, var(--accent), var(--accent-strong)); }
/* the deck card's coverage line: the statement, then the one-line explainer */
.coverage-how { margin: var(--space-1) 0 0; font-size: var(--fs-xs); line-height: var(--lh-tight); }
/* One hub card per page: a heading row across the card (icon · title + line ·
   arrow) and the chart below it, full width — the chart used to be squeezed
   between the icon and the arrow. */
/* A card that is a link keeps the card's own type colours — the accent and
   the underline belong to links in prose, not to a whole card (the hub cards'
   titles were accent-coloured and underlined, 2026-10-16). */
a.card { text-decoration: none; color: inherit; }
.hub-card { display: flex; flex-direction: column; align-items: stretch; gap: var(--space-2); }
.hub-head { display: flex; align-items: center; gap: var(--space-3); width: 100%; }
.hub-head .ico { font-size: var(--fs-2xl); line-height: var(--lh-tight); flex-shrink: 0; }
.hub-titles { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.hub-titles > .muted { font-size: var(--fs-sm); }
.hub-head .go { margin-left: auto; font-size: var(--fs-2xl); color: var(--accent-strong); flex-shrink: 0; }
/* Pastel hub cards (his pick from his references, 2026-11-04): each page's card
   on its own tint, the icon in a white circle, a small ↗ in the corner; the
   charts' empty parts on --hub-track. */
.hub-card.hub-sky { background: var(--sky-soft); }
.hub-card.hub-gold { background: var(--gold-soft); }
.hub-card.hub-mint { background: var(--good-soft); }
.hub-card.hub-fire { background: var(--warn-soft); }
.hub-card.hub-lavender { background: var(--accent-soft); }
.hub-card.hub-sky, .hub-card.hub-gold, .hub-card.hub-mint, .hub-card.hub-fire, .hub-card.hub-lavender { border-color: transparent; }
/* A round icon: the hub cards' white circle, and the same geometry for a lesson
   row's and a finish's tile (`.tile-ico`, coloured by `.tint-*`). */
.hub-ico, .tile-ico {
    display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; width: 2.75rem; height: 2.75rem;
    border-radius: var(--radius-pill);
}
.hub-ico svg.lucide, .tile-ico svg.lucide { width: 1.4rem; height: 1.4rem; }
.hub-ico { background: var(--card); color: var(--accent-strong); }
/* Continue: a white card, so its icon sits on the lavender tint */
.hub-continue .hub-ico { background: var(--accent-soft); }
.hub-sky .hub-ico { color: var(--sky-text); }
.hub-gold .hub-ico { color: var(--gold-text); }
.hub-mint .hub-ico { color: var(--good-text); }
.hub-fire .hub-ico { color: var(--fire-2); }
.hub-head .go-corner { align-self: flex-start; margin-left: auto; flex-shrink: 0; color: var(--accent-strong); }
.hub-head .go-corner svg.lucide { width: 1.1rem; height: 1.1rem; }
.hub-card .mini-row .track, .hub-card .mini-bars i, .hub-card .mini-stack, .hub-card .dot-line { background: var(--hub-track); }
.hub-card .mini-ring circle:first-child { stroke: var(--hub-track); }
.hub-chart { display: block; width: 100%; margin: 0; }
.hub-chart-row { display: flex; align-items: center; gap: var(--space-3); }
.hub-cheer strong { color: var(--accent-strong); font-weight: var(--fw-black); }
.legend { display: flex; flex-direction: column; gap: var(--space-1); font-size: var(--fs-xs); font-weight: var(--fw-regular); color: var(--muted); min-width: 0; }
.legend i { display: inline-block; width: .6rem; height: .6rem; border-radius: var(--radius-pill); margin-right: var(--space-1); }
/* vertical bars (words added per day) */
.mini-bars { position: relative; display: flex; align-items: stretch; gap: var(--space-1); height: 30px; }
/* the score card's bars and the hub's stand taller than a row's */
.score-bars .mini-bars, .hub-card .mini-bars { height: 34px; }
.mini-bars i { position: relative; flex: 1; min-width: 0; border-radius: var(--radius-md); background: var(--line); overflow: hidden; }
.mini-bars i b {
    position: absolute; left: 0; right: 0; bottom: 0;
    height: calc(var(--v, 0) * 100%); border-radius: var(--radius-md);
    background: linear-gradient(180deg, var(--accent), var(--accent-strong));
}
/* labelled bar rows (training modes) */
.mini-row { display: flex; align-items: center; gap: var(--space-2); font-size: var(--fs-xs); font-weight: var(--fw-regular); }
.mini-row + .mini-row { margin-top: var(--space-1); }
.mini-row .lbl { flex: 0 0 8rem; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mini-row .track { flex: 1; height: 8px; border-radius: var(--radius-pill); background: var(--line); overflow: hidden; }
.mini-row .fill { display: block; height: 100%; border-radius: var(--radius-pill); background: var(--accent); }
.mini-row .val { flex: 0 0 1.6rem; text-align: right; }
/* the same row, large: the landing's chart */
.mini-row-lg { gap: var(--space-3); font-size: var(--fs-md); font-weight: var(--fw-bold); }
.mini-row-lg + .mini-row-lg { margin-top: 0; }
.mini-row-lg .lbl { flex-basis: 9rem; color: var(--text); }
.mini-row-lg .track { height: 14px; }
.mini-row-lg .val { flex-basis: 3rem; font-weight: var(--fw-black); color: var(--accent-strong); }
/* on a phone, the bar on a line of its own: the row's width is the bar's */
@media (max-width: 600px) {
    .mini-row-lg { flex-wrap: wrap; }
    .mini-row-lg .lbl { flex: 1; }
    .mini-row-lg .track { order: 1; flex-basis: 100%; }
}
/* horizontal stacked bar (parts of the dictionary) */
.mini-stack { display: flex; height: 12px; border-radius: var(--radius-pill); overflow: hidden; background: var(--line); }
.mini-stack i { display: block; height: 100%; }
/* small donut (part of a whole) */
.mini-ring { position: relative; width: 56px; height: 56px; flex-shrink: 0; }

/* ---------- progress and rewards (the theme overhaul, 2026-11-04) ---------- */
/* One progress bar (Meter.svelte): a track and its fill, its value said in
   words beside it. Marks are stops on the way, labelled under the track; what
   rides the tip (Snappy) sits over it. Blocks: one per step of a session. */
.meter { position: relative; }
.meter-marked { padding-bottom: var(--space-5); }
.meter-ridden { padding-top: var(--space-7); }
.meter-track { position: relative; display: block; height: .75rem; border-radius: var(--radius-pill); background: var(--accent-track); }
.meter-fill {
    position: absolute; top: 0; bottom: 0; left: 0; border-radius: var(--radius-pill);
    background: var(--accent-fill); transition: width var(--dur-fill) var(--ease-out);
}
.meter-mark { position: absolute; top: 0; bottom: 0; width: 2px; transform: translateX(-50%); background: var(--card); }
.meter-mark-label {
    position: absolute; top: 100%; left: 50%; transform: translateX(-50%); margin-top: var(--space-1);
    font-size: var(--fs-xs); font-weight: var(--fw-bold); line-height: var(--lh-tight); color: var(--muted); white-space: nowrap;
}
.meter-mark.passed .meter-mark-label { color: var(--accent-strong); }
.meter-tip { position: absolute; bottom: 100%; transform: translateX(-50%); transition: left var(--dur-fill) var(--ease-out); }
.meter-tip .mascot { width: 3rem; height: 3rem; margin: 0; }
.meter-blocks { display: flex; gap: var(--space-1); background: none; }
.meter-block { flex: 1; border-radius: var(--radius-pill); background: var(--accent-track); transition: background-color var(--dur-react) var(--ease-out); }
.meter-block.filled { background: var(--accent); }
/* a session's blocks beside its count (Training's 4/12, a lesson's steps, the deck's round) */

/* The numeric rung (U1 §4, §5): a figure read as a figure — score, tiles, streak,
   level, calendar, percentage, a plan's counts, a deck's totals — in tabular
   figures, so a digit changing never re-measures the number beside it. No size
   of its own: it inherits its rung. ONE rule (the 2026-11-06 type pass): `.num`
   is what a figure in prose carries; the selectors cover the places a figure
   rides a class that only ever holds one. */
.ring .count, .ring .of, .tile-value, .streak-stat strong, .streak-stat .muted,
.milestone-num, .landing-big-num, .cal-day, .coverage .pct,
.mini-row .val, .mini-row-lg .val, .pill, .mode-count, .hub-cheer strong,
.level-row strong, .num { font-variant-numeric: tabular-nums; }

/* a reward in a pill: the streak's flame, the level's trophy */
.pills { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-bottom: var(--space-2); }
.pill {
    display: inline-flex; align-items: center; gap: var(--space-1); padding: var(--space-1) var(--space-3);
    border-radius: var(--radius-pill); font-size: var(--fs-sm); font-weight: var(--fw-black); line-height: var(--lh-tight); white-space: nowrap;
}
.pill svg.lucide { width: 1.1em; height: 1.1em; }
.pill-fire { background: var(--warn-soft); color: var(--warn-text); }
.pill-fire svg.lucide { color: var(--fire-2); }
.pill-gold { background: var(--gold-soft); color: var(--gold-text); }

/* The streak card (Streak.svelte): the days in a row and the record side by
   side, the level and its bar, this month's days with points. */
.ico-fire { color: var(--fire-2); }
.streak-stats { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-2); }
.streak-stat { display: flex; flex-direction: column; padding: var(--space-2) var(--space-3); border-radius: var(--radius-md); background: var(--bg); }
.streak-stat strong { font-size: var(--fs-2xl); font-weight: var(--fw-black); line-height: var(--lh-tight); }
.streak-stat .muted { font-size: var(--fs-xs); font-weight: var(--fw-bold); }
/* the rescues held, or how one is earned: a line under the numbers */
.streak-rescues { display: flex; align-items: center; gap: var(--space-2); margin: 0; color: var(--muted); font-size: var(--fs-xs); font-weight: var(--fw-bold); }
.streak-rescues svg.lucide { width: 1rem; height: 1rem; flex-shrink: 0; color: var(--accent-strong); }
.level-row { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--space-2); margin-top: var(--space-1); }
.level-row > .muted { font-size: var(--fs-xs); font-weight: var(--fw-bold); }
.cal { display: flex; flex-direction: column; gap: var(--space-2); margin-top: var(--space-2); }
.cal-month { font-size: var(--fs-sm); font-weight: var(--fw-black); text-transform: capitalize; }
.cal-grid { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: var(--space-1); text-align: center; }
.cal-wd { font-size: var(--fs-xs); font-weight: var(--fw-bold); color: var(--muted); }
.cal-day {
    display: flex; align-items: center; justify-content: center; height: 2rem; border-radius: var(--radius-md);
    background: var(--bg); color: var(--muted); font-size: var(--fs-xs); font-weight: var(--fw-bold);
}
.cal-day.future { background: none; }
/* a missed day a rescue bridged: the run's tint, not its fill */
.cal-day.rescued { background: var(--accent-soft); color: var(--accent-strong); }
.cal-day.done { background: var(--accent-fill); color: var(--on-accent); }
.cal-day.today { box-shadow: inset 0 0 0 var(--control-border) var(--accent); color: var(--text); }
.cal-day.today.done { color: var(--on-accent); box-shadow: inset 0 0 0 var(--control-border) var(--accent-edge); }

/* A finish's numbers (Tiles.svelte): two to a row, an odd last one across;
   each a round tinted icon, its label and its value. */
.tiles {
    list-style: none; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-2);
    margin: var(--space-4) 0; padding: 0; text-align: left;
}
.tile { display: flex; align-items: center; gap: var(--space-3); min-width: 0; padding: var(--space-3); border-radius: var(--radius-md); background: var(--bg); }
.tile:last-child:nth-child(odd) { grid-column: 1 / -1; }
/* the tiles' icons are `.tile-ico`, drawn with `.hub-ico` (above); a `.tint-*` colours them */
.tile-text { display: flex; flex-direction: column; min-width: 0; }
.tile-label { font-size: var(--fs-xs); font-weight: var(--fw-bold); line-height: var(--lh-tight); color: var(--muted); }
.tile-value { font-size: var(--fs-xl); font-weight: var(--fw-black); line-height: var(--lh-tight); }
.tint-sky { background: var(--sky-soft); color: var(--sky-text); }
.tint-gold { background: var(--gold-soft); color: var(--gold-text); }
.tint-mint { background: var(--good-soft); color: var(--good-text); }
.tint-fire { background: var(--warn-soft); color: var(--fire-2); }
.tint-lavender { background: var(--accent-soft); color: var(--accent-strong); }

/* ---------- the word card (one row component everywhere) ----------
   The same block appears in the dictionary list, under "Similar words" in Add
   word, as the result list after a training session, and as a lesson's words
   to learn. It is a flat row (a hairline border, no drop shadow) so it can sit
   inside a card without nesting cards. Status tints: ok / bad for a trained
   word. */
.wcard {
    background: var(--card); border: 1px solid var(--line);
    border-radius: var(--radius-md); padding: var(--space-3);
}
.wcard + .wcard { margin-top: var(--space-2); }
/* the head row: [the checkbox, in select mode] the word (+ its sound) over the
   definition and the tags, the ⋯ menu at the right; a long word wraps beside
   the sound, under itself (his call, 2026-11-01) */
.wcard-head { display: flex; align-items: flex-start; gap: var(--space-2); }
.wcard .wcard-title { flex-wrap: nowrap; align-items: flex-start; }
.wcard-head > input[type="checkbox"] { margin: var(--space-3) 0 0; }
.wcard-main { flex: 1; min-width: 0; }
.wcard-term { font-weight: var(--fw-black); font-size: var(--fs-lg); overflow-wrap: anywhere; }
.wcard-tags { display: flex; flex-wrap: wrap; gap: var(--space-1); margin-top: var(--space-2); }
/* A long tag stays on one line and is cut with an ellipsis, never wider than
   the card (a 36-letter tag wrapped onto two lines at 360 px, 2026-11-04). */
.wcard-tags .chip { max-width: 100%; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* A word saved to define later (the day's AI used up): the line and its ✨ side by side. */
.wcard-undefined { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); }
.wcard-note { margin: 0; color: var(--muted); }
/* The edit takes the card's place (no hairline, no second copy of the word;
   his call, 2026-11-01). The definition opens tall enough for all of it, and grows as it's typed
   (Chromium; elsewhere its 3 rows). */
.wcard-edit textarea { field-sizing: content; max-height: 60dvh; }
/* a verdict's tint, on a trained word's card and on a scramble's slot */
.wcard--ok, .scramble-slot.is-right { border-color: var(--good); background: var(--good-soft); }
.wcard--bad, .scramble-slot.is-wrong { border-color: var(--bad); background: var(--bad-soft); }

/* ---------- a lesson ---------- */
/* prev/next: a deliberate pair (full-width on a phone) instead of a lone
   button dropped at the page edge (2026-10-17) */
.lesson-actions { justify-content: center; }
@media (max-width: 899px) { .lesson-actions .btn { flex: 1; } }
.q { text-align: center; min-height: 150px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--space-2); }

/* ---------- answering: training and lesson exercises ----------
   One prompt, its options and the feedback after the answer; the same shapes
   on Quiz, Listening, Word scramble and a lesson's practice. */
/* the options: one column, two from 601px with every row as tall as the
   longest option, so a pair never sits on a ragged line */
.options { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--space-3); counter-reset: opt; }
@media (min-width: 601px) {
    .options { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); grid-auto-rows: 1fr; }
}
.opt {
    /* A choice, not a CTA: the base `button` rule would paint it with the accent
       gradient, white text and a glow — and `.opt` set no colour of its own, so
       the label sank into the card (white on white in light, 1.2:1 in dark).
       Options are sentences: long ones wrap instead of overflowing on phones. */
    min-height: var(--control-h-lg); border-radius: var(--radius-md); border: var(--control-border) solid var(--line);
    background: var(--card); color: var(--text); box-shadow: none;
    font-weight: var(--fw-bold); font-size: var(--fs-lg); cursor: pointer; white-space: normal;
    padding: var(--space-2) var(--space-4); transition: transform var(--dur-press) var(--ease-out);
    display: flex; align-items: center; justify-content: flex-start; gap: var(--space-3);
    text-align: left; counter-increment: opt;
}
.opt.right, .answer-input.is-right { background: var(--good-soft); border-color: var(--good); color: var(--good-text); }
/* Each option's letter in a small tinted circle (his pick from his references,
   2026-11-04), the label beside it; answered, the circle goes white on the
   option's green or red. Hover and the keyboard's focus outline it in indigo. */
.opt::before {
    content: counter(opt, upper-alpha); flex: 0 0 auto; display: inline-grid; place-items: center; width: 1.75rem; height: 1.75rem;
    border-radius: var(--radius-pill); background: var(--accent-soft); color: var(--accent-strong); font-size: var(--fs-sm); font-weight: var(--fw-black);
}
.opt.right::before { background: var(--card); color: var(--good-text); }
.opt.wrong::before { background: var(--card); color: var(--bad-text); }
.opt.wrong { background: var(--bad-soft); border-color: var(--bad); color: var(--bad-text); }
.opt[disabled] { cursor: default; }
/* "Not quite" (his ask, 2026-11-03): every wrong answer and every error toast takes
   one gentle sway, dying out, not a hard shake (±6px); then the right answer eases
   into green, after it. Reduced motion: neither (the end of the file). The stagger
   is a token, not a raw .15 s (U9 §2.2). */
@keyframes ws-not-quite { 20% { transform: translateX(-4px); } 45% { transform: translateX(3px); } 70% { transform: translateX(-1px); } }
@keyframes ws-found { from { background-color: var(--card); border-color: var(--line); color: var(--text); } }
.opt.wrong, .scramble-slot.is-wrong, .toast.error.show .toast-msg { animation: ws-not-quite var(--dur-react) ease-out; }
.opt.right, .answer-input.is-right, .scramble-slot.is-right { animation: ws-found var(--dur-react) ease-out var(--dur-press) backwards; }
.answer-input.is-missed { animation: ws-not-quite var(--dur-react) ease-out, ws-found var(--dur-react) ease-out var(--dur-press) backwards; }
.prompt-xl { font-size: var(--fs-2xl); font-weight: var(--fw-black); margin: var(--space-2) 0; }
/* the same rung as .prompt-xl: a definition to listen to, the tutor's sentence */
.prompt-md { font-size: var(--fs-2xl); font-weight: var(--fw-black); margin-top: var(--space-2); }
/* The session's frame (2026-10-26): the card is a column with the answer bar
   (AnswerBar.svelte) at its foot, so Check / Next sit in one place for the
   whole session and the feedback fills a line that was already there — the
   prompt, the options and the buttons never move when you answer. A phone's
   card fills the screen between the app bar and the bottom nav (svh: the
   browser chrome showing and hiding must not resize it), which puts the bar
   under the thumb; a longer prompt grows into the free space above the bar. */
.session-card { display: flex; flex-direction: column; margin-bottom: 0; min-height: min(40rem, calc(100dvh - 7rem)); }
/* A screen's one caption, small and muted: how far in (a training's 3/10),
   how many left (Review's 12 to review). The page's name is the navigation's
   and a hidden h1's (his call, 2026-10-31: no titles, no "Term:"). */
.caption { margin: 0 0 var(--space-3); font-size: var(--fs-sm); line-height: var(--lh-tight); color: var(--muted); font-weight: var(--fw-bold); }
/* A centred word (a swipe card: Review, Discover; a quiz's prompt) has the full
   width and its sound sits centred under it, so a wrapped word stays centred (his
   call, 2026-11-01). Elsewhere the sound sits left of its word. */
.word-line { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--space-2); }
.word-line > * { min-width: 0; max-width: 100%; }
@media (max-width: 899px) {
    /* the card fills the screen between the bars, minus .app-main's own padding:
       one token, --screen-h, for the six hand sums this replaces (U2 §2) */
    .session-card, .lesson-page { min-height: var(--screen-h); }
    /* A long prompt or a long word makes the card taller than the screen: the
       bar then sticks above the bottom nav instead of sliding under it, so
       Check / Next never leave the thumb's spot. It spans the card's padding
       and carries the card's surface, so content scrolls under it cleanly. */
    .session-card .answer-bar {
        position: sticky; bottom: calc(var(--nav-h) + env(safe-area-inset-bottom) + var(--space-4)); z-index: 1;
        margin: auto calc(-1 * var(--card-pad)) calc(-1 * var(--card-pad));
        padding: var(--space-4) var(--card-pad) var(--card-pad);
        background: var(--card); border-radius: 0 0 var(--radius-lg) var(--radius-lg);
    }
    /* The keyboard is up (src/lib/keyboard.ts; the page ends at its top): the
       bottom nav steps aside and the page's foot with it, so Check / Next sit
       right on the keyboard (his call, 2026-10-31). */
    html.keyboard-up .mainnav-bottom { display: none; }
    html.keyboard-up .app-main { padding-bottom: var(--space-4); }
    html.keyboard-up .session-card, html.keyboard-up .lesson-page { min-height: var(--screen-h-kb); }
    html.keyboard-up .session-card .answer-bar { bottom: var(--space-4); }
}
.answer-bar { margin-top: auto; padding-top: var(--space-4); }
.answer-feedback {
    min-height: 1.5em; margin: 0 0 var(--space-2); text-align: center;
    font-size: var(--fs-lg); font-weight: var(--fw-black);
}
.answer-actions > button { min-height: var(--control-h-lg); white-space: normal; }
.answer-actions > .is-gone { visibility: hidden; }
/* The typed answer, judged in place (listening): the box then holds the right
   word, so it turns green; a miss sways it first (his pick, 2026-11-03: the right
   word in a red box read as wrong). */
/* The box the assembled letters land in, and the pool they come from (word
   scramble). Both rows hold one tile per letter from the start — the slots are
   empty, the pool keeps a dashed gap where a letter left — so the same count
   wraps the same way and neither row grows while you build the word. */
.scramble-slot, .scramble-pool {
    min-height: 3rem; padding: var(--space-2);
    border: 1px solid var(--line); border-radius: var(--radius-md);
}
.scramble-pool { border-color: transparent; }
/* the slot's right/wrong tint is the word card's pair (above) */
/* lessons: the step's Back / Next stay at the column's foot on a phone (the
   min-height above) */
.lesson-page { display: flex; flex-direction: column; }
@media (max-width: 899px) {
    .lesson-page > .lesson-actions { margin-top: auto; padding-top: var(--space-4); }
}

/* ---------- reveals ----------
   A block that appears slides into place, one that goes slides out first (the
   component adds .is-leaving and waits) — nothing pops in or vanishes
   (2026-10-26). A block opts in with .slide; showing needs no script: the
   animation runs as the block becomes displayed. */
@keyframes ws-reveal { from { opacity: 0; transform: translateY(-.5rem); } }
@keyframes ws-conceal { to { opacity: 0; transform: translateY(-.5rem); } }
@keyframes ws-rise { from { opacity: 0; transform: translateY(1rem); } }
.slide:not([hidden]), details[open] > .collapse-body,
.user-menu[open] > .user-menu-panel, .word-menu[open] > .word-menu-panel { animation: ws-reveal var(--dur-in) var(--ease-out); }
.wcard.is-leaving, .slide.is-leaving,
details[open] > .collapse-body.is-leaving { animation: ws-conceal var(--dur-out) var(--ease-in) forwards; }
.bulk-bar:not([hidden]) { animation: ws-rise var(--dur-in) var(--ease-out); }

/* A control at work (aria-busy): the loader turns where the icon was, and the
   control takes no second press while the request runs. */
[aria-busy="true"] { pointer-events: none; cursor: progress; }
.spin { display: inline-flex; animation: ws-spin .9s linear infinite; }
@keyframes ws-spin { to { transform: rotate(360deg); } }
/* How many words the list holds (the dictionary), and Select at the right. */
.list-count { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); margin: 0 0 var(--space-3); }
.list-count > p { margin: 0; }

/* ---------- utilities ----------
   The handful of adjustments a page still needs after the components. They are
   spacing and one flex switch — nothing that invents a look; before these, each
   was an inline style (2026-10-19). */
/* margins on the spacing scale: the number is the step (--space-N) */
.mt-2 { margin-top: var(--space-2); }
.mt-3 { margin-top: var(--space-3); }
.mt-4 { margin-top: var(--space-4); }
.my-3 { margin: var(--space-3) 0; }
/* a link that is context, not a destination to be underlined (the way back) */
.plain-link { text-decoration: none; }
/* a lesson's title, and its exercise prompt on the one prompt rung, 24/800 (DES-1,
   2026-11-08: it was 18/700 while the quiz, Listening and the tutor read at 24/800) */
.lesson-title { margin: var(--space-1) 0 var(--space-3); }
.q-prompt { font-weight: var(--fw-black); font-size: var(--fs-2xl); }
/* The dictionary's bulk-action bar: it floats at the foot of the list column
   while words are ticked (written after the list, so `sticky` holds it at the
   window's bottom edge — over the phone's bottom nav — and lets it rest under
   the last card), and rises in (2026-10-26). */
.bulk-bar {
    position: sticky; z-index: 5; bottom: calc(var(--nav-h) + env(safe-area-inset-bottom) + var(--space-3));
    justify-content: space-between; margin-top: var(--space-3);
    background: var(--card); border: 1px solid var(--line); box-shadow: var(--shadow);
    padding: var(--space-2) var(--space-2) var(--space-2) var(--space-4); border-radius: var(--radius-md);
}
@media (min-width: 900px) { .bulk-bar { bottom: var(--space-4); } }
/* "Remove “tag”" with a long name: the actions may shrink and the label is cut
   with an ellipsis instead of running past a 360 px screen (2026-11-04). */
.bulk-bar .row-actions { min-width: 0; }
.bulk-bar .untag-btn { min-width: 0; max-width: 100%; }
/* the three legend dots on Discover's coverage counters */
.dot-accent { background: var(--accent); }
.dot-good { background: var(--good); }
.dot-line { background: var(--line); }
/* the two gradient stops of Discover's score ring */
.stop-fire { stop-color: var(--fire-1); }
.stop-accent { stop-color: var(--accent); }

/* ---------- hover (a mouse only) ----------
   One rule per family: filled buttons brighten; line and soft buttons, chips
   and answer options take the accent border and text; rows take the tint;
   text links take the accent. */
@media (hover: hover) {
    .btn:hover { filter: brightness(1.06); }
    .btn-soft:hover, .btn-line:hover, .chips .chip-btn:hover, .opt:not([disabled]):hover, .nav-toggle:hover {
        filter: none; border-color: var(--accent); color: var(--accent-strong);
    }
    .btn-danger:hover { filter: none; background: var(--bad-soft); }
    .chips .chip-btn[aria-pressed="true"]:hover, .chips .chip-btn[aria-current="true"]:hover { border-color: transparent; color: var(--on-accent); }
    .menu-item:hover, .btn-bare:hover, .mainnav a:hover:not([aria-current="page"]) { background: var(--accent-soft); color: var(--accent-strong); }
    .menu-item:hover svg.lucide { color: inherit; }
    .user-menu > summary:hover, .user-menu > summary:hover .caret { color: var(--accent-strong); }
    .public-nav a:hover, .public-foot-col a:hover, .public-foot-col button:hover { color: var(--accent-strong); }
}

/* ---------- public pages ----------
   The bar (PublicBar.svelte): the logo, the landing's sections (from 900px),
   one dropdown for the language and the theme (the account menu's look: a globe
   and the language's own name; on a phone a menu icon, the sections and Start
   free inside), Install app and Start free (from 900px), Log in. A narrow phone keeps the logo,
   not its name. The foot (PublicFooter.svelte) closes every public page. */
@media (max-width: 400px) { .public .brand-text { display: none; } }
.public-nav { display: none; }
.public-nav a { color: var(--text); font-weight: var(--fw-bold); font-size: var(--fs-sm); text-decoration: none; }
.public-menu { display: flex; } /* no baseline gap under the trigger: it centres on the bar's buttons */
.public-menu > summary { font-size: var(--fs-sm); font-weight: var(--fw-bold); gap: var(--space-2); }
.public-menu > summary svg.lucide { width: 1.25em; height: 1.25em; }
.public-menu .user-menu-panel { min-width: min(17rem, calc(100vw - 6rem)); }
.public-menu-nav { display: flex; flex-direction: column; gap: var(--space-1); }
.wide-ico, .public-menu > summary .menu-name, .public-start, .public-install { display: none; }
.public-menu-install { margin-top: var(--space-1); }
.phone-ico { display: inline-flex; }
@media (min-width: 900px) {
    .public-nav { display: flex; gap: var(--space-5); margin-left: var(--space-5); }
    .wide-ico, .public-start { display: inline-flex; }
    .public-menu > summary .menu-name { display: inline; }
    .phone-ico, .public-menu-nav { display: none; }
}
/* Install: in the menu until the bar has room, its icon alone until the label fits (Spanish, Russian) */
.public-install-text { display: none; }
@media (min-width: 960px) { .public-install { display: inline-flex; } .public-menu-install { display: none; } }
@media (min-width: 1200px) { .public-install-text { display: inline; } }
/* the foot at the screen's foot, however short the page (a column: the wrapper is display: contents) */
body.public { display: flex; flex-direction: column; min-height: 100dvh; }
/* A page's top is the shell's padding, in both shells (U2 §2): the public pages
   opened 8 px lower than every app screen. The bottom keeps the foot's space. */
.public-main { width: 100%; max-width: var(--form-max); margin: 0 auto; padding: var(--space-4) var(--space-4) calc(env(safe-area-inset-bottom) + var(--space-7)); }
@media (min-width: 900px) { .public-main { padding-top: var(--space-5); } }
/* log in and sign up: one card, the other page linked under it */
.public-main.auth-main { max-width: var(--auth-max); }
/* The reading measure (DES-1, 2026-11-08): a line of a reading page holds at most 80
   characters (Nunito's 16 px averages ~7 px a character, so 34rem is ~78); the 680
   column ran to 96. A centred block keeps its paragraph centred. */
.public-main p, .public-main li { max-width: 34rem; }
.public-main :is(.done, .center, .empty) p { margin-inline: auto; }
.auth-other { text-align: center; color: var(--muted); font-weight: var(--fw-regular); }
.public-foot { margin-top: auto; background: var(--nav-bg); border-top: 1px solid var(--line); padding: var(--space-7) calc(env(safe-area-inset-right) + var(--space-4)) calc(env(safe-area-inset-bottom) + var(--space-5)) calc(env(safe-area-inset-left) + var(--space-4)); }
.public-foot-in { max-width: var(--app-max); margin: 0 auto; display: grid; grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); gap: var(--space-6) var(--space-5); }
/* A narrow phone's foot: «Конфиденциальность» is wider than a 152 px track at
   360, and «Начать бесплатно» all but fills it, so those two columns take the
   whole row and the product's and the language's share one (walk 2026-11-05). */
@media (max-width: 420px) {
    .public-foot-in { grid-template-columns: repeat(2, minmax(0, 1fr)); grid-auto-flow: dense; row-gap: var(--space-5); }
    .public-foot-legal, .public-foot-account { grid-column: 1 / -1; }
}
.public-foot-about { grid-column: 1 / -1; }
.public-foot-about p { color: var(--muted); font-weight: var(--fw-regular); margin: var(--space-2) 0 0; max-width: 20rem; }
.public-foot-col { display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-1); }
.public-foot-col h2 { margin: 0 0 var(--space-1); padding: 0; }
.public-foot-col a, .public-foot-col button {
    display: inline-flex; align-items: center; min-height: var(--control-h-sm); padding: 0;
    color: var(--muted); font-weight: var(--fw-regular); text-decoration: none;
    background: none; border: 0; box-shadow: none;
}
.public-foot-col button[aria-current="true"] { color: var(--text); font-weight: var(--fw-bold); }
.public-foot-end { max-width: var(--app-max); margin: var(--space-6) auto 0; color: var(--muted); font-size: var(--fs-xs); }
@media (min-width: 900px) {
    .public-foot-in { grid-template-columns: 2fr repeat(3, minmax(0, 1fr)); }
    .public-foot-about { grid-column: auto; }
}
/* the landing (routes/+page.svelte): full-width sections, the bands on the
   card surface between hairlines, each holding a column as wide as the app.
   Text sits in a reading column; two columns side by side from 900px. */
.landing-section { padding: var(--space-7) calc(env(safe-area-inset-right) + var(--space-4)) var(--space-7) calc(env(safe-area-inset-left) + var(--space-4)); scroll-margin-top: var(--bar-h); }
.landing-section.band { background: var(--card); border-block: 1px solid var(--line); }
.landing-wrap { max-width: var(--app-max); margin: 0 auto; }
.landing-split { display: grid; gap: var(--space-6); align-items: center; }
@media (min-width: 900px) { .landing-split { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: var(--space-7); } }
.landing-section h2 { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); font-size: var(--fs-2xl); }
.landing-section h3 { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); }
.landing-section p { font-weight: var(--fw-regular); margin: 0 0 var(--space-3); }
.landing-center { justify-content: center; text-align: center; margin-bottom: var(--space-5); }
@media (min-width: 900px) { .landing-section h2 { font-size: var(--fs-3xl); } }
.landing-text { max-width: var(--form-max); }
/* the hero: the promise, one way in and the check's link; beside it a slice of the app */
.landing-hero-text { display: grid; gap: var(--space-4); justify-items: start; }
.landing-hero h1 { font-size: var(--fs-display); margin: 0; }
.landing-hero .landing-lede, .landing-hero .landing-note, .landing-hero .landing-for { margin: 0; }
.landing-for { color: var(--accent-strong); font-size: var(--fs-sm); font-weight: var(--fw-black); }
.landing-lede { color: var(--muted); font-size: var(--fs-lg); font-weight: var(--fw-regular); }
.landing-note { color: var(--muted); font-size: var(--fs-sm); font-weight: var(--fw-regular); }
.landing-ctas { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3) var(--space-5); }
.btn-lg { min-height: var(--control-h-lg); padding-inline: var(--space-5); font-size: var(--fs-lg); }
/* the hero's low-commitment way in is a destination too: a thumb's box, not a bare line */
.landing-more {
    display: inline-flex; align-items: center; min-height: var(--touch);
    color: var(--accent-strong); font-weight: var(--fw-black); text-decoration: none;
}
/* the slice: the deck's card, tilted, and the coverage line over it */
.landing-slice { position: relative; min-height: 19rem; }
.landing-slice::before { content: ""; position: absolute; inset: 8% 6% 0 10%; border-radius: 50%; background: var(--accent-soft); }
.flash.landing-slice-word { position: absolute; top: 0; right: 0; width: min(78%, 17rem); transform: rotate(4deg); }
.card.landing-slice-cov { position: absolute; bottom: 0; left: 0; width: min(92%, 22rem); margin: 0; transform: rotate(-3deg); }
.landing-slice-cov .coverage { font-size: var(--fs-md); }
.landing-cues { display: flex; justify-content: space-between; gap: var(--space-2); font-size: var(--fs-sm); font-weight: var(--fw-bold); color: var(--muted); }
.landing-cues .know { color: var(--accent-strong); }
/* the pain: what's heard and what's read, drawn (no show's picture) */
.landing-screen {
    aspect-ratio: 16 / 9; display: flex; flex-direction: column; justify-content: flex-end; align-items: center;
    padding: var(--space-3); border-radius: var(--radius-md); background: var(--screen); color: var(--on-screen);
}
.landing-tvs { display: grid; gap: var(--space-3); }
.landing-tvs figure { margin: 0; }
.landing-tvs figcaption { margin-top: var(--space-2); color: var(--muted); font-size: var(--fs-sm); text-align: center; }
@media (min-width: 560px) { .landing-tvs { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.landing-sub span.lit { padding-inline: var(--space-1); border-radius: var(--space-1); background: var(--accent-soft); color: var(--accent-strong); }
.landing-sub { padding: var(--space-1) var(--space-3); border-radius: var(--radius-md); background: var(--scrim); font-size: var(--fs-md); font-weight: var(--fw-bold); text-align: center; }
/* cards side by side from 900px: who it's for, what else, the promise */
.landing-list { list-style: none; margin: 0 0 var(--space-3); padding: 0; display: grid; gap: var(--space-3); }
.landing-list li { display: flex; align-items: flex-start; gap: var(--space-2); font-weight: var(--fw-regular); }
.landing-list svg.lucide { flex: 0 0 auto; width: 1.25em; height: 1.25em; color: var(--good); }
.landing-list-no svg.lucide { color: var(--bad); }
/* the three steps, each a phone with its slice of the app (in English: the product teaches English) */
.landing-how { list-style: none; margin: 0; padding: 0; counter-reset: step; display: grid; gap: var(--gap-lg); }
.landing-how li { counter-increment: step; display: grid; gap: var(--space-3); justify-items: center; text-align: center; align-content: start; }
.landing-section .landing-how p { margin: 0; }
.landing-how h3 { justify-content: center; }
.landing-how h3::before {
    content: counter(step); flex: 0 0 auto; display: inline-grid; place-items: center; width: 1.75em; height: 1.75em;
    border-radius: var(--radius-pill); background: var(--accent-fill); color: var(--on-accent); font-weight: var(--fw-black);
}
@media (min-width: 900px) { .landing-how { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
/* a phone: the frame's border is the ink, the screen the page's own ground */
.landing-phone {
    width: min(100%, 13rem); min-height: 15rem; padding: var(--space-4) var(--space-3); border: var(--control-border) solid var(--text);
    box-shadow: 0 0 0 var(--raise); border-radius: var(--radius-lg); background: var(--bg); display: grid; gap: var(--space-3); align-content: center; text-align: start;
}
.landing-phone > .flash, .landing-phone > .card { width: 100%; margin: 0; box-shadow: none; }
.landing-phone .flash .word-row { min-height: 0; }
.landing-phone .word { font-size: var(--fs-2xl); }
.landing-phone .landing-cues { font-size: var(--fs-xs); }
.landing-phone > .btn { pointer-events: none; }
.landing-ring-cap { text-align: center; font-weight: var(--fw-bold); font-size: var(--fs-sm); }
.landing-phone .ring { margin: 0 auto; }
.landing-langs { justify-content: center; }
.landing-duo { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-2); }
.landing-duo .btn { padding-inline: var(--space-1); font-size: var(--fs-sm); pointer-events: none; }
.landing-quiz { display: grid; gap: var(--space-2); }
.landing-quiz .opt { display: flex; align-items: center; justify-content: center; min-height: var(--control-h); font-size: var(--fs-md); }
.landing-quiz-q { display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2); color: var(--muted); font-size: var(--fs-sm); font-weight: var(--fw-bold); }
/* the try-it: the free check's first words on the deck */
.landing-try { width: 100%; max-width: 26rem; margin: 0 auto; }
.landing-try .done h3 { justify-content: center; } /* a finish card centres its title */
/* the 85%: the number first, big */
.landing-big-num, .landing-section .landing-big-num { font-size: var(--fs-icon); font-weight: var(--fw-black); line-height: var(--lh-none); color: var(--accent-strong); }
.landing-section .landing-big { font-size: var(--fs-xl); font-weight: var(--fw-bold); }
/* the price: one card */
/* the card in the closing band: the price, white on the accent, its own colours */
.landing-end .card { color: var(--text); text-align: start; }
.landing-end .card h2 { justify-content: flex-start; font-size: var(--fs-xl); }
.landing-finale { display: grid; gap: var(--gap-lg); align-items: center; }
@media (min-width: 900px) { .landing-finale { grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); gap: var(--space-7); } }
.landing-price { margin: 0; display: grid; gap: var(--space-3); }
.landing-price > * { margin: 0; }
.landing-section .landing-price-big { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-2); color: var(--muted); font-weight: var(--fw-bold); }
/* the closing band, on the accent: both ways in again */
.landing-section.landing-end { background: var(--accent-fill); color: var(--on-accent); text-align: center; }
.landing-end h2 { justify-content: center; }
.landing-end .card .chip { background: var(--accent-soft); }
.landing-end .landing-ctas { justify-content: center; }
.landing-end .landing-ctas .btn { background: var(--card); color: var(--accent-strong); box-shadow: none; }
.landing-end .landing-ctas .btn-line { background: transparent; color: var(--on-accent); border-color: currentColor; }
/* a phone's Start free at the screen's foot, once the hero's is gone */
/* the pain and the number: two equal cards */
.landing-pair, .landing-who, .landing-bento, .landing-ledger, .landing-finale { display: grid; gap: var(--gap); }
.landing-pair > .card, .landing-who > .card, .landing-bento > .card, .landing-ledger > .card { margin: 0; display: grid; gap: var(--space-4); align-content: start; }
.landing-pair .landing-list, .landing-pair p { margin: 0; }
.landing-stat { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3) var(--space-4); }
.landing-section .landing-stat p { margin: 0; }
.landing-stat .landing-big { flex: 1; min-width: 13rem; }
@media (min-width: 900px) { .landing-pair, .landing-ledger { grid-template-columns: repeat(2, minmax(0, 1fr)); } .landing-ledger { grid-template-columns: 3fr 2fr; } }
/* the story: a quote at the display size, three beats beside it */
.landing-letter { display: grid; gap: var(--space-5); align-items: center; }
@media (min-width: 900px) { .landing-letter { grid-template-columns: minmax(0, 7fr) minmax(0, 5fr); gap: var(--space-7); } }
.landing-section h2.landing-for { margin: 0 0 var(--space-3); font-size: var(--fs-sm); }
.landing-section .landing-quote { margin: 0; font-size: var(--fs-display); font-weight: var(--fw-black); line-height: var(--lh-tight); letter-spacing: var(--tracking-tight); }
.landing-quote em { font-style: normal; color: var(--accent-strong); }
.landing-beats { display: grid; gap: var(--space-3); }
.landing-section .landing-beat { display: flex; align-items: center; gap: var(--space-3); margin: 0; }
.landing-beat svg.lucide { flex: 0 0 auto; width: 2.5rem; height: 2.5rem; padding: var(--space-2); border-radius: var(--radius-pill); background: var(--accent-soft); color: var(--accent-strong); }
.landing-section .landing-sign { margin: 0; color: var(--muted); font-size: var(--fs-sm); }
/* two ways in, and what else is in it: cards of one height, each with its picture */
.landing-who > .card { grid-template-columns: minmax(0, 1fr); align-items: center; }
@media (min-width: 560px) { .landing-who > .card { grid-template-columns: minmax(0, 1fr) 13.5rem; } }
@media (min-width: 900px) { .landing-bento { grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); } .landing-tall { grid-row: span 2; } }
@media (min-width: 560px) { .landing-wide { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); align-items: center; } }
.landing-tall { align-content: space-between; }
.landing-who h3, .landing-bento h3 { margin: 0 0 var(--space-1); }
.landing-who p, .landing-bento p { margin: 0 0 var(--space-2); }
.landing-more-title { margin-top: var(--space-7); }
.landing-inset { display: grid; gap: var(--space-3); align-content: center; padding: var(--space-4); border-radius: var(--radius-md); background: var(--bg); }
.landing-inset .coverage { font-size: var(--fs-md); }
.landing-inset .stats, .landing-inset .wcard-def { margin: 0; }
.landing-tall .landing-inset { min-height: 15rem; }
.landing-inset .wcard-title b { font-size: var(--fs-xl); }
.landing-say { display: inline-flex; align-items: center; gap: var(--space-2); font-weight: var(--fw-bold); }
.landing-say svg.lucide { flex: 0 0 auto; width: 1.25em; height: 1.25em; color: var(--accent-strong); }
.landing-ok { background: var(--good-soft); color: var(--good-text); }
.landing-off { background: var(--warn-soft); color: var(--warn-text); }
/* what we promise: three numbers, and what we don't */
.landing-row { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-1) var(--space-4); }
.landing-row span { flex: 1 1 13rem; }
.landing-row + .landing-row { padding-top: var(--space-3); border-top: 1px solid var(--line); }
.landing-row b { min-width: 9rem; font-size: var(--fs-icon); line-height: var(--lh-none); font-weight: var(--fw-black); color: var(--accent-strong); }
.landing-ledger .landing-list { margin: 0; }
.landing-sticky {
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 40;
    padding: var(--space-3) calc(env(safe-area-inset-right) + var(--space-4)) calc(env(safe-area-inset-bottom) + var(--space-3)) calc(env(safe-area-inset-left) + var(--space-4));
    background: var(--nav-bg); border-top: 1px solid var(--line);
    transform: translateY(100%); visibility: hidden; transition: transform var(--dur-out) var(--ease-in), visibility var(--dur-out) var(--ease-in);
}
/* …and it comes in on the entry's, out on the exit's (U9 §2.6) */
.landing-sticky.on { transform: none; visibility: visible; transition: transform var(--dur-in) var(--ease-out), visibility var(--dur-in) var(--ease-out); }
@media (min-width: 900px) { .landing-sticky { display: none; } }
/* the three steps: numbered dots */
.landing-steps { list-style: none; margin: 0; padding: 0; counter-reset: step; }
.landing-steps li { display: flex; align-items: baseline; gap: var(--space-3); font-weight: var(--fw-regular); counter-increment: step; }
.landing-steps li + li { margin-top: var(--space-3); }
.landing-steps li::before {
    content: counter(step); flex: 0 0 auto; display: inline-grid; place-items: center; width: 1.75em; height: 1.75em;
    border-radius: var(--radius-pill); background: var(--accent-soft); color: var(--accent-strong); font-weight: var(--fw-black);
}
/* the install sheet's iPhone steps (InstallSheet.svelte): each lit in turn, its icon tapped */
.install-steps li { align-items: center; padding: var(--space-2) var(--space-3); border: 1px solid transparent; border-radius: var(--radius-md); animation: install-step 6s ease-in-out infinite; }
.install-steps li + li { margin-top: var(--space-1); }
.install-steps li:nth-child(2) { animation-delay: -4s; }
.install-steps li:nth-child(3) { animation-delay: -2s; }
.install-steps svg.lucide { flex: 0 0 auto; width: 1.25em; height: 1.25em; color: var(--accent-strong); animation: install-tap 6s ease-in-out infinite; animation-delay: inherit; }
@keyframes install-step {
    0%, 28% { background: var(--accent-soft); border-color: var(--accent); }
    34%, 100% { background: transparent; border-color: transparent; }
}
@keyframes install-tap { 0%, 4%, 14%, 100% { transform: scale(1); } 9% { transform: scale(.75); } }
/* the chart: words known → share of the words said (.mini-row-lg) */
.landing-bars { display: grid; gap: var(--space-4); }
.landing-section .landing-source { color: var(--muted); font-size: var(--fs-xs); margin: 0; }
/* the questions: a slide-down each, in reading type — the shared trigger's rung
   since 2026-11-06, so the `text-transform: none; letter-spacing: normal`
   override that proved the caps recipe wrong is gone (U1 §6 pick 3) */
.landing-faq-head { display: flex; align-items: center; justify-content: center; gap: var(--space-3); margin-bottom: var(--space-5); }
.landing-faq-head h2 { margin: 0; }
.landing-faq-head .mascot { width: 6rem; height: 6rem; margin: 0; }
.landing-faq-cols { display: grid; gap: 0 var(--gap-lg); align-items: start; }
@media (min-width: 900px) { .landing-faq-cols { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.landing-faq .collapse { margin: 0; padding-block: var(--space-3); border-top: 1px solid var(--line); }
.landing-faq .collapse-body { margin: 0 0 var(--space-3); color: var(--muted); }

/* The landing's Add word try-it (TryAdd.svelte): the mascot beside the heading, the
   same on a phone and a desktop, the app's own form under it in a card that nudges
   until touched, never under his feet (his note, 2026-11-04); in the closing band,
   the same on the accent (the card keeps its own colours). */
.try-add { display: grid; gap: var(--space-4); justify-items: center; }
.try-add-head { display: flex; align-items: center; gap: var(--space-3); width: 100%; max-width: 30rem; text-align: start; }
.try-add-head h2 { margin-bottom: var(--space-1); }
.try-add-titles p { margin: 0; }
.try-add .mascot { width: 96px; height: 96px; margin: 0; flex-shrink: 0; animation: ws-pop var(--dur-react) var(--ease-spring); }
.try-add-card { width: 100%; max-width: 30rem; margin: 0; text-align: start; color: var(--text); }
.try-add-end { font-weight: var(--fw-bold); }
.landing-end .try-add { margin-top: var(--space-6); }
/* The keyboard up on a phone leaves the sheet shorter than what it holds: it scrolls
   inside its own box (a modal locks the page), and the last field keeps a margin. */
.keep-sheet {
    width: min(480px, calc(100vw - var(--space-6)));
    overflow-y: auto; overscroll-behavior: contain; padding-bottom: var(--space-3);
}
@media (min-width: 900px) {
    .try-add-head, .try-add-card { max-width: 36rem; }
    .try-add .mascot { width: 120px; height: 120px; }
}

}
@layer platform {
@media (prefers-reduced-motion: reduce) {
    * { animation: none !important; transition: none !important; }
    /* `*` never selects a pseudo-element: the dialogs' scrim and the screen's view
       transition kept animating with motion reduced (U9 §2.3). */
    ::backdrop, ::view-transition-group(screen),
    ::view-transition-old(screen), ::view-transition-new(screen) {
        animation: none !important; transition: none !important;
    }
}
/* The landing's section links glide (his ask, 2026-11-03): +page.svelte sets the
   class once the page has settled, so a route change or a Back lands at once. */
@media (prefers-reduced-motion: no-preference) {
    html.scroll-smooth { scroll-behavior: smooth; }
}

}
@layer components {
/* ---------- thumbs: on a phone everything tappable reaches --touch ----------
   Measured 390×844 (2026-10-17): chips 28-34, segmented options 32, the sound
   pill 36-40, the account trigger 25, text links 22, small buttons 38, the
   scramble letters 40. At the end of the file on purpose: it must out-rank
   the component rules it raises (`.btn-sm`, `.letter-tile`). */
@media (max-width: 899px) {
    .chips .chip-btn { min-height: var(--touch); min-width: var(--touch); }
    .segmented-opt, .menu-item { min-height: var(--touch); }
    .btn-sm { min-height: var(--touch); min-width: var(--touch); }
    .sound.sm { --sound-size: var(--touch); }
    .letter-tile, .letter-slot, .letter-empty { width: 2.75rem; height: 2.75rem; }
    .user-menu > summary { min-height: var(--touch); min-width: var(--touch); }
    /* the toast's one button (Undo, Dismiss) is a thumb's target too */
    .toast button { min-height: var(--touch); min-width: var(--touch); }
    /* text links that are destinations get a thumb-sized box, not a bare line */
    .page-head > a.muted, main details.collapse > summary {
        display: inline-flex; align-items: center; min-height: var(--touch);
    }
    .public-foot-col a, .public-foot-col button { min-height: var(--touch); }
}
}
