/* ===== English vocabulary words — helium3words.com ===== */
/* ===== The palette =====
   Light is the default and stays the default. Nothing here reads prefers-color-scheme: a
   device set to dark does not turn the app dark on its own, because the colours are
   part of what the app looks like and not a setting the browser gets to make for it.
   Dark is something the visitor asks for on the settings screen, and the answer is
   written onto <html> as data-theme (see applyDisplay in app.js).

   The default was the dark palette until 29 August 2026, and the swap is why the two
   blocks are the way round they are: the block that carries no attribute is whichever
   one the app opens in, so making light the default meant moving the light values up
   into :root and giving the dark ones an attribute of their own — not adding a second
   default beside the first. A visitor who had already chosen is unaffected: their record
   names a theme, and that record is still the only thing consulted.

   Every colour on a screen is a token declared here, and the dark block below redefines
   the tokens and nothing else. That is the whole discipline of it: a rule that spells a
   colour out where it is used is a rule the dark theme cannot reach, and it would sit
   there as a light patch on a dark page. A new colour is named here first and given a
   dark value in the same edit.

   legal.html and maintenance.html load this file too. Their small local theme.js reads
   the saved display preference and applies the same attributes before paint; when no
   preference exists, they keep this default light palette. */
:root {
  color-scheme: light;
  --bg: #F5F7F6;
  --card: #FFFFFF;
  --border: #DBE2DF;
  /* Decorative dividers may stay quiet, but the edge that identifies an input or
     button has to remain visible without relying on its label. This clears 3:1 against
     both --bg (3.64:1) and --card (3.91:1); --border remains the quieter token for
     decorative cards and rules. */
  --control-border: #79837F;
  --text: #12171A;
  --muted: #5A656C;
  /* A light page needs ink of its own. The bright greens and reds that carry on a
     near-black background disappear on a near-white one, so --primary, --success and
     --error are darkened here until they clear 4.5:1 against both --bg and --card.
     --on-primary is white, because what it names is whatever sits on top of --primary,
     and here that is a deep green.

     Darkening is not the whole of it, though, and the first attempt read as neither
     colour: a green at 143° is an emerald that the eye takes for teal, and a red at
     64% saturation was a brick. Both are moved towards the colour they are meant to
     be — the hue nearer plain green, the saturation of both up.

     --success-bg and --error-bg are the fill of a right and a wrong answer, and on a
     white page a tint is not what those are for. Three tries at deepening one produced
     mint and pink, because a colour mixed with that much white cannot be the colour
     itself; so they are the green and the red at full strength, with white on them
     instead of the page's ink. That is the only pairing here that carries a real red:
     white on #CC1F1F clears 5.55:1, where near-black on the same red clears 3.8:1 and
     could not be used at all. Green 4.92:1 the same way. Where a pale red really is
     wanted — a form error, the icon on the delete dialog — that is --error-soft, which
     is why it exists rather than the fills being pulled back towards white again. */
  --primary: #1B7A32;
  --on-primary: #FFFFFF;
  --success: #137A26;
  --success-bg: #178230;
  --error: #C41616;
  --error-bg: #CC1F1F;
  /* The pale red, for a background that has ordinary text on it rather than a verdict. */
  --error-soft: #FDE7E7;
  /* The word on a right or wrong answer. It is not --text: the fills above are strong
     enough that the page's near-black ink no longer clears 4.5:1 on them. */
  --verdict-ink: #FFFFFF;
  /* The delete buttons. Their label is the one place the red must not also be the
     ink: a red word on a red tint is the weakest text on the screen, and it says
     "delete everything". On a light page white ink cannot sit on a pale tint, so the
     button is filled with the red itself: white on #C41616 clears 6.1:1. */
  --danger-btn-bg: #C41616;
  --danger-btn-ink: #FFFFFF;
  /* The badge colours: the level numbers on the home screen, the tile icons and the
     matching icons on the help screen. Each is a pair — a fill and the ink on it — and
     the two are always used together. Here it is a pale fill with dark ink on it; the
     dark block below turns every pair round. */
  --badge-green-bg: #DDF3E1;
  --badge-green: #14672A;
  --badge-amber-bg: #FBEBC8;
  --badge-amber: #7A5300;
  --badge-blue-bg: #DCEBFB;
  --badge-blue: #14528F;
  --badge-purple-bg: #EEE2F8;
  --badge-purple: #6B2E9E;
  --badge-orange-bg: #FCE7D4;
  --badge-orange: #8A4A12;
  /* Rose, added 18 September 2026 for the vocabulary-check band — the one band that is
     filled with its colour rather than edged in it, so this pair is read both ways
     round: the deep one under the pale one on the band, the pale one under the deep one
     on its pill. 6.0:1 in this theme and 7.3:1 in the dark one. */
  --badge-rose-bg: #FBE1EE;
  --badge-rose: #A3195B;
  /* A starred word, and the empty groove behind a chart bar or a progress bar */
  --star: #9A6B00;
  --track: #DDE3E1;
  --skeleton-shine: rgba(255, 255, 255, 0.82);
  --overlay: rgba(16, 22, 26, 0.45);
  --radius: 20px;
  --radius-btn: 16px;
  --shadow: 0 8px 24px rgba(16, 24, 20, 0.10);
  /* The solid edge a raised card stands on (the home tiles): the shadow's own tint, dark
     enough to read as a lip against --bg under a white card. */
  --raise-edge: rgba(16, 24, 20, 0.22);
  --shadow-modal: 0 16px 48px rgba(16, 24, 20, 0.22);
  /* Soft halo on the Advanced Learning panel that is being read aloud. The colour is
     the accent at a low alpha, so dark theme can brighten the same token without a
     separate rule at the call site. */
  --learn-glow: rgba(27, 122, 50, 0.16);
  /* The hover halo on the "Listen now" pill of the listening band: the amber ink at a
     low alpha, for the same reason as the green above. */
  --learn-cta-glow: rgba(122, 83, 0, 0.22);
  /* The halo a refusal puts round the terms box: the error ink at a low alpha, the
     --learn-glow of the red pair. --error-soft cannot do this job - the refused row is
     already filled with it, so a halo in the same colour would be invisible. */
  --error-glow: rgba(196, 22, 22, 0.28);
  /* The step a real pointer gets on a bordered button. One shade away from --card in
     each theme, so a hovered button parts from the page without its ink moving. */
  --btn-hover-bg: #EDF2EF;
  /* ===== The tip bubble =====
     The short explanation a control carries, and the small "i" that opens it on a
     touch screen. The bubble is deliberately the one surface on the light page that
     is not a white card: it is a transient thing sitting on top of the page rather
     than part of it, and a white bubble on a white card is a shadow's worth of
     difference — it has to read as an overlay at a glance, from the colour alone,
     before anything is read. #F5F7F6 on #12171A clears 15.6:1. The dark theme cannot
     use the same trick in reverse, and does not try: a near-white bubble on a
     near-black page is the brightest thing on the screen and glares in the dim room
     the dark theme exists for, so there the bubble is a raised slate instead, two
     steps above --card, which separates from the page by lightness the way the light
     theme's does by darkness. */
  --tip-bg: #12171A;
  --tip-ink: #F5F7F6;
  --tip-border: #12171A;
  /* The glyph in front of the text. Muted against the bubble rather than the page, so
     it sits behind the words it introduces instead of competing with them. */
  --tip-glyph: #9FB3A6;
  --tip-shadow: 0 10px 30px rgba(16, 24, 20, 0.28);
  /* The "i" control. Quiet at rest — it stands beside a real control and must never be
     mistaken for it — and filled with the accent once its bubble is open. */
  --tip-btn-ink: #5A656C;
  --tip-btn-border: #79837F;
  --tip-btn-bg: #FFFFFF;
  /* ===== Text weight =====
     Four tokens covering every weight the app uses, so that "Bold text" on the settings
     screen is one switch and not a hunt through the file. No rule states a weight in
     figures any more: --weight-normal is ordinary running text, --weight-medium the
     slightly heavier answer buttons and chips, --weight-bold the headings and labels,
     --weight-heavy the scores, the level numbers and the word being practised.

     All four have to move together, and that is the lesson of the first attempt at this:
     it lifted the two lightest tokens only, on the reasoning that what was already 700
     was bold enough. But most of what is on a screen here is already 700 or 800 — every
     heading, every tile name, every chip — so the switch moved almost nothing that the
     eye was actually resting on, and read as broken rather than subtle. The steps below
     keep the four ranks distinct at both settings; the page gets heavier all through,
     and its hierarchy survives.

     There is no font licence to breach in doing this. The app bundles no font and loads
     none: it asks for system-ui, which is the font the device already uses for its own
     interface, and drawing it heavier is what the operating system itself does when its
     own bold-text accessibility setting is switched on. NOTICE.md records that there is
     no third-party font here to have terms. A device whose system font has no face at a
     given weight is not a problem either — the browser picks the nearest one it has. */
  --weight-normal: 400;
  --weight-medium: 500;
  --weight-bold: 700;
  --weight-heavy: 800;
  /* The last step of all, for the rank that has run out of font. See the rule below
     .heavy-rank; zero here means the ordinary page draws no stroke at all. */
  --heavy-stroke: 0;
}

/* ===== Dark =====
   Not an inversion of the block above — a near-black page needs ink of its own. The deep
   greens and reds that carry on white go muddy on it, so --primary, --success and
   --error are lifted here until they clear 4.5:1 against both --bg and --card, and the
   badge pairs are turned round again: a dark fill with pale ink on it. --on-primary goes
   to a near-black, because what it names is whatever sits on top of --primary, and that
   is now a bright green. */
:root[data-theme="dark"] {
  color-scheme: dark;
  --bg: #0C0F12;
  --card: #15191E;
  --border: #252B32;
  /* The dark-theme control edge clears 3:1 against --bg (3.78:1) and --card (3.47:1). */
  --control-border: #62707C;
  --text: #F2F5F3;
  --muted: #98A2A9;
  --primary: #7EE787;
  --on-primary: #0B1F10;
  --success: #4ADE80;
  --success-bg: #10331F;
  --error: #FF5C5C;
  --error-bg: #3A1616;
  /* The light page fills a verdict with the colour itself; a near-black one cannot,
     because a fill bright enough to be a real green would be the brightest thing on
     the screen. The dark tints above are already unmistakable against #0C0F12, so
     these two keep the page's own ink and nothing about a dark verdict changes. */
  --error-soft: #3A1616;
  --verdict-ink: #F2F5F3;
  /* On this page the tint is dark enough to carry white ink itself, which clears
     14.9:1 on it, so the button keeps the tint rather than the red. */
  --danger-btn-bg: #3A1616;
  --danger-btn-ink: #FFFFFF;
  --badge-green-bg: #12351C;
  --badge-green: #7EE787;
  --badge-amber-bg: #3A2E10;
  --badge-amber: #FFC94A;
  --badge-blue-bg: #16283C;
  --badge-blue: #6CB4FF;
  --badge-purple-bg: #2E1B3C;
  --badge-purple: #D9A1FF;
  --badge-orange-bg: #3A2412;
  --badge-orange: #FFB870;
  --badge-rose-bg: #3C1630;
  --badge-rose: #FF8CC6;
  --star: #FFC94A;
  --track: #2A313A;
  --skeleton-shine: rgba(255, 255, 255, 0.10);
  --overlay: rgba(4, 6, 8, 0.72);
  --shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
  --raise-edge: rgba(0, 0, 0, 0.6);
  --shadow-modal: 0 16px 48px rgba(0, 0, 0, 0.55);
  --learn-glow: rgba(126, 231, 135, 0.22);
  --learn-cta-glow: rgba(255, 201, 74, 0.28);
  --error-glow: rgba(255, 92, 92, 0.34);
  --btn-hover-bg: #1D242B;
  /* A raised slate rather than the light theme's inversion — see the note on --tip-bg
     above. #F2F5F3 on #262E36 clears 11.4:1, and the bubble still parts from --card
     (#15191E) and from --bg (#0C0F12) without being the brightest thing on the page.
     The border is one step brighter again, because a dark bubble on a dark page needs
     a drawn edge where the light theme's is carried by the colour change alone. */
  --tip-bg: #262E36;
  --tip-ink: #F2F5F3;
  --tip-border: #3D4954;
  --tip-glyph: #98A2A9;
  --tip-shadow: 0 10px 30px rgba(0, 0, 0, 0.55);
  --tip-btn-ink: #98A2A9;
  --tip-btn-border: #62707C;
  --tip-btn-bg: #15191E;
}

/* ===== Bold text =====
   Set on the settings screen and written onto <html> beside data-theme. Every weight on
   the page moves, and each one has to be aimed at a face the font actually has — the
   figures are not a scale, they are a lookup into four faces (see the measurements under
   .heavy-rank below).

   The steps are two faces each wherever there is room for two, because one face is not
   enough to notice on a phone. That is not a guess: --weight-medium first went 500 -> 700,
   which reads as a jump of two hundred and is nothing of the kind — 500 already resolves to
   the Semibold face, so the answers in a round were half-bold to begin with and moved a
   single step to Bold. Side by side at 2.5x they were barely distinguishable, and at
   ordinary size they were not distinguishable at all. 800 lands them on Black instead. */
:root[data-bold="on"] {
  --weight-normal: 700;  /* Regular -> Bold */
  --weight-medium: 800;  /* Semibold -> Black */
  --weight-bold: 900;    /* Bold -> Black */
  --weight-heavy: 900;   /* Black -> Black, so the stroke below does the work */
  --heavy-stroke: 0.021em;
}

/* ===== The rank that has no heavier face =====
   The step from --weight-heavy 800 to 900 draws nothing new, and it was worth measuring
   rather than assuming: rendered at 64px, system-ui on Windows gives 454.91px at weight
   400, 457.84 at 500 and 600, 483.69 at 700, and 499.56 at both 800 and 900. Four faces,
   not nine. So the three lower ranks each move to a face they did not have before, and
   the top one asks for a face the font does not contain and is handed the one it was
   already using. Every heading, score and level number sat still — and so did the word
   being practised, which is the largest thing on the screen and the first thing anyone
   would look at to see whether the switch had done anything. It had not.

   A hairline stroke in the text's own colour supplies the step the font cannot. It is
   measured in em, so it holds its proportion on a 0.72rem badge and on the 3.2rem score
   alike, and it is worth nothing at all unless bold text is switched on.

   The list is written out because CSS cannot select "everything using that token". Every
   rule that sets font-weight: var(--weight-heavy) belongs here; a new one has to be added
   in the same edit, or it will be the one element on the screen that does not move.
   ::marker is deliberately absent — it takes font properties and not this one. */
:root[data-bold="on"] .home-titles h1,
:root[data-bold="on"] .level-num,
:root[data-bold="on"] .level-name,
:root[data-bold="on"] .tile-icon-help,
:root[data-bold="on"] .notice-group-icon,
:root[data-bold="on"] .quiz-word,
:root[data-bold="on"] .learn-word,
:root[data-bold="on"] .score-big,
:root[data-bold="on"] .page-title,
:root[data-bold="on"] .stat-num,
:root[data-bold="on"] .game-score,
:root[data-bold="on"] .help-hero-icon,
:root[data-bold="on"] .help-icon-warn,
:root[data-bold="on"] .modal-title,
:root[data-bold="on"] .legal-title,
:root[data-bold="on"] .legal-section h2,
:root[data-bold="on"] .maint-title {
  -webkit-text-stroke: var(--heavy-stroke) currentColor;
}

* { margin: 0; padding: 0; box-sizing: border-box; }

/* Amharic beside English. Ethiopic has no glyphs in Segoe UI, so Windows hands it to
   Ebrima, which draws its letters about a sixth taller than Segoe UI's capitals at the
   same font-size: "አማርኛ → English" then reads as two sizes on one line, the Amharic
   looming over the Latin rather than sitting with it, and the taller line box pushes the
   button it sits in taller than the row beside it. Nothing is out of place — the letters
   are simply bigger — so the fix is to ask for the same font one notch smaller, not to
   move anything. size-adjust does that at the font rather than in the cascade, so it
   composes with every font-size rule in this file and cannot compound where one
   language-marked element sits inside another, as the consent notice's do.
   unicode-range holds it to the Ethiopic blocks: every other character is still drawn by
   system-ui, and where Ebrima is not installed the face has no source at all and
   Ethiopic falls back exactly as it did before. The bold face is named as well —
   Ebrima Bold is what the buttons and headings ask for, and without it the browser
   would smear the regular one instead. */
@font-face {
  font-family: "Ethiopic UI";
  src: local("Ebrima");
  font-weight: 400;
  size-adjust: 90%;
  unicode-range: U+1200-139F, U+2D80-2DDF, U+AB00-AB2F;
}
@font-face {
  font-family: "Ethiopic UI";
  src: local("Ebrima Bold"), local("Ebrima");
  font-weight: 700;
  size-adjust: 90%;
  unicode-range: U+1200-139F, U+2D80-2DDF, U+AB00-AB2F;
}

html, body {
  background: var(--bg);
  color: var(--text);
  font-family: "Ethiopic UI", system-ui, sans-serif;
  font-weight: var(--weight-normal);
  min-height: 100%;
}
html { scrollbar-gutter: stable; }

button {
  font-family: inherit;
  /* Inherited rather than left to the browser, so that "Bold text" reaches the buttons
     too — a form control does not take the page's font weight on its own. */
  font-weight: inherit;
  color: inherit;
  background: none;
  border: none;
  cursor: pointer;
}

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.hidden { display: none !important; }
.muted { color: var(--muted); }
/* The consent banner sits fixed at the bottom of the screen and covers the end of the
   page — including the footer links to the terms and the policy that the banner itself
   points at. app.js measures its height and feeds it in here, so it is possible to
   scroll below it. */
body.consent-banner-open { padding-bottom: var(--consent-banner-h, 0px); }
/* A phrase that must not break across two lines */
.nobreak { white-space: nowrap; }

/* ===== Accessibility ===== */
/* A visible focus ring for keyboard navigation (WCAG 2.4.7) */
:focus-visible {
  outline: 3px solid var(--primary);
  outline-offset: 2px;
  border-radius: 4px;
}
h1:focus, .page-title:focus, .quiz-word:focus { outline: none; }
h1:focus-visible, .page-title:focus-visible, .quiz-word:focus-visible {
  outline: 3px solid var(--primary);
  outline-offset: 4px;
}

/* The sticky .page-top/.quiz-top row paints an opaque band over the top ~72px of the
   window (its 20px ::before band, the 42-44px row, 10px padding) for the whole scroll
   length of a screen, and legal.html/faq.html carry the same bar. Without this, a
   fragment link or a Tab press scrolled its target flush to y=0 — behind the bar, so
   the reader arrived at a heading they could not see and a focused link had no visible
   ring (WCAG 2.4.11 Focus Not Obscured). scroll-padding on the scroll container covers
   both anchor navigation and the browser's own scroll-into-view on focus, which
   per-target scroll-margin does not. */
html { scroll-padding-top: 76px; }

/* Skip link to the content (WCAG 2.4.1). The skip target carries tabindex="-1" so that
   focus really does move to it, but it is not a control that can be activated and is
   therefore not marked with a focus ring — a frame around the whole app would confuse
   rather than inform. */
#main:focus { outline: none; }
.skip-link {
  /* fixed, not absolute: top: 0 on an absolute box is the top of the document, so a
     reader who had followed an in-page anchor and then Shift+Tabbed back to this link
     focused an element rendered thousands of pixels above the window — a focused
     control with no visible ring anywhere on screen. Fixed pins it to the viewport
     wherever the page is scrolled to. */
  position: fixed;
  top: -60px;
  inset-inline-start: 16px;
  background: var(--primary);
  color: var(--on-primary);
  font-weight: var(--weight-bold);
  padding: 12px 20px;
  border-radius: 0 0 12px 12px;
  text-decoration: none;
  z-index: 200;
  transition: top 0.2s;
}
.skip-link:focus { top: 0; }

/* Honouring the reduced-motion preference (WCAG 2.3.3) */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

.app {
  max-width: 520px;
  margin: 0 auto;
  padding: 20px 16px 40px;
}
/* #app-footer used to be the last child of .app and took its width, its side padding and
   the 40px beneath it from that box. It is outside <main> now, so that <footer> can be
   the page's contentinfo landmark, and it carries the same box itself: the same cap, the
   same centring, the same side padding, and the 40px that used to close the page moved
   down with it. [data-app] is on <html> in index.html only, so the maintenance page,
   whose .app really does end with its own content, is untouched. */
[data-app] .app { padding-bottom: 0; }
[data-app] #app-footer {
  max-width: 520px;
  margin: 0 auto;
  padding: 0 16px 40px;
}

/* The footer comes in with the first screen on load, rather than jumping in after it.
   It is never hidden afterwards, so this runs once: moving between screens animates
   the screen alone and leaves the footer still. */
.screen, #app-footer { animation: screen-in 0.25s ease; }
@keyframes screen-in {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}
/* The quiz already swaps a complete loading shell for a complete question. Moving
   that text-heavy screen while scroll is reset makes the deliberate swap look like
   a shake, so quiz entry is immediate while the other page transitions stay intact. */
#screen-quiz { animation: none; }

/* ===== The row at the top of a screen =====
   Every screen but the home screen opens with a row carrying the way out of it - the
   arrow back home, or the x that leaves a round - and on a long screen (the settings,
   the help, a list of results) that row used to scroll off the top, so leaving meant
   scrolling all the way back up to reach it again. The row sticks to the top of the
   window instead and stays there for as long as the screen is on it.
   top: 20px is .app's own padding above the row, so the row never moves: it sits 20px
   from the top of the window with the page at rest, and it is still 20px from the top
   once the page has scrolled. The negative side margins pull its background out to the
   two edges of .app and the matching padding puts its contents back where they were,
   so what passes behind the row is covered across the full width of the page while
   nothing on the row has shifted.
   z-index stays below the dialog overlay (100), the update bar (60) and the toast
   (50): this is part of the page, not something layered over it. */
.page-top, .quiz-top {
  position: sticky;
  top: 20px;
  z-index: 20;
  margin-left: -16px;
  margin-right: -16px;
  padding: 0 16px 10px;
  background: var(--bg);
}
/* The 20px band above the row. The row cannot cover it with its own background - that
   band is .app's padding, above the row and outside it - so the row paints it: an empty
   box the height of that padding, sitting directly on top of the row and travelling
   with it. It is the page's own colour, so it shows as nothing while the page is at
   rest and hides the text passing behind the row once the page scrolls. */
.page-top::before, .quiz-top::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 100%;
  height: 20px;
  background: var(--bg);
}

h2 { font-size: 1.15rem; font-weight: var(--weight-bold); margin: 26px 0 12px; }

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

/* ===== Home ===== */
.home-header { margin: 10px 0 22px; }
/* The mark and the wordmark share a row and are centred on each other, so the name sits
   level with the logo rather than above it. The pair title is a second row underneath:
   with all three lines stacked beside a 48px logo, the name floated past the logo's top
   edge and looked like a caption that had slipped. */
.home-brand-row {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 8px;
}
.logo-link {
  display: block;
  flex-shrink: 0;
  line-height: 0;
  text-decoration: none;
  border-radius: 18px;
}
.logo-link:focus-visible { border-radius: 18px; }
.logo {
  display: block;
  width: 62px;
  height: 62px;
  border-radius: 18px;
  object-fit: cover;
  flex-shrink: 0;
}
/* min-width lets a long pair name wrap inside the block instead of widening the row
   past the screen. */
.home-titles { min-width: 0; }
/* The wordmark. Roboto is named here and never fetched: the policy in .htaccess and in
   each page's <meta> tag has no font-src, so a url() face would be refused by the
   browser anyway, and NOTICE.md records that no font file is shipped or downloaded. A
   reader who already has Roboto - Android and ChromeOS do - is drawn in it; everyone
   else falls through the stack to a face their system already supplies. The stack is
   deliberately not system-ui: that is what the rest of the page uses, and a wordmark
   that matches its own body text is not a wordmark.
   --weight-bold rather than --weight-heavy on purpose: bold steps 700 to 900 under the
   bold-text setting, a face the font really has, so this rule stays out of the stroke
   list above. In the brand colour, and bigger than the level line - the first, muted
   0.85rem version disappeared beside the heading. Set in capitals since 4 September
   2026, at the operator's request; the markup keeps the lower-case address, so a
   screen reader still hears a domain and a copy still pastes one. The tracking is
   slightly open rather than tight, because capitals set at -0.01em run together. */
.home-brand {
  min-width: 0;
  font-family: Roboto, "Helvetica Neue", Arial, system-ui, sans-serif;
  font-size: 1.4rem;
  font-weight: var(--weight-bold);
  text-transform: uppercase;
  letter-spacing: 0.02em;
  line-height: 1.1;
  color: var(--primary);
  overflow-wrap: anywhere;
}
/* A sentence since 14 September 2026 - "Learn 10 new Spanish words a day", in the
   reader's language - where it was two language names: text-wrap: balance, because the
   catalogs are not the same length and two even lines read as chosen where one long line
   and an orphan reads as an accident. */
.home-titles h1 { font-size: 1.9rem; font-weight: var(--weight-heavy); line-height: 1.15; text-wrap: balance; }

/* Spanish and English is the default pair. The other names sit behind Change rather
   than occupying two grids on the first screen. */
.lang-change-btn {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 8px 12px;
  width: 100%;
  padding: 10px 12px;
  /* Under Start Learning Now since 14 September 2026, where the agreement line sits
     between the two only until the device holds the record: without a margin of its
     own the pill touched the button the moment that line went. */
  margin-top: 12px;
  margin-bottom: 6px;
  background: var(--card);
  border: 1px solid var(--control-border);
  border-radius: var(--radius);
  font-size: 0.95rem;
  font-weight: var(--weight-bold);
  color: var(--text);
  text-align: start;
  /* Raised, since 6 September 2026: the operator asked for "Tap to Change Languages"
     to look more like a button. The row takes what the tiles and the level cards took
     the same day - a solid edge under it in the shadow's tint (--raise-edge, per theme)
     over the soft shadow, and a faint darkening towards the bottom - and the green pill
     inside it takes what Start Learning Now took (see .lang-change-action). A press
     sinks the row onto its edge and hover lifts it; the size never changes. */
  background-image: linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0) 45%, rgba(0, 0, 0, 0.045));
  box-shadow: 0 3px 0 var(--raise-edge), var(--shadow);
  transition: background-color 0.2s, color 0.2s, border-color 0.2s, transform 0.15s, box-shadow 0.15s;
}
.lang-change-btn:active {
  background-color: var(--track);
  transform: translateY(2px);
  box-shadow: 0 1px 0 var(--raise-edge), 0 2px 6px rgba(0, 0, 0, 0.08);
}
.lang-change-btn[aria-expanded="true"] { border-color: var(--primary); }
.lang-change-pair {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.4em;
  min-width: 0;
}
.lang-change-sep { opacity: 0.55; font-weight: var(--weight-bold); }
/* Where the pair came from, when the browser suggested it rather than the visitor
   choosing it. Lighter and smaller than the two language names beside it, because it is a
   note about them and must not read as a third language. flex-wrap on the row above lets
   it drop to its own line on a narrow screen instead of pushing the names out. */
.lang-change-source {
  color: var(--muted);
  font-size: 0.8rem;
  font-weight: var(--weight-normal);
}
/* This used to be plain green words at the end of the row, and it read as a caption of
   the pair beside it rather than the control that changes it — people did not find how to
   change their languages at all. The same word in a filled pill, with a globe in front of
   it, says press me at a glance. */
.lang-change-action {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  padding: 8px 14px;
  border-radius: 999px;
  background: var(--primary);
  color: var(--on-primary);
  font-size: 0.9rem;
  /* Medium, not bold: the whole pill used to be bold, and a bold count inside it was
     bold beside bold — the number did not stand out at all. The filled pill and the
     globe carry the "press me" now; the weight is saved for the count. */
  font-weight: var(--weight-medium);
  /* Raised like Start Learning Now, since 6 September 2026: a light sheen over the
     fill, a solid darker edge under it and a soft drop shadow, in black and white at
     low alpha so both themes' --primary keep their own colour - and a little larger
     than the 6px-by-12px pill it was, because it is the button the operator wanted
     seen. After the background shorthand above, which would otherwise wipe the sheen. */
  background-image: linear-gradient(180deg, rgba(255, 255, 255, 0.22), rgba(255, 255, 255, 0) 55%, rgba(0, 0, 0, 0.08));
  border: 1px solid rgba(0, 0, 0, 0.22);
  box-shadow: 0 3px 0 rgba(0, 0, 0, 0.28), 0 4px 10px rgba(0, 0, 0, 0.16);
  text-shadow: 0 1px 0 rgba(0, 0, 0, 0.12);
  transition: box-shadow 0.2s, transform 0.15s;
}
.lang-change-glyph { font-size: 1em; line-height: 1; }
/* The count after the verb. The separator before it keeps its parent's colour rather
   than the dimmed one the pair's separator wears on the card. */
.lang-change-action .lang-change-sep { opacity: 0.7; }
.lang-change-count {
  font-weight: var(--weight-heavy);
  white-space: nowrap;
  /* Bold and underlined so the number stands out from the verb beside it as the fact
     worth noticing; the offset keeps the line clear of the descenders in "languages". */
  text-decoration: underline;
  text-underline-offset: 0.15em;
}
/* Hover sits behind (hover: hover) for the reason given at .answer-btn: a touch screen
   has no pointer, so it leaves :hover on whatever was last touched. */
@media (hover: hover) {
  .lang-change-btn:hover {
    border-color: var(--primary);
    transform: translateY(-1px);
    box-shadow: 0 4px 0 var(--raise-edge), var(--shadow);
  }
  /* The halo the pill had before, on top of its edge rather than in place of it. */
  .lang-change-btn:hover .lang-change-action {
    box-shadow: 0 3px 0 rgba(0, 0, 0, 0.28), 0 4px 10px rgba(0, 0, 0, 0.16), 0 0 0 3px var(--learn-glow);
  }
  /* After the hover rule, so a mouse press sinks the row rather than lifting it. */
  .lang-change-btn:active {
    transform: translateY(2px);
    box-shadow: 0 1px 0 var(--raise-edge), 0 2px 6px rgba(0, 0, 0, 0.08);
  }
}

/* The language picker dialog: header, tabs and search stay put; the list scrolls.
   max-height is 70vh as the compact picker asks, so the page behind is still visible. */
.lang-picker-dialog {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-modal);
  padding: 16px 14px 8px;
  width: 100%;
  max-width: 400px;
  max-height: 70vh;
  display: flex;
  flex-direction: column;
  text-align: start;
  animation: modal-in 0.22s ease;
  overscroll-behavior: contain;
  overflow: hidden;
  isolation: isolate;
  position: relative;
}
/* The dialog takes the focus itself when it opens on a touch device, and a programmatic
   focus on a whole box has nothing useful to draw a ring around. */
.lang-picker-dialog:focus { outline: none; }
.lang-picker-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 12px;
  flex-shrink: 0;
}
.lang-picker-title {
  font-size: 1.05rem;
  font-weight: var(--weight-bold);
  margin: 0;
  line-height: 1.2;
}
.lang-picker-close {
  width: 36px;
  height: 36px;
  border-radius: 12px;
  background: var(--bg);
  border: 1px solid var(--control-border);
  font-size: 1rem;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--muted);
  transition: border-color 0.15s, color 0.15s;
}
.lang-picker-close:hover { border-color: var(--primary); color: var(--text); }
.lang-picker-tabs {
  display: flex;
  gap: 4px;
  background: var(--bg);
  border: 1px solid var(--control-border);
  border-radius: var(--radius-btn);
  padding: 4px;
  margin-bottom: 10px;
  flex-shrink: 0;
}
.lang-picker-tab {
  flex: 1;
  padding: 9px 8px;
  border-radius: 12px;
  font-size: 0.88rem;
  font-weight: var(--weight-bold);
  color: var(--text);
  transition: background 0.2s, color 0.2s;
}
.lang-picker-tab[aria-selected="true"] {
  background: var(--primary);
  color: var(--on-primary);
}
.lang-picker-search {
  width: 100%;
  padding: 12px 14px;
  margin-bottom: 8px;
  border-radius: var(--radius-btn);
  background: var(--bg);
  border: 1px solid var(--control-border);
  color: var(--text);
  font-size: 1rem;
  font-family: inherit;
  font-weight: inherit;
  flex-shrink: 0;
}
.lang-picker-search::placeholder { color: var(--muted); }
.lang-picker-search:focus { border-color: var(--primary); outline: none; }
.lang-picker-search:focus-visible {
  outline: 3px solid var(--primary);
  outline-offset: 2px;
}
.lang-picker-list {
  flex: 1 1 auto;
  min-height: 140px;
  overflow-y: auto;
  overscroll-behavior: contain;
  margin: 0 -6px;
  padding: 0 6px 8px;
}
.lang-picker-heading {
  color: var(--muted);
  font-size: 0.72rem;
  font-weight: var(--weight-bold);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  margin: 10px 6px 4px;
}
/* Only the heading of the first section loses the 10px above it. Each heading is now
   the first child of its own role="group", so :first-child alone would match every one
   of them and lift the whole list; the group has to be the first thing in the list.
   When English is pinned it is that first thing, and "Recent" keeps its 10px - which is
   what the flat markup did too. */
.lang-picker-list > .lang-picker-group:first-child .lang-picker-heading { margin-top: 2px; }
.lang-picker-option {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 9px 10px;
  border-radius: 12px;
  text-align: start;
  color: var(--text);
  transition: background 0.15s;
}
.lang-picker-option:hover { background: var(--bg); }
.lang-picker-option.is-active {
  background: var(--badge-green-bg);
}
.lang-picker-names {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 1px;
  flex: 1;
  min-width: 0;
}
.lang-picker-native {
  font-weight: var(--weight-bold);
  font-size: 0.98rem;
  line-height: 1.25;
  unicode-bidi: isolate;
  max-width: 100%;
}
.lang-picker-english {
  color: var(--muted);
  font-size: 0.78rem;
  font-weight: var(--weight-normal);
  line-height: 1.3;
  unicode-bidi: isolate;
  max-width: 100%;
}
.lang-picker-check {
  flex-shrink: 0;
  width: 1.15em;
  color: var(--primary);
  font-weight: var(--weight-heavy);
  font-size: 1.05rem;
  text-align: center;
}
.lang-picker-empty {
  color: var(--muted);
  text-align: center;
  padding: 32px 12px;
  font-size: 0.95rem;
}
.lang-picker-dialog.is-busy .lang-picker-option {
  pointer-events: none;
  opacity: 0.55;
}

.direction-toggle {
  display: flex;
  gap: 6px;
  background: var(--card);
  border: 1px solid var(--control-border);
  border-radius: var(--radius);
  padding: 6px;
  margin-bottom: 6px;
}
.dir-btn {
  flex: 1;
  /* The direction row holds three choices — the pair each way round and the mixed
     round — and on a narrow phone three language names do not fit side by side.
     minmax(0, …) through flex-basis lets a button shrink below its content, and the
     names wrap inside it instead of pushing the row wider than the screen. */
  flex-basis: 0;
  min-width: 0;
  padding: 13px 6px;
  border-radius: 14px;
  font-size: 1rem;
  font-weight: var(--weight-bold);
  color: var(--text);
  overflow-wrap: anywhere;
  transition: background 0.2s, color 0.2s;
}
.dir-btn.active { background: var(--primary); color: var(--on-primary); }

.section-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  margin-top: 26px;
}
.section-head h2 { margin: 0 0 12px; }
.section-head .muted { font-size: 0.85rem; }

/* minmax(0, 1fr) and not 1fr: the minimum of a 1fr track is its content's min-content
   width, so a single long word — "Intermediate", once a reader's own letter-spacing is
   applied (WCAG 1.4.12), or any longer word a future level name carries — widens the
   track past the column and scrolls the whole page sideways at 320px. Capping the
   minimum at 0 makes the track share the container and never exceed it. The same
   applies to every grid below that holds text. */
/* One row of three since 6 September 2026, when the Mix card went at the operator's
   request and the three that remain were asked to fit one line. Smaller than the two
   rows of two were - a 32px badge over the name and the line under it, rather than a
   40px badge beside them - because a third of a phone's width is about 90px, and
   "Intermediate vocabulary" beside a badge does not fit in it; stacked, it wraps into
   two short lines under the name, and the one row is shorter than the two rows were. */
.levels { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
.level-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  width: 100%;
  padding: 10px 6px;
  background: var(--card);
  border: 1px solid var(--control-border);
  border-radius: var(--radius);
  /* Raised, since 6 September 2026, like the tiles under "More in the app" and for the
     same request: the operator asked for the cards to look like buttons. The solid edge
     in the shadow's tint (--raise-edge, per theme) over the soft shadow, and the faint
     darkening towards the bottom, are the tiles' exactly, so the two grids read as one
     kind of control. */
  background-image: linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0) 45%, rgba(0, 0, 0, 0.045));
  box-shadow: 0 3px 0 var(--raise-edge), var(--shadow);
  text-align: center;
  transition: background-color 0.15s, border-color 0.15s, transform 0.15s, box-shadow 0.15s;
}
/* Press feedback must not resize the card just before the screen changes; that scale
   was perceived as a last-frame shake when a level opened. Sinking the card onto its
   edge moves the whole card two pixels and changes no size, which the eye reads as the
   press it is. The held background stays. */
.level-card:active {
  background: var(--track);
  transform: translateY(2px);
  box-shadow: 0 1px 0 var(--raise-edge), 0 2px 6px rgba(0, 0, 0, 0.08);
}
/* Hover sits behind (hover: hover) for the reason given at .answer-btn: a touch screen
   has no pointer, and would leave the last card pressed standing a pixel higher than
   the two beside it. Before the active rule, so that a mouse press sinks rather than
   lifts. */
@media (hover: hover) {
  .level-card:hover {
    border-color: var(--primary);
    transform: translateY(-1px);
    box-shadow: 0 4px 0 var(--raise-edge), var(--shadow);
  }
  .level-card:active {
    transform: translateY(2px);
    box-shadow: 0 1px 0 var(--raise-edge), 0 2px 6px rgba(0, 0, 0, 0.08);
  }
}
/* Placeholders in the HTML, shown until renderHome replaces them. They must look like
   the live cards — a disabled fade would flash as the script arrives. */
.level-card:disabled,
.dir-btn:disabled {
  opacity: 1;
  cursor: default;
  pointer-events: none;
}
.level-card:disabled { color: inherit; }
.dir-btn:disabled { color: var(--text); }
.dir-btn.active:disabled { color: var(--on-primary); background: var(--primary); }
.level-card:disabled:hover { border-color: var(--control-border); }
.level-card:disabled:active { transform: none; box-shadow: 0 3px 0 var(--raise-edge), var(--shadow); }
.level-num {
  width: 32px;
  height: 32px;
  border-radius: 10px;
  font-size: 1rem;
  font-weight: var(--weight-heavy);
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
.level-num-1 { background: var(--badge-green-bg); color: var(--badge-green); }
.level-num-2 { background: var(--badge-amber-bg); color: var(--badge-amber); }
.level-num-3 { background: var(--badge-blue-bg); color: var(--badge-blue); }
.level-info { width: 100%; min-width: 0; }
/* Both are spans and not a span over a div: a <div> inside a <button> is flow content
   in a phrasing-only element, which is a parse error and puts the description outside
   the button in some parsers. display:block gives the two lines the stacking the div
   used to give them, so the card looks exactly as it did. */
.level-name { display: block; font-size: 0.95rem; font-weight: var(--weight-heavy); }
.level-desc { display: block; color: var(--muted); font-size: 0.72rem; line-height: 1.3; margin-top: 2px; }
/* The chosen card, since the three became the Level setting on the Customize screen on
   6 September 2026: a ring in the primary colour inside the border and the name in the
   same ink, so the choice reads without anything moving - the .chip.active fill would
   swallow the three coloured badges and the muted line under each name. The ring is
   thickness as well as colour, so an eye that cannot tell the green from the border
   still sees which card is chosen; aria-checked says it to everyone else. */
.level-card.active {
  border-color: var(--primary);
  box-shadow: 0 3px 0 var(--raise-edge), var(--shadow), inset 0 0 0 2px var(--primary);
}
.level-card.active .level-name { color: var(--primary); }

/* The one control on this screen that starts anything, since the level cards moved to
   the Customize screen on 6 September 2026 (until then it stood full width above them).
   It is full width under the pair, and it reads as one line ending in the level it
   will start.
   Until 4 September 2026 the level sat small beside a "Start Now" name, held apart by a
   faded dot; the dash that replaced the dot is part of the line and is set in the same
   ink and size as the rest, so both halves share the button's own size and may wrap
   onto a second line on a narrow phone rather than shrink.
   The top margin is the section's own 16px (#screen-home .section-head), not the 10px
   it had under the "Choose a level" heading: since 5 September 2026 that heading sits
   over the cards, and this button is the first thing after the exercise row, so it
   carries the gap that separates one section of the home screen from the next. */
/* .btn.start-now, not .start-now: .btn's own `display: block` sits later in the file and
   has won this tie for as long as the rule existed, so the call-to-action line never
   reached the row of its own that its flex-basis: 100% was written to give it - it ran on
   inline after the level and the button read as one dense paragraph instead of an
   invitation. Anything here that .btn also sets has to be written at this specificity.
   Since 11 September 2026 it is also deliberately the largest control on the home screen,
   at the operator's request: it is the only thing on that screen that starts a round, so
   it is a step up in size over every other button, with room on both sides for the arrow
   so that widening one side cannot push the centred text off centre.
   The same tie is why .btn-primary's background shorthand and the .btn hover rules,
   both later still, are answered here too. */
.btn.start-now {
  margin-top: 16px;
  position: relative;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: center;
  column-gap: 0.3em;
  row-gap: 5px;
  /* clamp rather than a second entry in the narrow-screen block below: the step up in
     size has to give itself back on a small phone, where the longest interface languages
     (German, Kazakh) would otherwise take the name onto a third line and the offer onto a
     fourth. One rule that follows the width beats two values that drift apart. */
  padding: 18px clamp(18px, 7vw, 32px);
  font-size: clamp(1.04rem, 4.56vw, 1.16rem);
  /* Raised, since 6 September 2026: the operator said the flat green rectangle did not
     look like a button. A light sheen over the fill, a solid darker edge under it and a
     soft drop shadow make it stand off the page; a press sinks it onto that edge instead
     of the .btn scale, and hover lifts it a little further. The sheen and the edge are
     black and white at low alpha, so both themes' --primary keep their own colour. */
  background-image: linear-gradient(180deg, rgba(255, 255, 255, 0.22), rgba(255, 255, 255, 0) 55%, rgba(0, 0, 0, 0.08));
  border: 1px solid rgba(0, 0, 0, 0.22);
  box-shadow: 0 4px 0 rgba(0, 0, 0, 0.28), 0 6px 16px rgba(0, 0, 0, 0.18);
  text-shadow: 0 1px 0 rgba(0, 0, 0, 0.12);
}
.btn.start-now:not(:disabled):active {
  /* animation: none here and on hover because the invitation below animates box-shadow,
     and an animation beats every ordinary declaration of the property it runs on - the
     press and the pointer would have had no shadow of their own while it played. */
  animation: none;
  transform: translateY(3px);
  box-shadow: 0 1px 0 rgba(0, 0, 0, 0.28), 0 2px 6px rgba(0, 0, 0, 0.14);
}
/* The invitation: a soft halo in the button's own green that breathes out three times as
   the button goes live, and then rests. Three, not forever - the eye has to be brought to
   the one control that starts a round, and after that a mark that never stops moving is
   just noise on a screen somebody is reading. Since 14 September 2026 the button is
   pressable from the first paint, so the halo plays as the page settles; :not(:disabled)
   is kept so that a button some later state disables does not breathe at nobody, not
   because anything disables it today. The blanket prefers-reduced-motion rule runs it
   once at 0.01ms, which is why 0% and 100% are the resting shadow: parked, it is the
   button exactly as it looks with no animation at all. */
@keyframes start-now-call {
  0%, 100% { box-shadow: 0 4px 0 rgba(0, 0, 0, 0.28), 0 6px 16px rgba(0, 0, 0, 0.18), 0 0 0 0 var(--learn-glow); }
  55% { box-shadow: 0 4px 0 rgba(0, 0, 0, 0.28), 0 6px 16px rgba(0, 0, 0, 0.18), 0 0 0 9px var(--learn-glow); }
}
.btn.start-now:not(:disabled) { animation: start-now-call 2.2s ease-in-out 3; }
/* The go-arrow. The button said "press me" only in words, and the arrow is the part a
   reader takes in before reading any of them. It is absolutely positioned rather than a
   fourth flex item because the call-to-action line takes a whole row of its own, so a
   trailing item would land on a third row underneath it instead of beside the name.
   ::before, not ::after: ::after already belongs to the loading sweep further down.
   Drawn out of borders rather than written as "\25B6", because CSS generated content
   counts towards a control's accessible name: the glyph put "black right-pointing
   triangle" in front of everything this button says. The transparent top and bottom with
   a solid border-inline-start also turn themselves round - in a right-to-left interface
   that solid side is the right one, so the triangle points left without a second rule. */
.btn.start-now::before {
  content: "";
  position: absolute;
  inset-inline-end: 14px;
  top: 50%;
  width: 0;
  height: 0;
  border-top: 7px solid transparent;
  border-bottom: 7px solid transparent;
  border-inline-start: 11px solid currentColor;
  opacity: 0.62;
  transform: translateY(-50%);
  pointer-events: none;
}
@media (hover: hover) {
  /* Keeps the raised edge under the .btn-primary glow ring, which would otherwise
     replace it and flatten the button the moment the pointer arrives. */
  .btn.start-now:not(:disabled):hover {
    animation: none;
    transform: translateY(-2px);
    box-shadow: 0 6px 0 rgba(0, 0, 0, 0.28), 0 10px 20px rgba(0, 0, 0, 0.2), 0 0 0 3px var(--learn-glow);
  }
}
.start-now-level { font-weight: var(--weight-bold); }
/* The call-to-action line under the name, since 6 September 2026. flex-basis: 100% puts
   it on a row of its own under the two halves whatever they wrapped to. The row above it
   is held off by the flex container's row-gap now, not by a margin here, because the
   margin was doing nothing while .btn's display: block kept the button out of flex
   layout altogether.
   Since 12 September 2026 it is a band rather than a caption, at the operator's request:
   at 0.7em, --weight-medium and 0.9 opacity it was the quietest thing on the home screen
   while being the line that says what pressing costs and gains - free, no sign-up, this
   many words - and a reader scanning the button read the name, the level and nothing
   else. It now carries its own inset strip, and it stays the button's own ink at full
   strength rather than a second colour, so the green button keeps one voice.
   The strip is black at low alpha for the reason the sheen and the edge above are: it
   darkens whichever --primary the theme chose instead of painting a colour over it, so
   the near-white ink on the light theme's deep green gains contrast and the near-black
   ink on the dark theme's pale green keeps its own. A band in currentColor would have
   moved the ground towards the text and cost contrast in both.
   Still a step below the name in size, because a button whose second line shouts louder
   than its first reads as two buttons - 0.84em, where a single line of the longest
   wording fits from about 500px up and a phone takes two. Measured, not guessed: the
   Hebrew line wants 302px of the 290px a 420px phone has, so no size that is still
   prominent keeps it on one line there, and the old 0.7em wrapped on a 360px screen
   too. text-wrap: balance is what makes the two lines look chosen. */
.start-now-cta {
  flex-basis: 100%;
  padding: 6px 10px;
  border-radius: 10px;
  background: rgba(0, 0, 0, 0.17);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.14);
  font-size: 0.84em;
  font-weight: var(--weight-bold);
  line-height: 1.35;
  letter-spacing: 0.01em;
  opacity: 1;
  text-wrap: balance;
}
/* The line that makes a press an agreement, under Start Learning Now and under the
   example's Continue button - since 14 September 2026, in place of the tick box that
   stood under the button from 1 September. It has to be read before the press: 0.9rem in
   the page's own ink rather than --muted, centred under the button it names, with the
   link in the accent green, bold and underlined, because a sentence somebody is to be
   held to cannot be the palest thing on the screen - the sign-in-wrap cases turn on
   exactly that. Nothing here moves: the button above it already breathes, and a line
   that has to be read should stand still. */
.terms-accept {
  margin: 8px auto 4px;
  max-width: 34rem;
  text-align: center;
  font-size: 0.9rem;
  line-height: 1.45;
  color: var(--text);
}
/* The link holds together: "terms of" at the end of one line and "use." at the start of
   the next read as two things, and the sentence under the example's Continue button was
   long enough to break exactly there on a 570px window. */
.terms-accept a { color: var(--primary); font-weight: var(--weight-bold); text-decoration: underline; white-space: nowrap; }
/* Inside the example card the line sits under the Continue button it names. */
.home-example-accept { margin: 8px 0 0; }
/* The dialog every other gated control opens says the same sentence of its own button. */
#terms-overlay .modal-text a { color: var(--primary); font-weight: var(--weight-bold); text-decoration: underline; }

/* A press that starts a round now waits for the whole response before any screen
   changes, so the control that was pressed has to carry that wait - otherwise the
   first round of a session looks like a press that missed.

   The mark is the same shimmer the quiz's loading shell already uses, swept across
   the whole card: one loading language everywhere, and nothing rotates - the earlier
   spinning arc over the badge drew the eye like a stuck hourglass for as long as the
   round was in flight. The card also keeps the background :active already gives it,
   so the press simply reads as held rather than missed. The overlay is positioned, so
   appearing costs no layout: it cannot itself move the screen it exists to keep
   still, and overflow:hidden keeps the sweep inside the card's rounded corner. */
.tile[aria-busy="true"],
.learn-cta[aria-busy="true"] {
  position: relative;
  overflow: hidden;
  background: var(--track);
}
/* Start now carries the same sweep but keeps its own fill: --track under --on-primary
   text is the one place this mark would have taken the contrast down with it. */
.start-now[aria-busy="true"] { position: relative; overflow: hidden; }
.tile[aria-busy="true"]::after,
.learn-cta[aria-busy="true"]::after,
.start-now[aria-busy="true"]::after {
  content: "";
  position: absolute;
  inset: 0;
  transform: translateX(-105%);
  background: linear-gradient(90deg, transparent, var(--skeleton-shine), transparent);
  animation: quiz-shell-shimmer 1.25s ease-in-out infinite;
  pointer-events: none;
}
/* The blanket reduced-motion rule above runs the sweep once and parks it off-screen,
   which is exactly right: the held background alone carries the wait and never moves,
   so no extra reduced-motion rule is needed here. */

/* The first word stays on the home screen, and since 14 September 2026 it is the first
   thing under the heading and answered before any agreement. Keep long words and mixed
   writing directions within the compact card. */
.home-example {
  margin-top: 16px;
  padding: 16px;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.home-example h2 {
  margin: 0;
  font-size: 1.05rem;
  line-height: 1.4;
}
.home-example-status,
.home-example-pair {
  color: var(--muted);
  font-size: 0.85rem;
  line-height: 1.5;
  overflow-wrap: anywhere;
}
.home-example-status { margin: 8px 0 0; }
.home-example-pair { margin: 6px 0 0; }
/* The word and the speaker beside it, and the same again for the verdict line.
   Wrapping is allowed: a long word in a narrow card takes the whole line and the
   speaker drops under it rather than squeezing the word into a column. */
.home-example-ask,
.home-example-verdict {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 10px;
}
/* The answered card's verdict. It used to be a bare sentence with the speaker trailing
   it like one more word of it, and on a narrow card the speaker fell to a line of its
   own under nothing in particular. It is a framed row instead, on the flashcard's
   pattern: the frame and the verdict word carry the outcome, the answer keeps the
   page's own ink, and the speaker sits at the end of the row where a control belongs.
   The wording wraps inside its paragraph rather than pushing the speaker off the row. */
.home-example-verdict {
  justify-content: flex-start;
  margin-top: 12px;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: 12px;
}
.home-example-verdict.correct { border-color: var(--success); }
.home-example-verdict.wrong { border-color: var(--error); }
.home-example-verdict.correct .home-example-outcome { color: var(--success); }
.home-example-verdict.wrong .home-example-outcome { color: var(--error); }
/* The row does not wrap: the wording is what wraps, inside its own paragraph. Left to
   wrap, the paragraph grew to the whole row and pushed the speaker onto a line of its
   own under the middle of nothing — which is how it looked before this row existed. */
.home-example-verdict { flex-wrap: nowrap; }
.home-example-verdict .home-example-feedback { flex: 1 1 auto; min-width: 0; margin: 0; }
.home-example-verdict .home-example-speak-slot { margin-inline-start: auto; }
.home-example-outcome { font-weight: var(--weight-bold); white-space: nowrap; }
/* What the answer adds - the verdict, Continue and its line - is 165px tall, and it
   arrived in one frame: every control under the card, Start Learning Now first, jumped
   that far down the screen the moment an answer was pressed, which the operator saw as
   the whole page jumping. So it stands in a grid of one fr track that is 0fr until the
   answer and 1fr after it, and the growth is a transition: what is under the card
   slides over a quarter of a second rather than leaping. The "By answering" line above
   it is the same kind of row, closing as this one opens. Two grids of one track each,
   not one grid of two: Chromium interpolates 0fr to 1fr but not "1fr 0fr" to
   "0fr 1fr", measured on 14 September 2026, and closing the line with display: none
   instead left a one-frame hop of 29px upwards before the slide down. Nothing scrolls -
   the pressed answer stays where it was. Each transition is declared on the state it is
   wanted in and nowhere else: the answered row opening, and the line closing only while
   an answered row is opening beside it (:has), so the question's first paint and a
   replacement question are as instant as they were. A closed row clips its content and,
   once it has finished closing, takes it out of the accessibility tree with a delayed
   visibility; the side padding leaves the button's halo room, because a clipped halo is
   a square one. The blanket reduced-motion rule makes the slide instant, which is the
   old behaviour, and so does a browser that cannot interpolate a track size. */
.home-example-agree-row,
.home-example-after { display: grid; grid-template-rows: 0fr; }
.home-example-agree-row.open,
.home-example-after.open { grid-template-rows: 1fr; }
.home-example-after.open { transition: grid-template-rows 0.28s ease-out; }
.home-example:has(.home-example-after.open) .home-example-agree-row { transition: grid-template-rows 0.28s ease-out; }
.home-example-row-inner {
  overflow: hidden;
  min-height: 0;
  padding: 0 12px;
  margin: 0 -12px;
  visibility: hidden;
  transition: visibility 0s linear 0.28s;
}
.home-example-agree-row.open > .home-example-row-inner,
.home-example-after.open > .home-example-row-inner { visibility: visible; transition: none; }
.home-example-answer { font-weight: var(--weight-bold); }
.home-example-speak-slot { display: inline-flex; }
/* The speaker itself, on .answer-speak's pattern: a plain round control beside the
   word, muted until it is hovered, and never carrying a verdict colour of its own. */
.home-example-speak {
  flex: 0 0 auto;
  min-width: 44px;
  min-height: 44px;
  padding: 0 10px;
  border: 1px solid var(--control-border);
  border-radius: var(--radius-btn);
  background: var(--bg);
  color: var(--muted);
  font-size: 1.1rem;
  line-height: 1;
  transition: border-color 0.2s, transform 0.15s;
}
.home-example-speak .speak-glyph { opacity: 1; }
.home-example-word {
  margin: 10px 0 14px;
  cursor: pointer;
  font-size: clamp(1.35rem, 5vw, 1.75rem);
  font-weight: var(--weight-heavy);
  line-height: 1.4;
  text-align: center;
  overflow-wrap: anywhere;
}
.home-example-choices {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 8px;
}
.home-example-choice {
  /* .answer-btn's three columns, for .answer-btn's reason: the ✓ / ✗ after a press
     goes in a column of its own rather than against the word — appended to the label
     it arrived with no space at all ("✗Deprivation"), and on the side the page read
     from rather than the side the answer does. The empty third column keeps the word
     centred, so nothing shifts at the moment of answering. */
  display: grid;
  grid-template-columns: 1em minmax(0, 1fr) 1em;
  align-items: center;
  column-gap: 4px;
  min-width: 0;
  min-height: 48px;
  padding: 10px 8px;
  border: 1px solid var(--control-border);
  border-radius: 12px;
  background: var(--bg);
  color: var(--text);
  font-size: 1rem;
  font-weight: var(--weight-medium);
  line-height: 1.4;
  overflow-wrap: anywhere;
  text-align: center;
}
.home-example-choice-text {
  grid-column: 2;
  grid-row: 1;
  min-width: 0;
  /* Two columns of these buttons hold half a phone's width each, and the mark's column
     takes a little more of it. A long answer is broken where its own language allows
     one — the button carries the answer's lang, so the browser has what it needs —
     rather than wherever the line happens to run out ("Grandmoth / er"). */
  hyphens: auto;
  overflow-wrap: break-word;
}
/* Both rows named, because the mark is appended after the word: auto-placement has
   already passed column 2 by then, and a bare grid-column: 1 put the tick on a second
   row under the answer instead of beside it. */
.home-example-mark {
  grid-column: 1;
  grid-row: 1;
  justify-self: start;
  line-height: 1;
}
/* The word this question existed to teach, underlined on .answer-btn.correct's
   pattern — on the span, so the mark in the first column keeps its own weight. */
.home-example-choice.correct .home-example-choice-text {
  text-decoration: underline;
  text-decoration-thickness: 0.07em;
  text-underline-offset: 0.18em;
  text-decoration-color: var(--verdict-ink);
}
.home-example-choice[aria-disabled="true"] { cursor: not-allowed; }
.home-example-choice:disabled { cursor: default; opacity: 1; }
.home-example-choice.selected { font-weight: var(--weight-heavy); border-width: 2px; }
.home-example-choice.correct {
  background: var(--success-bg);
  border-color: var(--success);
  color: var(--verdict-ink);
}
.home-example-choice.wrong {
  background: var(--error-bg);
  border-color: var(--error);
  color: var(--verdict-ink);
}
.home-example-feedback {
  margin: 12px 0 0;
  font-size: 0.95rem;
  line-height: 1.5;
  font-weight: var(--weight-medium);
  overflow-wrap: anywhere;
}
.home-example-retry { margin-top: 12px; min-height: 44px; }
/* The button that takes the card away, since 15 September 2026. Written small and
   quiet on purpose: "Hide the example question" is the least important thing on a card
   whose whole job is the question, so it is a line of text at the end of the card
   rather than a full-width control competing with Continue — its width is its text, it
   sits at the end of the reading direction (a block of its text's width with an auto
   start margin, so the card itself stays the block flow it was), and it keeps the 44px
   a finger needs. Nothing stands in for the hidden card: the way back is the Customize
   screen. */
.btn.home-example-hide {
  width: fit-content;
  max-width: 100%;
  min-height: 44px;
  margin-top: 12px;
  margin-inline-start: auto;
  padding: 10px 14px;
  font-size: 0.85rem;
  font-weight: var(--weight-medium);
  color: var(--muted);
  background-image: none;
  box-shadow: none;
}
/* The button under the answered example, since 14 September 2026: the operator asked
   that once the first question has been answered there be an obvious way on, and that
   the way on be impossible to mistake for anything else on the card. So it is written
   at .btn.home-example-continue, for the same reason .btn.start-now is: .btn's own
   display, padding and font sit later in this file and win a plain-class tie.
   It is the loudest thing in a deliberately quiet card - the choices are outline
   buttons and the retry is a ghost - and it borrows the raise Start Learning Now wears
   (a light sheen, a solid darker edge under the fill, a soft drop shadow), one step
   smaller than that button so the card cannot out-shout the screen's own call to
   action. The sheen and the edge are black and white at low alpha, so both themes'
   --primary keep their own colour. */
.btn.home-example-continue {
  margin-top: 14px;
  min-height: 48px;
  padding: 14px 18px;
  font-size: 1.02rem;
  background-image: linear-gradient(180deg, rgba(255, 255, 255, 0.22), rgba(255, 255, 255, 0) 55%, rgba(0, 0, 0, 0.08));
  border: 1px solid rgba(0, 0, 0, 0.22);
  box-shadow: 0 3px 0 rgba(0, 0, 0, 0.28), 0 5px 13px rgba(0, 0, 0, 0.16);
  text-shadow: 0 1px 0 rgba(0, 0, 0, 0.12);
}
/* The same halo Start Learning Now breathes, and for the same reason: the eye has to be
   brought to the one control that carries on from here, and then left alone. Three
   breaths, then rest. 0% and 100% are the resting shadow, so the blanket
   prefers-reduced-motion rule - which runs every animation once at 0.01ms - parks it
   looking exactly as it does with no animation at all. */
@keyframes home-example-continue-call {
  0%, 100% { box-shadow: 0 3px 0 rgba(0, 0, 0, 0.28), 0 5px 13px rgba(0, 0, 0, 0.16), 0 0 0 0 var(--learn-glow); }
  55% { box-shadow: 0 3px 0 rgba(0, 0, 0, 0.28), 0 5px 13px rgba(0, 0, 0, 0.16), 0 0 0 8px var(--learn-glow); }
}
.btn.home-example-continue:not(:disabled) { animation: home-example-continue-call 2.2s ease-in-out 3; }
.btn.home-example-continue:not(:disabled):active {
  /* animation: none here and on hover for the reason given at .btn.start-now: an
     animation beats every ordinary declaration of the property it runs on, so the press
     and the pointer would have had no shadow of their own while the halo played. */
  animation: none;
  transform: translateY(2px);
  box-shadow: 0 1px 0 rgba(0, 0, 0, 0.28), 0 2px 6px rgba(0, 0, 0, 0.14);
}
@media (hover: hover) {
  /* Keeps the raised edge under .btn-primary's glow ring, which would otherwise replace
     it and flatten the button the moment the pointer arrived. */
  .btn.home-example-continue:not(:disabled):hover {
    animation: none;
    transform: translateY(-2px);
    box-shadow: 0 5px 0 rgba(0, 0, 0, 0.28), 0 9px 18px rgba(0, 0, 0, 0.2), 0 0 0 3px var(--learn-glow);
  }
}
.home-example :is(.home-example-status, .home-example-pair, .home-example-word,
  .home-example-choices, .home-example-feedback):empty { display: none; }

/* The card's shape while the first word is on its way. It used to hold the heading and
   the status line alone, 86px, and grow to about 330px when the question arrived - so on
   every return to this screen Start Learning Now and everything under it stood high for
   up to a second and then dropped 240px, which the operator saw as the page not loading
   properly. The pieces are drawn at the real ones' sizes: the word line at the speaker's
   44px with the word's own margins, four boxes at .home-example-choice's 48px in its
   grid, a line where the "By answering" sentence goes and one for the speech notice under
   it. Measured on 15 September 2026 that puts the waiting card
   within a few percent of the ready one on any width; app.js then holds it at the exact
   height of the last question shown, once it has one to remember. The pieces share
   .quiz-loading-shape, so the quiz shell's shimmer and its reduced-motion rule apply. */
.home-example-skeleton { display: none; }
.home-example[aria-busy="true"] .home-example-skeleton { display: block; }
.home-example-skeleton-word {
  display: block;
  width: min(60%, 240px);
  height: 44px;
  margin: 10px auto 14px;
  border-radius: 99px;
}
.home-example-skeleton-choices {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 8px;
}
.home-example-skeleton-choices > span {
  display: block;
  min-height: 48px;
  border: 1px solid var(--border);
  border-radius: 12px;
}
.home-example-skeleton-line {
  display: block;
  width: min(70%, 300px);
  height: 14px;
  margin: 14px auto 6px;
  border-radius: 99px;
}
.home-example-skeleton-notice { width: min(85%, 360px); margin: 12px auto 4px; }

/* A native summary provides its own expanded state and keyboard behavior. The
   original secondary controls stay together, including the install reply panel. */
.home-more-options { margin-top: 16px; }
.home-more-options > summary {
  min-height: 48px;
  padding: 13px 16px;
  border: 1px solid var(--control-border);
  border-radius: var(--radius-btn);
  background: var(--card);
  color: var(--text);
  font-size: 1rem;
  font-weight: var(--weight-bold);
  line-height: 1.5;
  overflow-wrap: anywhere;
  cursor: pointer;
}
.home-more-options > summary::marker { color: var(--primary); }
.home-more-options-content { padding-top: 4px; }
@media (hover: hover) {
  .home-example-choice:not(:disabled):not([aria-disabled="true"]):hover,
  .home-more-options > summary:hover { background: var(--btn-hover-bg); }
  .home-example-speak:hover { border-color: var(--primary); color: var(--primary); }
}
@media (max-width: 359px) {
  .home-example { padding: 12px; }
  .home-example-choice { padding: 9px 7px; }
}

.more-title { margin-top: 16px; }
/* Tiles: two columns on a narrow screen, three from 480px up. There is no wider
   breakpoint, and they do not go on one line: .app is capped at 520px however wide the
   window is, and packing them across that width leaves each tile narrower than its own
   label ("Favourites", "Statistics"), which then wraps and makes the row taller than
   the two rows it was meant to replace. There are eight tiles since 6 September 2026:
   Advanced Learning left the grid for its own band under the level cards on
   5 September and the space game took its cell, later that day Settings left it too,
   as the Customize band under that one, and on 6 September Install app left it for a
   band between those two. Eight fill four rows of two exactly, and three columns with
   two on the last row. */
.tiles { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
@media (min-width: 480px) {
  .tiles { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
.tile {
  background: var(--card);
  border: 1px solid var(--control-border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 18px 8px 14px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  /* Raised, since 6 September 2026: the operator asked for the tiles to look like
     buttons, as they had for Start Learning Now and the three bands' pills. A card has
     no fill to put a sheen on, so the depth is a solid edge under it in the shadow's
     tint (--raise-edge, per theme) over the soft shadow it already had, and a faint
     darkening towards the bottom of the card. The press sinks the tile onto that edge
     rather than shrinking it: a last-frame scale read as a shake, as the level cards
     found. */
  background-image: linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0) 45%, rgba(0, 0, 0, 0.045));
  box-shadow: 0 3px 0 var(--raise-edge), var(--shadow);
  transition: transform 0.15s, border-color 0.15s, box-shadow 0.15s;
}
.tile:active {
  transform: translateY(2px);
  box-shadow: 0 1px 0 var(--raise-edge), 0 2px 6px rgba(0, 0, 0, 0.08);
}
/* Hover sits behind (hover: hover) for the reason given at .answer-btn: a touch screen
   has no pointer, and would leave the last tile pressed standing a pixel higher than
   its neighbours. The hover colour takes the border, but not the edge that says which
   tile this is: losing the mark on the one tile the pointer is over is exactly
   backwards. Before the active rule, so that a mouse press sinks rather than lifts. */
@media (hover: hover) {
  .tile:hover {
    border-color: var(--primary);
    border-inline-start-color: var(--tile-accent);
    transform: translateY(-1px);
    box-shadow: 0 4px 0 var(--raise-edge), var(--shadow);
  }
  .tile:active {
    transform: translateY(2px);
    box-shadow: 0 1px 0 var(--raise-edge), 0 2px 6px rgba(0, 0, 0, 0.08);
  }
}
/* Six tiles are buttons and two of them (FAQ and Report) are links, because those two
   leave the app - for the questions page and for the contact clause on the legal page.
   They take back the two things a link carries and a button does not - the underline and
   the visited colour - and are otherwise the same tile. */
a.tile { color: inherit; text-decoration: none; }
.tile-icon {
  width: 46px;
  height: 46px;
  border-radius: 14px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.3rem;
  margin-bottom: 4px;
}
.tile-icon-fav { background: var(--badge-green-bg); color: var(--badge-green); }
.tile-icon-improve { background: var(--badge-orange-bg); color: var(--badge-orange); }
.tile-icon-mine { background: var(--badge-purple-bg); color: var(--badge-purple); }
/* Blue, and a tint like the other nine rather than the solid accent fill it used to be.
   The fill was the same green as --badge-green next to it, which cost nothing while the
   colour lived in a 32px badge and everything once it ran down the edge of the row: in
   the dark theme --primary and --badge-green are the same hex, so Favourites and
   Statistics would have carried an identical bar. Blue also belongs to the chart. */
.tile-icon-stats { background: var(--badge-blue-bg); color: var(--badge-blue); }
.tile-icon-set { background: var(--badge-green-bg); color: var(--badge-green); }
.tile-icon-report { background: var(--badge-orange-bg); color: var(--badge-orange); }
/* Purple, so the two tiles that explain the app are not the same colour: Help is blue
   beside it and the eye needs something to tell them apart at a glance. */
.tile-icon-faq { background: var(--badge-purple-bg); color: var(--badge-purple); }
/* The question mark is a letter and not an emoji, so unlike 🎯 and 📊 it takes the
   colour set here — and it needs the extra weight to carry the same visual weight as
   the emoji beside it. */
.tile-icon-help { background: var(--badge-blue-bg); color: var(--badge-blue); font-weight: var(--weight-heavy); }
/* The badge alone did not carry far enough. Ten cards of the same white, on the same
   grid, in the same order every time, and the only thing telling one row from another
   was a 32px badge inside a 48px row - so the eye looking for Statistics was reading
   labels instead of recognising a tile. The leading edge repeats that badge colour down
   the whole height of the row, which is what a two-column grid is actually scanned by.
   It is set here, beside the badge colour it copies, so that the two cannot be changed
   apart; a tile with no data-accent keeps the ordinary border and looks as it did.
   The 3px it costs is taken out of the padding on that side and never out of the text
   column - see the 320px measurements further down, where "Favourites" has five pixels
   to spare and a border on a border-box element would have spent all of them. */
.tile { --tile-accent: var(--control-border); }
.tile[data-accent="fav"] { --tile-accent: var(--badge-green); }
.tile[data-accent="improve"] { --tile-accent: var(--badge-orange); }
.tile[data-accent="mine"] { --tile-accent: var(--badge-purple); }
.tile[data-accent="stats"] { --tile-accent: var(--badge-blue); }
.tile[data-accent="set"] { --tile-accent: var(--badge-green); }
.tile[data-accent="help"] { --tile-accent: var(--badge-blue); }
.tile[data-accent="faq"] { --tile-accent: var(--badge-purple); }
.tile[data-accent="report"] { --tile-accent: var(--badge-orange); }
.tile-name { font-weight: var(--weight-bold); font-size: 1rem; }
.tile-sub { color: var(--muted); font-size: 0.8rem; }

/* The badge on the fine print beside a word list. The disclaimer itself is in the
   footer, on every screen, and is not repeated here; what this marks is the notice
   about the speech engine, which is about something happening now rather than about
   the content, and so is said where the speak buttons are. */
.notice-group-icon {
  width: 18px;
  height: 18px;
  border-radius: 6px;
  background: var(--badge-amber-bg);
  color: var(--badge-amber);
  font-weight: var(--weight-heavy);
  font-size: 0.72rem;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  /* The row is 18px and the line of text is taller, so the badge is nudged onto the
     first line's centre rather than its top */
  margin-top: 1px;
}

/* Home Screen icon control: one muted text line after the six tiles, not a seventh
   tile. Hidden in standalone so an already-installed open does not spend height.
   The howto sits out of flow so it cannot grow this block and shove the footer;
   overflow-anchor is off so a press does not let the browser re-anchor on a
   footer link. The row keeps one line of height even after a successful install. */
/* Gone from the flow after an install in a tab. While the control was a tile it was
   only made invisible, so its cell and the footer under the grid kept their places;
   a band between two bands has no cell to keep, and an invisible band left a blank
   the height of one between Advanced Learning and Customize. */
.home-install.is-installed { display: none; }
.home-install-howto {
  /* In the flow directly under the band since 6 September 2026, when the control left
     the tile grid: hung above that grid, as it was while the control was a tile, the
     panel opened over the "More in the app" heading, two controls away from the press
     it answers. Under the band it is where the eye already is, and mid-page a short
     viewport still shows it. */
  margin-top: 8px;
  padding: 10px 12px;
  border-radius: 10px;
  /* Not another card. This panel is the whole answer to some presses, and it is the one
     place the warning about the tab reaches a visitor who has switched the tooltips off,
     so it is tinted with the purple the install band already wears instead of blending
     into the bands it sits between. Both tokens are declared for the light and the dark
     theme alike, so this needs no media query of its own. */
  background: var(--badge-purple-bg);
  border: 1px solid var(--border);
  border-inline-start: 4px solid var(--badge-purple);
  color: var(--text);
  font-size: 0.8rem;
  line-height: 1.5;
  box-shadow: var(--shadow);
}
/* The first sentence is either the whole answer or the warning that the tab is about to
   be handed over, so it gets a line of its own and the accent colour rather than the
   body colour — small print that matters should not look like the small print. */
.home-install-howto strong {
  display: block;
  margin-bottom: 3px;
  color: var(--badge-purple);
  font-weight: 700;
}
/* The text and its dismiss button on one row, the button held to the top so it stays
   beside the first line however many lines the message runs to. */
.home-install-howto {
  display: flex;
  align-items: flex-start;
  gap: 8px;
}
.home-install-howto-text { flex: 1; }
/* A real button and not a decorative ×: 28px square is the smallest target this can be
   and still be pressed on a phone, and the glyph is inside an aria-hidden span so the
   accessible name is the label on the button, not the character. */
.home-install-howto-close {
  flex-shrink: 0;
  width: 28px;
  height: 28px;
  margin-block-start: -3px;
  margin-inline-end: -3px;
  padding: 0;
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: var(--badge-blue);
  font-family: inherit;
  font-size: 1.05rem;
  line-height: 1;
  cursor: pointer;
}
.home-install-howto-close:hover { background: var(--card); }
.home-install-howto-close:focus-visible {
  outline: 2px solid var(--badge-blue);
  outline-offset: 2px;
}
@media (display-mode: standalone) {
  .home-install { display: none !important; }
}

.footer-legal-notice {
  margin-top: 32px;
  color: var(--muted);
  font-size: 0.78rem;
  line-height: 1.65;
  text-align: center;
  /* An even split between the lines — without it the last line is left holding a
     single orphaned word */
  text-wrap: balance;
}
.footer-legal-notice p + p { margin-top: 4px; }
/* The link is coloured like the rest of the footer (no default blue/purple), and the
   phrase "terms of use" stays on one line rather than breaking across two. */
.footer-legal-notice a {
  color: var(--muted);
  white-space: nowrap;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  transition: color 0.15s;
}
.footer-legal-notice a:hover { color: var(--primary); }
/* The same caution in the language inferred for the reader. Set apart from the two
   English paragraphs by a rule and a little air rather than by a heading, so it reads
   as the same notice said again and not as a further condition. dir is put on the element
   itself by markLang, which gives RTL text the correct bidi ordering while the footer
   deliberately remains centred.

   It is deliberately the same size as the English above it. Shrinking the one line
   that most of this app's readers can actually read would undo the point of adding
   it, so the height it costs is paid rather than hidden — see the note on the compact
   home screen further down, and measure that screen at 320px after touching this. */
.footer-disclaimer-i18n {
  margin-top: 10px !important;
  padding-top: 10px;
  border-top: 1px solid var(--border);
  /* Balancing works on short Latin lines and does poor things to scripts that do not
     break on spaces, so the translated line is left to wrap normally. */
  text-wrap: initial;
}
/* The English precedence line. It sits under the translation without a rule of its own,
   because it is a footnote to that line rather than a fourth statement. Centred on its
   own terms rather than inheriting the translation's direction: it is always English,
   so it is always read left to right, whatever pair is being practised. Nothing here
   may clip it — no height, no line clamp, no overflow — because a precedence clause
   that is cut off at 320px stops being a precedence clause. */
.footer-prevails {
  margin-top: 6px !important;
  text-align: center;
  text-wrap: initial;
  /* Full ink on purpose: the footer is already --muted, and fading that to 85% lands
     at about 4.02:1 on --bg - under the 4.5:1 AA floor for text this small. */
  /* Bold and a step above the footer's 0.78rem, so the one sentence that decides which
     text governs is not read as footer fine print. Both lines of the pair share this
     class, so the translation and the English grow and embolden together — emphasising
     only one would present them as statements of different rank. The weight goes through
     the same variable as the rest of the app so the reader's text-weight setting still
     applies. */
  font-weight: var(--weight-bold);
  font-size: 0.85rem;
}
/* The same sentence in the reader's language, sitting directly above the English one.
   The two are a pair, so they are tightened towards each other and away from the summary
   above: the reader should see one statement given twice, not two separate footnotes.
   It takes its direction from dir="auto" rather than the ltr forced on the English copy,
   because this one is whatever language the reader reads. The no-clipping rule above
   applies here just as hard — a precedence clause cut off at 320px is not one. */
.footer-prevails-i18n {
  margin-top: 8px !important;
}
.footer-prevails-i18n + .footer-prevails {
  margin-top: 2px !important;
}
/* The privacy summary's disclosure. It sits below the precedence pair and is separated
   from it by the same rule the translated disclaimer uses against the English above it:
   what comes before is one statement said twice, and this is a different statement.
   Nothing here may clip the body either — a summary of what a server log holds is not
   worth having if it is cut off at 320px — so the body gets no height and no clamp. */
.privacy-brief {
  margin-top: 12px;
  padding-top: 10px;
  border-top: 1px solid var(--border);
  text-align: start;
}
/* The visible label is the whole of this element until someone opens it, so it is styled
   as a control rather than as footer prose: it has to look like something that can be
   pressed. list-style-position keeps the marker beside the text in an RTL pair instead of
   hanging outside the footer's padding. */
.privacy-brief summary {
  cursor: pointer;
  color: var(--primary);
  font-weight: var(--weight-bold);
  padding: 4px 0;
  list-style-position: inside;
}
.privacy-brief summary:hover { text-decoration: underline; }
#privacy-brief-text {
  margin-top: 8px !important;
  /* Same reason as the translated disclaimer: balancing helps short Latin lines and
     hurts the scripts that do not break on spaces. */
  text-wrap: initial;
}
/* The English address line, under the translation and always read left to right whatever
   pair is being practised — it is the one part of this block that is never translated. */
.privacy-brief-en {
  margin-top: 8px !important;
  text-wrap: initial;
  /* Full ink for the same contrast reason as .footer-prevails above. */
}
#app-footer { overflow-anchor: none; }
.site-footer {
  text-align: center;
  color: var(--muted);
  font-size: 0.8rem;
  margin-top: 36px;
  letter-spacing: 0.03em;
}
/* The row is a <nav> holding a <ul> now, so the flex container is the list. The numbers
   below keep the rendering to the pixel: each link was 17px tall, which is under the
   24px minimum target (WCAG 2.2, 2.5.8). 4px of padding above and below makes it 25px,
   the 8px row-gap comes out (the padding of two stacked links already supplies exactly
   those 8px between the lines of text), and -4px at the top and 10px→6px at the bottom
   give back the 8px the padding added. Same text in the same place, a target that is
   half as tall again. */
.legal-links {
  /* The footer's letter spacing is meant for the domain name; on the row of links it
     only widens them and pushes the last one onto a line of its own. */
  letter-spacing: normal;
  margin: -4px 0 6px;
}
.legal-links ul {
  display: flex;
  justify-content: center;
  flex-wrap: wrap;
  gap: 0 14px;
  margin: 0;
  padding: 0;
  list-style: none;
}
.legal-links a {
  /* inline-flex with a min-height, because the 25px total in the comment above relies
     on the UA's `normal` line box being 17px — Segoe UI's, but not every font's. A UA
     resolving it a couple of pixels shorter dropped the target under the 24px floor
     (WCAG 2.5.8); the min-height pins the floor whatever the font metrics say, and in
     the 17px case changes nothing. */
  display: inline-flex;
  align-items: center;
  min-height: 24px;
  padding: 4px 0;
  color: var(--muted);
  text-decoration: none;
  transition: color 0.15s;
}
.legal-links a:hover { color: var(--primary); }
/* The links loose in the footer's last line — "Questions and answers" and "Back to the
   site" on legal.html and faq.html. They are --muted like every link in this footer, and
   that line of text is --muted too, so with no underline there was nothing whatever
   marking them as links: not colour, not weight, not shape. A hover colour is not an
   answer, because hovering is not how anyone finds out that something is there (WCAG
   1.4.1). The child combinator is doing the work: it reaches the anchors sitting directly
   in the footer's own text and leaves the five-document row alone, which is nested in
   .legal-links ul li and is a list in a row of its own rather than links inside a
   sentence. index.html has no such loose anchor, so its footer does not move. */
.site-footer > a { text-decoration: underline; }

/* ===== Quiz ===== */
.quiz-top {
  display: flex;
  align-items: center;
  gap: 14px;
  /* With the 10px of padding the sticky rule adds below the row, the gap under it is
     the 22px it has always been */
  margin-bottom: 12px;
}
.icon-btn {
  width: 42px;
  height: 42px;
  border-radius: 13px;
  background: var(--card);
  border: 1px solid var(--control-border);
  font-size: 1.1rem;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: border-color 0.15s;
}
.icon-btn:hover { border-color: var(--primary); }
.progress-track {
  flex: 1;
  height: 8px;
  border-radius: 99px;
  background: var(--card);
  border: 1px solid var(--control-border);
  overflow: hidden;
}
.progress-fill {
  height: 100%;
  width: 0;
  background: var(--primary);
  border-radius: 99px;
  transition: width 0.35s ease;
}
.quiz-counter { font-weight: var(--weight-bold); color: var(--muted); min-width: 44px; text-align: end; }

.quiz-card {
  position: relative;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 44px 20px 20px;
  text-align: center;
  margin-bottom: 20px;
}
.star-btn {
  position: absolute;
  top: 12px;
  inset-inline-end: 14px;
  font-size: 1.5rem;
  color: var(--muted);
  transition: transform 0.15s, color 0.15s;
}
.star-btn.active { color: var(--star); }
.star-btn:active { transform: scale(1.25); }
.quiz-word {
  display: block;
  width: 100%;
  font-size: 2.3rem;
  font-weight: var(--weight-heavy);
  color: var(--text);
  /* A word the user added can be a run of up to 60 characters with no space — without
     breaking inside the word it pushes the page wider and drags in a horizontal scroll */
  word-break: break-word;
  overflow-wrap: anywhere;
  padding: 6px;
}
.quiz-word:active { color: var(--primary); }
.quiz-hint {
  display: block;
  color: var(--muted);
  font-size: 0.82rem;
  margin-top: 10px;
}
/* Every round shows this, and it has to be read before the word rather than after it
   — it says which language the answer is in. */
.quiz-direction {
  margin: 0 0 6px;
  color: var(--muted);
  font-size: 0.82rem;
  font-weight: var(--weight-bold);
}
.voice-notice {
  display: block;
  color: var(--muted);
  font-size: 0.72rem;
  line-height: 1.55;
  margin-top: 8px;
  /* Two things go wrong when a short paragraph of fine print is left to wrap on its own:
     a full line followed by a stub, and a last line holding nothing but the link — which
     reads as a mistake rather than as a notice. Balancing evens out the lines; the
     non-breaking space before each link in index.html keeps a word with the link in the
     browsers that do not balance. */
  text-wrap: balance;
}
/* Muted like the text it sits in, and lifted off the baseline so the underline does not
   cut through the descenders at this size. The same treatment as the footer links. */
.voice-notice a {
  color: var(--muted);
  white-space: nowrap;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  transition: color 0.15s;
}
.voice-notice a:hover { color: var(--primary); }

/* The "i" that opens the fuller speech notice follows the Details link rather than a
   heading, and its invisible 44px target overhangs the visible 22px mark by 11px on each
   side — enough, at the 7px the mark carries everywhere else, to sit on the last letters
   of the link and swallow a press meant for it. One pixel of daylight is all this needs,
   and the target itself is untouched: WCAG 2.5.5 asks for the 44px, not for where it may
   not reach. */
.voice-notice .tip-btn { margin-inline-start: 12px; }

/* ===== The notices that go with a word list =====
   Loose on the page, one under another, these read as leftover text: two paragraphs of
   small grey type with nothing to say they belong together or what they belong to. Here
   they are one block instead — the home screen's badge, then the notices in a column
   beside it — so the eye takes them in as a single footnote to the list and moves on.
   The wording is untouched: it is clause 1 of the disclaimer and the speech notice, and
   only their presentation is at issue. */
.notice-group {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  /* Clear of the list on both sides: above, so it is not mistaken for the heading's
     subtitle; below, so it does not touch the first row. */
  margin: 12px 0 14px;
}
.notice-group-text {
  display: flex;
  flex-direction: column;
  /* Tighter than the gap between paragraphs elsewhere, for the screens that ever carry
     a second line here — inside the block the notices are one thing, and the badge
     already sets them apart from the page. */
  gap: 5px;
  /* Long words wrap inside the column instead of widening it past the screen */
  min-width: 0;
}
/* The column owns the spacing now */
.notice-group .voice-notice { margin-top: 0; }
/* After a list rather than before one — the block needs the room above, not below */
.word-list + .notice-group { margin: 14px 0 0; }

.answers { display: flex; flex-direction: column; gap: 12px; }
.answer-btn {
  /* Three columns: a mark slot, the answer, a matching empty slot. The ✓ / ✗ after a
     press go in the first column rather than into the word, so a long answer cannot
     wrap onto another line at the moment of answering and shove the footer down.
     dir on the button (from markLang) puts that first column at inline-start, which
     is the right side in Arabic and Hebrew. */
  display: grid;
  grid-template-columns: 1.15em minmax(0, 1fr) 1.15em;
  align-items: center;
  column-gap: 6px;
  padding: 17px 12px;
  border-radius: var(--radius-btn);
  background: var(--card);
  border: 1px solid var(--control-border);
  font-size: 1.15rem;
  font-weight: var(--weight-medium);
  text-align: center;
  color: var(--text);
  /* As in .quiz-word: a long answer with no space breaks instead of widening the page */
  min-width: 0;
  overflow-wrap: anywhere;
  transition: background 0.2s, border-color 0.2s, color 0.2s, opacity 0.3s, transform 0.15s;
}
.answer-btn::before,
.answer-btn::after {
  content: "";
  line-height: 1;
}
.answer-btn::before { justify-self: start; }
.answer-btn.correct::before { content: "✓"; }
.answer-btn.wrong::before { content: "✗"; }
.answer-btn > span {
  min-width: 0;
  overflow-wrap: anywhere;
}
/* Hover is asked for rather than assumed. A touch screen has no pointer to hover with,
   so a phone applies :hover to whatever was last touched and leaves it there until
   something else is touched. That is what put the green frame on an answer that was
   never registered as pressed, and made a lost press look like a half-taken one.
   Behind (hover: hover) the frame belongs to a real pointer, and a touch that comes to
   nothing now leaves nothing behind. */
@media (hover: hover) {
  .answer-btn:not(:disabled):hover { border-color: var(--primary); }
}
.answer-btn:not(:disabled):active { transform: scale(0.98); }
/* The fill carries the verdict, and --verdict-ink is the wording on it — never a green
   or red letter on a green or red ground. On the light page the fill is the colour
   itself and the ink is white; on the dark one the fill is a deep tint and the ink is
   the page's own. The tick and the cross are part of the label and take the same ink. */
.answer-btn.correct {
  background: var(--success-bg);
  border-color: var(--success);
  color: var(--verdict-ink);
  font-weight: var(--weight-bold);
}
.answer-btn.wrong {
  background: var(--error-bg);
  border-color: var(--error);
  color: var(--verdict-ink);
  font-weight: var(--weight-bold);
}
/* The correct answer's word is also underlined — the one underline on a button in the
   app, against the tile comment's rule that the underline is a link's mark and not a
   button's. It cannot read as one here: every answer button is already disabled when
   the class arrives, and what the line marks is the word this question existed to
   teach, in a way that survives without the green. On the span and not the button,
   because on the button the decoration would spread to the ::before ✓ as well. A
   decoration adds no width and no height, so nothing rewraps at the moment of the
   verdict. The offset keeps it clear of Arabic and Devanagari descenders. The chosen
   wrong answer is deliberately not struck through: a line through a word in a script
   the learner is still learning makes it unreadable exactly when they want to re-read
   what they confused. */
.answer-btn.correct > span {
  text-decoration: underline;
  text-decoration-thickness: 0.07em;
  text-underline-offset: 0.18em;
  text-decoration-color: var(--verdict-ink);
}
.answer-btn.faded { opacity: 0.35; }

/* Each option: the answer button plus a small speak button beside it */
.answer-row { display: flex; align-items: stretch; gap: 8px; }
.answer-row .answer-btn { flex: 1; }
.answer-speak {
  flex: 0 0 auto;
  width: 52px;
  border-radius: var(--radius-btn);
  background: var(--card);
  border: 1px solid var(--control-border);
  color: var(--muted);
  font-size: 1.15rem;
  transition: border-color 0.2s, transform 0.15s;
}
@media (hover: hover) {
  .answer-speak:hover { border-color: var(--primary); }
}
.answer-speak:active { transform: scale(0.95); }
/* The speak button beside a faded answer is deliberately NOT faded with it: unlike the
   answer button it is never disabled — hearing the wrong options after answering is
   the point of it — and 0.35 opacity blended its border to 1.48:1 against the page, a
   live control with no discernible edge (WCAG 1.4.11). The faded answer beside it
   already carries the "not chosen" signal. */

/* A held finger has to land as a press, not as a text selection. Left alone the browser
   reads a long press on the words inside a button as the start of a selection — the
   callout on iOS, the selection handles on Android, a drag-select with a mouse — and
   drops the click when the finger or button lifts. answer() then never runs: no option
   goes green or red, and the round does not move on. All that stays on screen is the
   green :hover frame the touch left behind, which is why a long press looked like it
   half-worked.

   The rule was written for the two answer controls and applied to them alone, which
   left the same hold doing nothing on every other button in the app: the word being
   asked, "Next word", the stars, the level cards, the tiles, the chips, the word rows
   and the Advanced Learning panels. Every selector below is a control that is only ever
   pressed — none of them is a place to select text from, exactly as the answer buttons
   are not — so taking the selection away leaves a press as the only thing a hold can
   mean. The pointer-event half of the fix is onPress in app.js. */
.answer-btn,
.answer-speak,
.home-example-speak,
.home-example-word,
.quiz-word,
.quiz-next,
.star-btn,
.star-inline,
.level-card,
.tile,
.chip,
.dir-btn,
.btn,
.icon-btn,
.word-action,
.word-en,
.word-target,
.learn-panel,
.learn-transport-btn,
.lang-picker-option {
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
}

/* ===== Typing quiz ===== */
.type-answer { display: flex; flex-direction: column; gap: 10px; }
.type-input {
  width: 100%;
  padding: 15px 14px;
  border-radius: var(--radius-btn);
  background: var(--card);
  border: 1px solid var(--control-border);
  color: var(--text);
  font-size: 1.1rem;
  font-family: inherit;
  font-weight: inherit;
  text-align: start;
}
.type-input::placeholder { color: var(--muted); }
.type-input:focus { border-color: var(--primary); }
/* An answered question freezes the field without disabling it and without making it
   read-only: either of those drops the focus or tells the browser to put the phone's
   keyboard away, and the keyboard leaving and coming back is what made the quiz jump
   between questions. The text is held still by the beforeinput handler in app.js instead,
   so all that is needed here is to stop the caret blinking in a field that has finished
   taking answers. */
.type-input.locked { caret-color: transparent; }
/* The field is still the focused element after the answer is checked, so it still carries
   the focus ring — and the ring is drawn in the accent green, which around a field marked
   wrong reads as a second, contradicting verdict. The ring stays, because the focus has
   to stay visible; it is only repainted in the colour of the outcome it surrounds. */
.type-input.locked.correct:focus-visible { outline-color: var(--success); }
.type-input.locked.wrong:focus-visible { outline-color: var(--error); }
.type-input.correct {
  background: var(--success-bg);
  border-color: var(--success);
  color: var(--verdict-ink);
  -webkit-text-fill-color: var(--verdict-ink);
  font-weight: var(--weight-bold);
}
.type-input.wrong {
  background: var(--error-bg);
  border-color: var(--error);
  color: var(--verdict-ink);
  -webkit-text-fill-color: var(--verdict-ink);
  font-weight: var(--weight-bold);
}
/* Check, Hint and "I don't know" stay on one row rather than stacking: a second row costs
   exactly the height the keyboard has already taken. The type is a step down from an
   ordinary button's so the three labels fit across a 360px phone, and the hint takes the
   smallest share of what is left over because its label is the shortest.

   nowrap is what keeps them on one line rather than what would break it: a flex item's
   own minimum is the width of its content, so refusing to break "I don't know" is what
   makes the row reserve room for it instead of dividing the width three ways and folding
   the label over two lines. flex-wrap is the way out when even that will not fit — a
   320px screen with the heavier text setting on. There the last button drops to a row of
   its own, which is worse than one row and much better than a page that scrolls
   sideways. */
.type-actions { display: flex; flex-wrap: wrap; gap: 10px; }
.type-actions .btn { flex: 1; padding: 13px; font-size: 1rem; white-space: nowrap; }
.type-actions #type-hint { flex: 0.6; }

/* The answer, named under the field after a typed question is missed — the one thing
   in the round said in words rather than colour, because in the typing quiz there is
   no green answer already on screen to read it off. Green, so it matches the correct
   button the multiple-choice quiz marks for the same reason.

   The element stays in the DOM between questions rather than being hidden, which is
   what keeps role="status" announcing. On the typing quiz the line's height is kept
   even while empty, so writing the answer into it cannot grow the quiz and shove the
   footer down — the same reason the answer clock keeps its place after a press. */
.type-reveal {
  text-align: center;
  font-size: 1rem;
  font-weight: var(--weight-bold);
  line-height: 1.5;
  color: var(--success);
  /* The answer is the headline of the outcome zone: the one thing enlarged besides the
     word being asked, so the two read as a pair top-to-bottom. A token rather than a
     literal, because the compressed keyboard mode below shrinks it and the reserved
     min-height has to follow the same number wherever it is set. */
  --answer-size: 1.4rem;
}
/* The house eyebrow (see .lang-picker-heading): small, muted, uppercase. Demoting the
   label is what promotes the answer — before this both sat at the same 1rem green and
   the label read as half the message. Uppercase and letter-spacing stay on this
   English-only span; on the answer they would deform Arabic joining and every other
   script the learner is here to learn. */
.type-reveal .type-reveal-label {
  font-size: 0.72rem;
  font-weight: var(--weight-bold);
  color: var(--muted);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
/* Size only. The " / " separators between synonyms are bare text nodes, stay at the
   line's own 1rem, and recede — which is the right hierarchy anyway. */
.type-reveal bdi { font-size: var(--answer-size); }
#screen-quiz.quiz-typing .type-reveal {
  margin-top: 12px;
  line-height: 1.5;
  /* The 1.5em line is only the pre-calc() fallback and resolves against the line's own
     1rem: under it alone the filled 1.4rem line is taller than the empty reserve and
     the footer would move at every reveal. The calc line is the real slot — empty and
     filled single-line states are the same height whatever --answer-size holds. */
  min-height: 1.5em;
  min-height: calc(var(--answer-size) * 1.5);
}

/* Written feedback accompanies the existing colour and symbol changes. On multiple
   choice the pressed button already carries ✓ or ✗, so the line is not shown there
   (it stays a live region for screen readers). The typing quiz has no such button,
   and that is the one place the words are painted. The slot is reserved while empty
   so filling it cannot push the footer.

   0.85rem, because this line is the caption under the enlarged answer above it, not
   its equal: verdict small, lesson large. Its em-based slot height follows the size
   on its own. */
.quiz-feedback {
  text-align: center;
  font-size: 0.85rem;
  font-weight: var(--weight-bold);
  color: var(--text);
}
/* The struck attempt in "✗ You wrote: Avo". A strike and not a recolour, so the
   verdict pair survives without its colours; it crosses mid x-height, clear of the
   accents above and the marks below — and the unstruck copy still stands in the field
   for a careful look. The field itself is never struck: a line through Avô would
   destroy the very comparison the reveal exists for. */
.quiz-feedback .wrote-text { text-decoration: line-through; text-decoration-thickness: 0.08em; }
#screen-quiz.quiz-typing .quiz-feedback {
  /* A reserved slot, not a clipped one. It was a fixed height with overflow: hidden,
     white-space: nowrap and an ellipsis, so that a filled line could never be taller
     than the empty reserve and shove the footer — but a reader who applies the WCAG
     1.4.12 text-spacing overrides (0.12em letter spacing, 0.16em word spacing) widens
     the verdict past one line, and the centred ellipsis then clipped it from the
     *start* side: the ✓/✗ and the "You wrote" label were exactly what disappeared.
     Losing the verdict to a text-spacing preference is a 1.4.12 failure, so the slot
     is min-height now: empty and one-line filled states still occupy the same box and
     the footer stands, and only the rare wrapped line grows it.

     The slot used to hold three fixed English strings; now it can hold the learner's
     own attempt, and Thai tone stacks or vocalised Arabic paint ink above the line box.
     The 0.4em of padding keeps that ink inside the box and the two margin adjustments
     give exactly those pixels back, so the text sits where it always sat and the
     48px compressed total and the shell spacer's 78px derivation stand unchanged. */
  margin-top: calc(12px - 0.4em);
  margin-bottom: -0.4em;
  line-height: 1.5;
  min-height: calc(1.5em + 0.8em);
  padding: 0.4em 0;
}

/* ===== The flashcard round =====

   Two decisions under the card, laid out like the typed quiz's row of actions because
   they occupy the same part of the screen and the hand goes to the same place. "I knew
   it" is the primary button and sits on the right, which is the direction the card is
   swiped for it — the row and the gesture point the same way.

   Both reserved slots below are copied from the typing quiz for the same reason: this
   round paints the outcome in words and then the translation underneath it, and a line
   that appears where there was no line grows the quiz and shoves the footer down. */
.flash-actions { display: flex; gap: 10px; }
.flash-actions .btn { flex: 1; padding: 13px; font-size: 1rem; white-space: nowrap; }
#screen-quiz.quiz-flash .type-reveal {
  margin-top: 12px;
  line-height: 1.5;
  /* Fallback + real reserve, exactly as the typing copy of this rule explains. */
  min-height: 1.5em;
  min-height: calc(var(--answer-size) * 1.5);
}
#screen-quiz.quiz-flash .quiz-feedback {
  margin-top: 12px;
  line-height: 1.5;
  /* min-height, not a clipped fixed height, for the reason on the typing copy of this
     rule: the 1.4.12 text-spacing overrides wrap the verdict, and a clip then swallowed
     the second line whole. Empty and one-line states still reserve the same box. */
  min-height: 1.5em;
}

/* The card follows the finger, and lets go of it again if the movement was not far
   enough to decide anything. pan-y and not none: a flashcard is answered sideways, and
   the page is still read up and down — without this the browser cannot tell which of
   the two a touch was going to be, and the round would eat the scroll.

   pinch-zoom is named because naming anything at all takes it away: a touch-action of
   pan-y alone permits vertical panning and nothing else, zooming included. This card
   is the width of the screen and holds the word being learnt in a script the visitor
   may not read, which makes it the one thing on the app most likely to be pinched —
   and the viewport meta deliberately never said user-scalable=no, so the round must
   not be the one place that quietly does.

   The transition is on the card and off while it is being dragged, so the movement
   tracks the finger exactly and only the release is animated. The reduced-motion block
   at the top of this file already takes the animation off it for anyone who asked. */
#screen-quiz.quiz-flash .quiz-card {
  touch-action: pan-y pinch-zoom;
  transition: transform 0.18s ease-out;
}
#screen-quiz.quiz-flash .quiz-card.is-dragging { transition: none; }

/* The verdict, on the card that was answered — the flashcard's equivalent of the answer
   button going green or red, and of the answer field doing the same when it is typed.
   The frame carries it rather than the fill: --success-bg and --error-bg come with
   --verdict-ink for the wording that sits on them, and this card holds a word in an
   unfamiliar script, a hint, a star and a small grey notice. The inset shadow doubles
   the frame's weight without adding a pixel to the box — a thicker border would resize
   the card at the exact moment it is answered.

   The line below the card takes the same colour in this round and in the typed one:
   both paint the outcome in words, and a caption that names the attempt beside a ✗
   ought to be the colour of the verdict it carries. Multiple choice stays announced
   rather than painted — the marked button is its verdict. */
#screen-quiz.quiz-flash .quiz-card.correct {
  border-color: var(--success);
  box-shadow: var(--shadow), inset 0 0 0 1px var(--success);
}
#screen-quiz.quiz-flash .quiz-card.wrong {
  border-color: var(--error);
  box-shadow: var(--shadow), inset 0 0 0 1px var(--error);
}
#screen-quiz.quiz-flash .quiz-feedback.correct,
#screen-quiz.quiz-typing .quiz-feedback.correct { color: var(--success); }
#screen-quiz.quiz-flash .quiz-feedback.wrong,
#screen-quiz.quiz-typing .quiz-feedback.wrong { color: var(--error); }

/* The continue button for manual advancing follows the answers, where the hand
   already is after choosing an answer. is-pending paints it invisible but still in
   the layout, so revealing it after a press does not grow the quiz. */
/* The answer clock sits between the word and the answers and keeps its place even
   after the question is answered — so the row of answers does not jump upwards at the
   moment of answering. */
.quiz-limit {
  margin: 12px 0 2px;
  text-align: center;
  font-size: 0.95rem;
  font-weight: var(--weight-bold);
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.quiz-limit.urgent { color: var(--error); }

/* ===== The typed question with the keyboard up =====

   On a phone the software keyboard takes the lower half of the screen, and the question
   has to fit in what is left: the word being asked, the field and the line that answers
   it. Full size it does not fit, and whatever does not fit is scrolled away — the word
   first, since it sits at the top, leaving a question that cannot be answered.

   typing-focus is the keyboard: on a touch screen the field is focused when, and only
   when, the keyboard is up, and app.js puts the class on for exactly as long as it holds
   the focus. Nothing here runs on a desktop, where the pointer is fine and no keyboard
   takes the screen. The state also holds steady across an answer, because the field keeps
   the focus while the outcome is shown (see answerTyped in app.js) — so answering does
   not resize the card underneath the reader.

   The word keeps its place at the top of the card and the disclaimer beneath it stays:
   what goes is the spare room around them and the line explaining that the word can be
   tapped to hear it, which is instruction rather than question. app.js then scrolls the
   least amount the keyboard still demands — see pinTypingView. */
@media (pointer: coarse) {
  #screen-quiz.quiz-typing.typing-focus .quiz-card {
    padding: 34px 16px 14px;
    /* The gap under the card, not the clock's margin above it, is what sets the distance
       to the field: adjacent margins collapse to the larger of the two. */
    margin-bottom: 8px;
  }
  #screen-quiz.quiz-typing.typing-focus .quiz-word {
    font-size: 1.9rem;
    padding: 2px;
  }
  #screen-quiz.quiz-typing.typing-focus .quiz-hint { display: none; }
  /* The answer clock is a line of its own between the card and the field, and on the
     shortest screens it is the line that decides whether the outcome fits. */
  #screen-quiz.quiz-typing.typing-focus .quiz-limit { margin: 6px 0 2px; }
  #screen-quiz.quiz-typing.typing-focus .type-input { padding: 12px 14px; }
  #screen-quiz.quiz-typing.typing-focus .type-actions .btn { padding: 11px; }
  #screen-quiz.quiz-typing.typing-focus .type-reveal { margin-top: 6px; }
  /* The caption keeps the same clip-padding compensation its uncompressed rule
     explains, against the compressed 6px. */
  #screen-quiz.quiz-typing.typing-focus .quiz-feedback { margin-top: calc(6px - 0.4em); }
  /* With the keyboard up the answer steps down but stays a clear step above the 1rem
     field. The reserved arithmetic returns exactly to what this mode spent before the
     answer was enlarged: 1.15rem × 1.5 = 27.6px here plus the 0.85rem caption's 20.4px
     is the same 48px the two 1rem lines used to reserve, so the enlargement costs the
     compressed screen nothing. The min-height calc follows the token on its own. */
  #screen-quiz.quiz-typing.typing-focus .type-reveal { --answer-size: 1.15rem; }
}

.quiz-next { margin-top: 10px; padding: 13px; font-size: 1.05rem; }
.quiz-next.is-pending {
  visibility: hidden;
  pointer-events: none;
}
/* The running score is the only feedback the quiz screen keeps on show, and as a grey
   0.9rem sentence under the button it read as a footnote — a lost five points went by
   unread. It is a scoreboard now: one capsule, centred under the round, with the points
   carrying the accent and turning red the moment the round is in the negative. */
.quiz-bottom {
  width: fit-content;
  margin: 12px auto 0;
  display: flex;
  align-items: baseline;
  gap: 9px;
  padding: 7px 18px;
  border-radius: 99px;
  background: var(--card);
  border: 1px solid var(--border);
  font-size: 1rem;
  color: var(--text);
}
.quiz-score-sep { color: var(--muted); }
.quiz-score-points { font-weight: var(--weight-bold); color: var(--primary); }
.quiz-score-points.is-loss { color: var(--error); }

/* ===== Advanced Learning =====
   A listen screen, not a new visual system: the same cards, accent, chips and buttons
   as the rest of the app. The only extra is which of the two language panels is the
   one being read aloud. */
.learn-title { margin: 4px 0 16px; font-size: 1.25rem; }
/* Under the title, only for a set drawn from a saved list: the level chips it stands
   in for are hidden then, so this line is what says where the words came from. */
.learn-source { margin: -8px 0 16px; font-size: 0.9rem; }
.learn-panels {
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.learn-panel {
  background: var(--card);
  /* --control-border, not --border: this is a pressable button, and the rule on the
     token definitions says the edge that identifies a control keeps the stronger
     token. It was the one button in the file on the quieter one. */
  border: 1px solid var(--control-border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 20px 16px 18px;
  text-align: start;
  min-height: 44px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  transition: border-color 0.2s, box-shadow 0.2s, background-color 0.2s;
}
.learn-panel.is-active {
  border-color: var(--primary);
  box-shadow: 0 0 24px var(--learn-glow), var(--shadow);
}
/* While a pair is being read, the panel not being read steps back by losing its card
   surface to the page ground — not by opacity: the panel stays a live button (pressing
   it restarts the pair from that language), and fading it to 0.58 blended its --muted
   language label to 2.47:1, under the 4.5:1 AA floor for text that size (WCAG 1.4.3).
   The active panel still carries the green edge, the glow and the larger word. */
.learn-panels:not(.is-idle) .learn-panel:not(.is-active) {
  background: var(--bg);
  box-shadow: none;
}
.learn-lang {
  color: var(--muted);
  font-size: 0.78rem;
  font-weight: var(--weight-bold);
  letter-spacing: 0.01em;
}
.learn-panel.is-active .learn-lang { color: var(--primary); }
.learn-word-wrap {
  display: flex;
  align-items: flex-start;
  gap: 8px;
}
.learn-panel .speak-glyph { margin-top: 0.45em; }
.learn-word {
  font-size: 1.7rem;
  font-weight: var(--weight-heavy);
  line-height: 1.25;
  overflow-wrap: anywhere;
  min-width: 0;
}
.learn-panel.is-active .learn-word { font-size: 1.95rem; }
/* Stepping back the panel that is not being read only means something while a word is
   being read — the :not(.is-idle) on the rule above. Paused, and on arrival, which is
   now the same thing, the pair stands whole. */

/* ----- The controls -----
   Four identical full-width buttons in a 2x2 grid said that Pause, Play, Repeat and Skip
   are four equal things, and left the one that matters indistinguishable from the rest.
   They are a transport row instead: the round Play/Pause in the middle at the size of the
   control that gets pressed, Repeat and Skip smaller either side. Each keeps its word
   underneath - a glyph alone is a guess. */
.learn-transport {
  margin-top: 22px;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  gap: 26px;
}
.learn-transport-btn {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 9px;
  color: var(--muted);
  /* The small circles are 52px against the middle one's 76px. The padding drops them
     onto the same bottom edge, so the three words below sit on one line instead of
     stepping down in the middle. */
  padding-top: 24px;
  transition: color 0.15s;
}
.learn-transport-glyph {
  width: 52px;
  height: 52px;
  border-radius: 50%;
  background: var(--card);
  border: 1px solid var(--control-border);
  color: var(--text);
  font-size: 1.2rem;
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: border-color 0.15s, transform 0.15s, box-shadow 0.2s;
}
.learn-transport-label {
  font-size: 0.78rem;
  font-weight: var(--weight-bold);
}
.learn-transport-btn:hover { color: var(--text); }
.learn-transport-btn:hover .learn-transport-glyph { border-color: var(--primary); }
.learn-transport-btn:active .learn-transport-glyph { transform: scale(0.93); }
/* The middle one wears the same glow as the panel being read, so the button and the
   sound it produces are plainly the same thing. */
.learn-toggle { padding-top: 0; }
.learn-toggle .learn-transport-glyph {
  width: 76px;
  height: 76px;
  background: var(--primary);
  border-color: var(--primary);
  color: var(--on-primary);
  font-size: 1.65rem;
  box-shadow: 0 0 0 8px var(--learn-glow);
  /* A play triangle reads as off-centre in a circle unless it is nudged across; a pause
     bar does not, so the nudge comes off again while it is playing. */
  text-indent: 0.14em;
}
.learn-toggle.is-playing .learn-transport-glyph { text-indent: 0; }
.learn-toggle .learn-transport-label { color: var(--text); font-size: 0.86rem; }
.learn-toggle:hover .learn-transport-glyph { border-color: var(--primary); }

/* ----- The gaps -----
   Settings, not controls, so they sit under a rule and a quiet heading rather than
   competing with the transport row above them. */
.learn-gaps {
  margin-top: 26px;
  padding-top: 18px;
  border-top: 1px solid var(--border);
}
.learn-gaps-title {
  font-size: 0.74rem;
  font-weight: var(--weight-bold);
  color: var(--muted);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  margin: 0 0 12px;
}
/* Level and Timing are two groups in one region. The second needs air above it, or its
   heading reads as a label for the chips it follows rather than for the ones under it. */
.learn-gaps-title-second { margin-top: 20px; }
.learn-gap { margin-bottom: 12px; }
.learn-gap:last-child { margin-bottom: 0; }
.learn-gap-label {
  font-size: 0.85rem;
  font-weight: var(--weight-bold);
  margin: 0 0 8px;
}
#screen-learn .chip {
  min-height: 44px;
  display: inline-flex;
  align-items: center;
}
#screen-learn .icon-btn {
  width: 44px;
  height: 44px;
}
.learn-end-line {
  font-size: 1.2rem;
  font-weight: var(--weight-bold);
  color: var(--text);
}
@media (min-width: 480px) {
  .learn-panels { flex-direction: row; align-items: stretch; }
  .learn-panel { flex: 1; min-width: 0; }
}

/* ===== Results ===== */
.score-card {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  text-align: center;
  padding: 32px 20px;
  margin-top: 8px;
}
.score-big { font-size: 3.2rem; font-weight: var(--weight-heavy); color: var(--primary); }
/* The percentage is the result said a second way, not a caption for the number above
   it. Set in --muted at body size it was the thing a reader had to lean in for, so it
   carries full ink; the big number keeps first place by size and colour alone. */
.score-pct { color: var(--text); font-size: 1.1rem; font-weight: var(--weight-medium); margin-top: 6px; }
/* The round's points are the reward, so they are shaped like one — the same badge pill
   the level numbers and tile icons use, rather than a grey line of small print. A round
   that finished in the negative says so in the pill's colour, because "−5 points" in the
   same green as a win is the one thing here that must not read as a win. */
.score-points {
  display: inline-block;
  margin-top: 14px;
  padding: 6px 18px;
  border-radius: 99px;
  background: var(--badge-green-bg);
  color: var(--badge-green);
  font-size: 1rem;
  font-weight: var(--weight-bold);
}
.score-points.is-loss { background: var(--error-soft); color: var(--error); }

.word-list { display: flex; flex-direction: column; gap: 10px; }
.word-row {
  display: flex;
  align-items: center;
  gap: 12px;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius-btn);
  padding: 13px 16px;
}
.word-row .mark { font-size: 1.05rem; flex-shrink: 0; }
.word-en, .word-target {
  background: none;
  font-size: 1.02rem;
  color: var(--text);
  padding: 2px 4px;
  /* The speaker icon sits against the word and makes clear it can be tapped to hear it */
  display: inline-flex;
  align-items: center;
  gap: 5px;
  /* On a very narrow screen the text wraps instead of pushing the row past the width of
     the screen. flex-wrap and overflow-wrap are both needed to break inside a long word
     with no space, which would otherwise stay a single flex item that cannot shrink. */
  min-width: 0;
  flex-wrap: wrap;
  overflow-wrap: anywhere;
  text-align: start;
}
.speak-glyph { font-size: 0.78em; line-height: 1; opacity: 0.8; flex-shrink: 0; }
/* A browser with no Web Speech API cannot read a word aloud, and a speaker icon that
   promises it can is worse than no icon: the control is offered, pressed, and nothing
   happens. The class is set once on <body> at start-up (SPEECH_SUPPORTED in app.js),
   and it removes the affordance rather than disabling it — there is nothing here to
   re-enable later. The word buttons themselves stay: they still show the word, and
   pressing one now says why it is silent. */
body.no-speech .speak-glyph,
body.no-speech .home-example-speak,
body.no-speech .answer-speak { display: none; }
.word-en:hover, .word-target:hover { color: var(--primary); }
.word-en:hover .speak-glyph, .word-target:hover .speak-glyph { opacity: 1; }
.word-en { font-weight: var(--weight-bold); }
.word-en:active, .word-target:active { color: var(--primary); }
.word-target { color: var(--muted); }
.word-row .spacer { flex: 1; }
.word-row .star-inline {
  font-size: 1.25rem;
  color: var(--muted);
  /* The glyph alone measured about 20×23px — the one row control under the 24px
     target floor (WCAG 2.5.8); its siblings all got sized (.word-action's padding,
     .back-link's min-height). The flex centring keeps the star where it sat. */
  min-width: 24px;
  min-height: 24px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: color 0.15s, transform 0.15s;
}
.word-row .star-inline.active { color: var(--star); }
.word-row .star-inline:active { transform: scale(1.2); }
.word-row .wrong-count { color: var(--error); font-size: 0.85rem; font-weight: var(--weight-bold); }
/* How many correct answers in a row are left before the word comes off the
   words-to-improve list */
.word-row .word-note {
  color: var(--muted);
  font-size: 0.78rem;
  white-space: nowrap;
  flex-shrink: 0;
}
/* A row for a word the user added: the level they chose, with edit and delete beside it
   in place of the star */
.word-row .word-level {
  color: var(--muted);
  font-size: 0.75rem;
  white-space: nowrap;
  flex-shrink: 0;
}
.word-row .word-action {
  font-size: 1.05rem;
  line-height: 1;
  color: var(--muted);
  padding: 6px;
  border-radius: 10px;
  flex-shrink: 0;
  transition: color 0.15s, background 0.15s;
}
.word-row .word-action:hover { color: var(--primary); background: var(--bg); }
.word-row .word-action:active { transform: scale(0.92); }
/* Taking one word off a list was a grey 🗑 the width of the row's own punctuation, so
   on the Favourites screen the only thing that looked like a way to remove anything was
   the red button that removes every word at once. The single-word press gets an edge and
   the danger ink, so the two read as one kind of action at two sizes. */
.word-row .word-action-remove {
  color: var(--error);
  border: 1px solid var(--error);
  padding: 6px 9px;
}
@media (hover: hover) {
  .word-row .word-action-remove:hover {
    color: var(--danger-btn-ink);
    background: var(--danger-btn-bg);
    border-color: var(--error);
  }
}

#screen-results h2 { margin-bottom: 6px; }
.results-hint { font-size: 0.82rem; margin-bottom: 12px; }

/* The actions appear twice on the results screen — above the word list and below it —
   so you can leave or practise again without scrolling. Both are more compact than an
   ordinary button. The two directions live under one heading so it is clear that they
   repeat the same finished set rather than draw a new one.

   A column of five identical full-width bars is what a finished round used to open on,
   and the one carrying a direction wrapped to two lines on a phone. They wrap into a
   row instead: the main action keeps the full width it deserves, the rest take the
   width of their own words and sit side by side when two of them fit. */
.results-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 20px;
}
.results-actions .btn {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  width: auto;
  flex: 1 1 auto;
  min-height: 44px;
  padding: 9px 16px;
  font-size: 0.98rem;
}
/* The action the round has just argued for takes the whole line; the quiet ones share. */
.results-actions > .btn-primary,
.results-actions .results-replay-group { flex: 1 1 100%; }
/* The quiet half of a two-line label: the languages a replay would use, or how many
   mistakes a retry would ask. Small and muted so the name above it stays the label. */
.results-pair,
.results-action-note {
  font-size: 0.82em;
  font-weight: var(--weight-medium);
  opacity: 0.78;
}
/* Leaving takes the width of its own two words rather than a whole bar of its own:
   it is the one control here nobody came to the results screen for, and the arrow at
   the top of the window offers it as well. */
.results-actions .results-home { flex: 0 1 auto; }
.results-actions-top { margin: 16px 0 0; }
/* Five controls of one size and one shape stacked in a column read as a wall, and the
   round has just ended on a number that says what to do next. The halo in the accent
   puts the main action first in the eye without making the others any harder to find. */
.results-actions .btn-primary { box-shadow: 0 6px 18px var(--learn-glow); }
/* The two directions get their own frame, so the heading above them is visibly the
   label of a pair rather than a line of grey type left over the stack. */
.results-replay-group {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  padding: 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
/* The heading names the pair below it, so it owns the whole first line. */
.results-replay-title { flex: 1 1 100%; }
.results-replay-group .btn { flex: 1 1 44%; }
.results-replay-title {
  margin: 0 0 2px;
  color: var(--muted);
  font-size: 0.9rem;
  font-weight: var(--weight-bold);
  text-align: start;
}
/* Leaving is the quietest thing here, so it gives up the card fill. It keeps its edge:
   what identifies a control has to stay visible without its label (see --control-border),
   and that holds for a control meant to recede as much as for any other. */
.results-actions .results-home {
  background: none;
  color: var(--muted);
}
@media (hover: hover) {
  .results-actions .results-home:hover {
    background: var(--btn-hover-bg);
    color: var(--text);
  }
}

.btn {
  display: block;
  width: 100%;
  padding: 16px;
  border-radius: var(--radius-btn);
  font-size: 1.1rem;
  font-weight: var(--weight-bold);
  text-align: center;
  transition: transform 0.15s, opacity 0.2s, background-color 0.2s, border-color 0.2s,
    box-shadow 0.2s;
}
.btn:active { transform: scale(0.98); }
.btn-primary { background: var(--primary); color: var(--on-primary); }
.btn-ghost {
  background: var(--card);
  border: 1px solid var(--control-border);
  color: var(--text);
}
.btn-danger {
  background: var(--danger-btn-bg);
  border: 1px solid var(--error);
  color: var(--danger-btn-ink);
}
/* Hover sits behind (hover: hover) for the reason given at .answer-btn: a touch screen
   has no pointer, so it leaves :hover on whatever was last touched and a button nobody
   pressed would keep the highlight. A mouse gets the button to answer under it — every
   button by the same one-pixel rise, and then the halo its own kind takes: the accent
   around a filled one, a soft accent shadow and the accent edge under a bordered one. */
@media (hover: hover) {
  /* The rise is written once for all of them. The shade and the edge alone were a quiet
     answer on a column of buttons that are all one size and one shape — the results
     screen after a round with no mistakes is four of those and nothing else — and a
     filled button answering by colour while a bordered one answered by shape read as
     two different kinds of control sitting in one stack. */
  .btn:not(:disabled):hover { transform: translateY(-1px); }
  .btn-primary:not(:disabled):hover { box-shadow: 0 0 0 3px var(--learn-glow); }
  .btn-ghost:not(:disabled):hover {
    background: var(--btn-hover-bg);
    border-color: var(--primary);
    box-shadow: 0 3px 12px var(--learn-glow);
  }
  .btn-danger:not(:disabled):hover { box-shadow: 0 0 0 3px var(--error-soft); }
  /* Leaving stays the quietest control on the results screen, so it answers with the
     rise, the fill and the ink and not with the halo — the rule at
     .results-actions .results-home says why it recedes. */
  .results-actions .results-home:not(:disabled):hover { box-shadow: none; }
  /* The hover rule above outranks the .btn:active scale, so without this line a pointer
     that was hovering and pressing held the hover rise and the press answered with
     nothing at all. */
  .btn:not(:disabled):active { transform: scale(0.98); }
}
/* The two ways out of a saved list — practise it, or hear it — side by side above the
   list, the listening one in the quiet style so the practise button stays the first
   thing the eye finds. They were full-width bars stacked under the list, which meant a
   visitor with real work saved had to scroll past every row to reach the only button
   the screen exists for; they wrap to two lines only when the words are long enough to
   need it. */
.list-actions { display: flex; flex-wrap: wrap; gap: 10px; margin: 2px 0 18px; }
.list-actions .btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  width: auto;
  flex: 0 1 auto;
  padding: 10px 20px;
  font-size: 0.98rem;
}

/* The backup area is built like the deletion area but is not destructive, so it gets an
   ordinary button */
.export-zone { margin-top: 28px; }
.export-zone h2 { font-size: 1.02rem; font-weight: var(--weight-bold); margin: 0 0 12px; }
.export-zone .danger-note { margin: 8px 0 0; }
.export-zone code { font-size: 0.95em; }

/* ===== Deletion area =====
   Every delete button comes with a line of explanation setting out what is deleted and
   what is not, so the scope is clear before the press and not only in the confirmation
   dialog. */
.danger-zone { margin-top: 28px; }
.danger-zone h2 { font-size: 1.02rem; font-weight: var(--weight-bold); margin: 0 0 12px; }
.danger-note { font-size: 0.82rem; line-height: 1.55; }
.danger-zone .danger-note { margin: 8px 0 18px; }
.danger-zone > .danger-note:last-child { margin-bottom: 0; }
/* On the settings cards the explanation comes before the button, and on the consent
   card it comes after it */
.setting-card .danger-note { margin: 0 0 14px; }
.setting-card .btn + .danger-note { margin: 12px 0 0; }

/* ===== Inner pages ===== */
.page-top {
  display: flex;
  align-items: center;
  gap: 14px;
  /* 10px here and 10px of padding from the sticky rule: the gap under the row is the
     20px it has always been */
  margin-bottom: 10px;
}
.page-title { font-size: 1.5rem; font-weight: var(--weight-heavy); }

.empty-state {
  text-align: center;
  padding: 60px 20px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  align-items: center;
}
.empty-icon { font-size: 3rem; margin-bottom: 6px; }
.empty-state .btn { max-width: 240px; margin-top: 12px; }

/* ===== Statistics ===== */
/* The line naming the language every number below belongs to; it sits directly above
   the summary, so the gap between them is the one that would otherwise separate the
   summary from the heading. */
.stats-scope { font-size: 0.85rem; margin-bottom: 12px; }
/* Directly under the three summary boxes and reading as their total, so it takes the
   accent colour the numbers above it use rather than the muted grey of a note. */
.stats-score-total {
  margin-top: 12px;
  font-size: 0.95rem;
  font-weight: var(--weight-heavy);
  color: var(--primary);
}
.stats-summary {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 12px;
}
.stat-box {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 16px 8px;
  text-align: center;
}
.stat-num { font-size: 1.5rem; font-weight: var(--weight-heavy); color: var(--primary); }
.stat-label { color: var(--muted); font-size: 0.8rem; margin-top: 4px; }

.chart {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 8px;
  height: 150px;
  padding: 18px 14px 10px;
}
.chart-col {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-end;
  gap: 6px;
  height: 100%;
}
.chart-bar {
  width: 100%;
  max-width: 34px;
  border-radius: 6px 6px 3px 3px;
  background: var(--track);
  min-height: 3px;
  transition: height 0.4s ease;
}
.chart-col.today .chart-bar { background: var(--primary); }
.chart-val { font-size: 0.72rem; color: var(--muted); }
.chart-day { font-size: 0.72rem; color: var(--muted); }
.chart-col.today .chart-day { color: var(--primary); font-weight: var(--weight-bold); }

/* A qualification at the bottom of the statistics: what has been gathered is stored per
   word id, so updating the word bank drops it. A dividing rule rather than a frame —
   this is a note, not a warning about an action. */
.data-note {
  font-size: 0.82rem;
  line-height: 1.55;
  margin-top: 24px;
  padding-top: 14px;
  border-top: 1px solid var(--border);
}

.level-stat {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius-btn);
  padding: 14px 16px;
  margin-bottom: 10px;
}
.level-stat-top {
  display: flex;
  justify-content: space-between;
  font-weight: var(--weight-bold);
  margin-bottom: 8px;
}
.level-stat-top .muted { font-weight: var(--weight-normal); font-size: 0.85rem; }
.level-stat-bar {
  height: 7px;
  background: var(--track);
  border-radius: 99px;
  overflow: hidden;
  margin-bottom: 8px;
}
.level-stat-fill { height: 100%; background: var(--primary); border-radius: 99px; }
/* Four figures now instead of two, and on a narrow phone they do not fit on one line —
   wrapping is what keeps the row inside the card instead of widening the page. */
.level-stat-nums { display: flex; flex-wrap: wrap; gap: 6px 14px; font-size: 0.82rem; }
.level-stat-nums .ok { color: var(--success); }
.level-stat-nums .bad { color: var(--error); }
.level-stat-nums .hints { color: var(--muted); }
.level-stat-nums .points { color: var(--primary); font-weight: var(--weight-heavy); }

.chip {
  padding: 10px 18px;
  border-radius: 99px;
  background: var(--card);
  border: 1px solid var(--control-border);
  font-size: 0.95rem;
  font-weight: var(--weight-medium);
  color: var(--text);
  transition: background 0.2s, color 0.2s, border-color 0.2s;
}
.chip.active { background: var(--primary); color: var(--on-primary); border-color: var(--primary); font-weight: var(--weight-bold); }
.chip:not(.active):hover { border-color: var(--primary); }
.chip-action {
  color: var(--primary);
  border-color: var(--primary);
  font-weight: var(--weight-bold);
  padding: 7px 14px;
  font-size: 0.85rem;
}

.game-row {
  display: flex;
  align-items: center;
  gap: 12px;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius-btn);
  padding: 12px 16px;
  margin-bottom: 10px;
}
.game-type { font-weight: var(--weight-bold); }
.game-date { color: var(--muted); font-size: 0.8rem; margin-top: 2px; }
.game-row .spacer { flex: 1; }
.game-score { font-weight: var(--weight-heavy); color: var(--primary); }
.game-dur { color: var(--muted); font-size: 0.8rem; text-align: end; }

/* ===== My words ===== */
.mywords-intro { font-size: 0.86rem; line-height: 1.6; margin: 4px 0 16px; }
.mywords-notice-text { line-height: 1.65; margin-bottom: 14px; }
.mywords-fields { display: flex; flex-direction: column; gap: 12px; }
.mywords-field { display: block; }
.mywords-field-label {
  display: block;
  color: var(--muted);
  font-size: 0.8rem;
  font-weight: var(--weight-bold);
  margin-bottom: 6px;
}
/* The label for choosing the level — a direct child of the form, not the label of a
   text field */
.mywords-form > .mywords-field-label { margin-top: 16px; }
.mywords-form .type-input { font-size: 1.02rem; padding: 12px 14px; }
/* :empty hides the message when there is no error, so it can stay in the DOM:
   role="alert" on an element that is re-added is not read out consistently by screen
   readers. */
.mywords-error {
  color: var(--error);
  background: var(--error-soft);
  border: 1px solid var(--error);
  border-radius: 12px;
  font-size: 0.85rem;
  line-height: 1.6;
  padding: 10px 12px;
  margin-top: 14px;
}
.mywords-error:empty { display: none; }
.mywords-actions { display: flex; flex-direction: column; gap: 10px; margin-top: 16px; }
.mywords-actions .btn { padding: 13px; font-size: 1.02rem; }
/* A row with two words, a level and two buttons — on a narrow screen it is better for
   it to break across two lines. The words-to-improve row has the same problem: a note,
   a delete button and a star sit after the two words. */
#mywords-list .word-row,
#improve-list .word-row { flex-wrap: wrap; gap: 8px; row-gap: 4px; padding: 12px 14px; }

/* ===== Settings ===== */
.setting-card { padding: 18px; margin-bottom: 14px; }
/* Used on My words / Improve as well as Settings: those pages still title a card
   with h2. The explicit margin cancels the default for h2. */
.setting-card h2 { font-size: 1.02rem; font-weight: var(--weight-bold); margin: 0 0 14px; }
.setting-hint { font-size: 0.82rem; line-height: 1.55; margin: 12px 0 0; }
.setting-actions { display: flex; flex-wrap: wrap; gap: 10px; margin: 12px 0; }
.setting-actions .btn { flex: 1 1 190px; width: auto; }
.voice-diag { font-size: 0.78rem; margin-top: 12px; line-height: 1.5; }
.voice-diag:empty { display: none; }

/* The settings screen is grouped: a small section name, then one card that holds the
   related controls. Improve and My words keep the older one-card-per-topic shape. */
.settings-group { margin: 0; }
.settings-group-title {
  font-size: 0.78rem;
  font-weight: var(--weight-bold);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
  margin: 26px 0 8px;
  line-height: 1.3;
}
#install-title { scroll-margin-top: 12px; }
/* The sticky row's own 10px of padding is the whole of this screen's tighter gap */
#screen-settings .page-top { margin-bottom: 0; }
#screen-settings .page-top + .settings-group .settings-group-title { margin-top: 4px; }
#screen-settings .settings-group .setting-card { margin-bottom: 0; }
#screen-settings .setting-card-stack { padding: 2px 16px 6px; }
#screen-settings .setting-block {
  padding: 15px 0;
  border-bottom: 1px solid var(--border);
}
#screen-settings .setting-block:last-child { border-bottom: none; }
#screen-settings .setting-block-title {
  font-size: 0.98rem;
  font-weight: var(--weight-bold);
  margin: 0 0 10px;
  line-height: 1.3;
}
#screen-settings .setting-block .chips { gap: 8px; }
#screen-settings .setting-card > .setting-hint:first-child { margin-top: 0; }
#screen-settings .consent-setting-status { margin: 0 0 12px; }
/* The one storage note under the three appearance rows: it follows the last block
   inside the card, so it wants the block's own bottom gap rather than the card's 6px. */
#screen-settings .setting-group-note { margin: 0 0 12px; }
#screen-settings .notice-group { margin: 22px 0 0; }

/* ===== The app's buttons =====
   The operator asked on 11 September 2026 for the buttons to look modern — first on the
   Customize screen, then on every screen. The level cards and the home tiles already
   did: raised on a solid edge in the shadow's tint (--raise-edge, per theme), sinking
   onto that edge when pressed rather than scaling, because a last-frame scale reads as
   a shake. Everything below is that same depth applied to the controls that were still
   flat — the chips, the two segmented rows, the wide .btn buttons, the answer buttons,
   the round icon buttons, the three home bands and the language picker's rows.
   One depth, three depths of shadow, and no new colour: the sheen and the shading are
   black and white at low alpha, so each theme keeps its own --card and --primary
   underneath, and the accent halo is --learn-glow, which is --primary at a low alpha in
   both themes. The space game is deliberately left out: it has its own neon idiom on a
   canvas, and none of these tokens exist in it.
   Nothing here changes a control's size, its ink or which element is pressed, so no
   HTML, no script and no label moved with it. */

/* The chips: every radiogroup on Customize, the Advanced Learning and game options, the
   My words level filter, and "Practise them" on the statistics screen. */
.chip {
  padding: 11px 18px;
  background-image: linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0) 45%, rgba(0, 0, 0, 0.045));
  /* A soft shadow and no solid edge, which is the one place the raise is not copied
     exactly: a 99px pill standing on a 2px lip shows that lip as a dark crescent poking
     out past each end, and eight of them in a row read as grubby rather than raised.
     The segments and the wide buttons keep the lip, because their corners hide it. */
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.07), 0 3px 10px rgba(0, 0, 0, 0.05);
  transition: background-color 0.2s, color 0.2s, border-color 0.2s, transform 0.15s,
    box-shadow 0.15s;
}
/* The chosen one keeps .chip.active's accent fill and gains the accent's own halo, so a
   row of eight voices says which is chosen from across the card and not by fill alone. */
.chip.active {
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.10), 0 4px 14px var(--learn-glow);
}
/* Pressing lets the chip down onto the page — the level cards' feedback, for the same
   reason: nothing resizes, so a chip that changes the screen behind it does not appear
   to shake on its last frame. */
.chip:active {
  transform: translateY(1px);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
}
/* Hover sits behind (hover: hover) for the reason given at .answer-btn: a touch screen
   has no pointer and would leave the last chip touched standing a pixel above the rest
   of its row. The press rule is repeated after it so a mouse press still sinks. */
@media (hover: hover) {
  .chip:not(.active):hover {
    background-color: var(--btn-hover-bg);
    border-color: var(--primary);
    transform: translateY(-1px);
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08), 0 6px 16px rgba(0, 0, 0, 0.07);
  }
  .chip:active {
    transform: translateY(1px);
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
  }
}

/* Direction and Quiz type — the two rows on Customize, and the only two .direction-toggle
   groups in the app — become one segmented control each. The box's own border goes,
   because the filled track already draws the group's edge and a border around a filled
   track reads as two boxes. The radius is the button radius and not a pill: these
   buttons wrap their language names onto two lines on a narrow phone (see .dir-btn), and
   a 999px corner on a two-line segment looks like a mistake. */
.direction-toggle {
  gap: 4px;
  padding: 4px;
  background: var(--track);
  border-color: transparent;
  border-radius: var(--radius-btn);
  box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.07);
}
.dir-btn {
  padding: 12px 6px;
  border-radius: 12px;
  transition: background-color 0.2s, color 0.2s, transform 0.15s, box-shadow 0.15s;
}
/* The chosen segment stands out of the track rather than only colouring in. .dir-btn.active
   already supplies the accent fill and its ink, in the live row and in the skeleton. */
.dir-btn.active {
  box-shadow: 0 2px 0 var(--raise-edge), 0 4px 12px var(--learn-glow);
}
.dir-btn:active {
  transform: translateY(2px);
  box-shadow: 0 0 0 var(--raise-edge), 0 1px 4px rgba(0, 0, 0, 0.08);
}
@media (hover: hover) {
  /* An unchosen segment answers by lifting out of the track onto the card's own
     surface, which is the only move left to it: the accent belongs to the chosen one. */
  .dir-btn:not(.active):hover {
    background-color: var(--card);
    box-shadow: 0 1px 0 var(--raise-edge), 0 2px 8px rgba(0, 0, 0, 0.08);
  }
  .dir-btn:active {
    transform: translateY(2px);
    box-shadow: 0 0 0 var(--raise-edge), 0 1px 4px rgba(0, 0, 0, 0.08);
  }
}
/* The skeleton in index.html is disabled until app.js builds the live rows, and
   .dir-btn:disabled already keeps it looking live rather than faded. It must not answer
   a hovering pointer, though — nothing is listening yet. */
.dir-btn:disabled { box-shadow: none; }
.dir-btn.active:disabled {
  box-shadow: 0 2px 0 var(--raise-edge), 0 4px 12px var(--learn-glow);
}

/* The wide buttons — Start Learning Now's neighbours everywhere else: the results
   screen's four, the two saved-list actions, "Practise them", the consent dialog's
   pair, My words' add and delete, Customize's saving and reset. They were flat
   rectangles that answered a press by shrinking 2%.
   .btn.start-now keeps its own stronger raise, written where it is because it is the
   one button on the home screen that starts anything; these are the same idea a step
   quieter, so a screen of four of them does not read as four Start buttons. */
.btn {
  background-image: linear-gradient(180deg, rgba(255, 255, 255, 0.10), rgba(255, 255, 255, 0) 55%, rgba(0, 0, 0, 0.05));
  box-shadow: 0 3px 0 var(--raise-edge), 0 4px 14px rgba(0, 0, 0, 0.08);
}
/* Sinking onto the edge, in place of the scale .btn had. A scale on a full-width button
   moves every one of its edges inward at once, which on the results screen's stack read
   as the page flinching; the sink moves the button and nothing else. */
.btn:not(:disabled):active {
  transform: translateY(2px);
  box-shadow: 0 1px 0 var(--raise-edge), 0 2px 6px rgba(0, 0, 0, 0.08);
}
/* A button that cannot be pressed should not look raised: the loading "Next word", the
   consent dialog's pair while the choice is being written. .start-now is the exception,
   because its own rule says a disabled skeleton must look exactly like the live button
   rather than flash as the script arrives. */
.btn:disabled:not(.start-now) {
  background-image: none;
  box-shadow: none;
}
@media (hover: hover) {
  /* The one-pixel rise stays as it was; what changes is that the edge rises with it
     instead of being replaced by the halo — a hover that flattens a raised button is
     the pointer pressing it down, which is backwards. The two hard 3px rings became
     soft accent glows for the same reason the fills got a sheen: the ring was the one
     hard-edged effect left in the app. */
  .btn-primary:not(:disabled):hover {
    box-shadow: 0 4px 0 var(--raise-edge), 0 6px 20px var(--learn-glow);
  }
  .btn-ghost:not(:disabled):hover {
    box-shadow: 0 4px 0 var(--raise-edge), 0 6px 18px var(--learn-glow);
  }
  .btn-danger:not(:disabled):hover {
    box-shadow: 0 4px 0 var(--raise-edge), 0 6px 18px var(--error-soft);
  }
  /* Leaving stays the quietest control on the results screen — the rise and the fill,
     and the edge it already stands on, but no halo. */
  .results-actions .results-home:not(:disabled):hover {
    box-shadow: 0 4px 0 var(--raise-edge), 0 4px 14px rgba(0, 0, 0, 0.08);
  }
  /* The hover rules outrank the press rule above, so the press is written once more
     here for a pointer that is hovering and pressing at the same time. */
  .btn:not(:disabled):active {
    transform: translateY(2px);
    box-shadow: 0 1px 0 var(--raise-edge), 0 2px 6px rgba(0, 0, 0, 0.08);
  }
}

/* The answer buttons, and the small speaker beside each of them. The verdict fills and
   the tick and cross are untouched: what the raise adds is that an option looks like
   something to press before it is pressed. */
.answer-btn,
.answer-speak {
  background-image: linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0) 45%, rgba(0, 0, 0, 0.045));
  box-shadow: 0 2px 0 var(--raise-edge), 0 3px 10px rgba(0, 0, 0, 0.06);
}
.answer-btn {
  transition: background 0.2s, border-color 0.2s, color 0.2s, opacity 0.3s,
    transform 0.15s, box-shadow 0.15s;
}
.answer-speak { transition: border-color 0.2s, transform 0.15s, box-shadow 0.15s; }
.answer-btn:not(:disabled):active,
.answer-speak:active {
  transform: translateY(2px);
  box-shadow: 0 0 0 var(--raise-edge), 0 1px 4px rgba(0, 0, 0, 0.08);
}
/* Once the question is answered the round is settled: the marked answer keeps its
   verdict fill and every option lies flat on the page, so the row stops inviting a
   press it will no longer take. The speaker beside it stays raised — it is never
   disabled, because hearing the wrong options afterwards is the point of it. */
.answer-btn:disabled {
  background-image: none;
  box-shadow: none;
}
@media (hover: hover) {
  .answer-btn:not(:disabled):hover {
    transform: translateY(-1px);
    box-shadow: 0 3px 0 var(--raise-edge), 0 5px 14px var(--learn-glow);
  }
  .answer-speak:hover {
    transform: translateY(-1px);
    box-shadow: 0 3px 0 var(--raise-edge), 0 5px 14px rgba(0, 0, 0, 0.08);
  }
  .answer-btn:not(:disabled):active,
  .answer-speak:active {
    transform: translateY(2px);
    box-shadow: 0 0 0 var(--raise-edge), 0 1px 4px rgba(0, 0, 0, 0.08);
  }
}

/* The home screen's own question — the four answers under "Try one question" — and the
   "More options" summary under it. The four are not .answer-btn (the example is kept
   apart from the quiz in every other way too, so it shares no class with it), and they
   were the one place a first-time visitor meets an answer button at all. They sit on
   --bg inside a white card rather than on --card, so the edge under them is the same
   2px the quiz's options take and the sheen is the same. */
.home-example-choice,
.home-more-options > summary {
  background-image: linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0) 45%, rgba(0, 0, 0, 0.045));
  box-shadow: 0 2px 0 var(--raise-edge), 0 2px 8px rgba(0, 0, 0, 0.06);
  transition: background-color 0.2s, border-color 0.2s, transform 0.15s, box-shadow 0.15s;
}
.home-example-choice:not(:disabled):not([aria-disabled="true"]):active,
.home-more-options > summary:active {
  transform: translateY(2px);
  box-shadow: 0 0 0 var(--raise-edge), 0 1px 4px rgba(0, 0, 0, 0.08);
}
/* Answered, so no longer pressable — the quiz's rule exactly, and the verdict fill on
   the chosen one is what stays. */
.home-example-choice:disabled {
  background-image: none;
  box-shadow: none;
}
/* The two speakers on the card, raised like the answers beside them. Never
   disabled, exactly as .answer-speak is not: hearing the word again after the
   example is answered is the point of them. */
.home-example-speak {
  background-image: linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0) 45%, rgba(0, 0, 0, 0.045));
  box-shadow: 0 2px 0 var(--raise-edge), 0 2px 8px rgba(0, 0, 0, 0.06);
}
.home-example-speak:active {
  transform: translateY(2px);
  box-shadow: 0 0 0 var(--raise-edge), 0 1px 4px rgba(0, 0, 0, 0.08);
}
@media (hover: hover) {
  .home-example-choice:not(:disabled):not([aria-disabled="true"]):hover,
  .home-example-speak:hover,
  .home-more-options > summary:hover {
    transform: translateY(-1px);
    box-shadow: 0 3px 0 var(--raise-edge), 0 4px 12px rgba(0, 0, 0, 0.08);
  }
  .home-example-choice:not(:disabled):not([aria-disabled="true"]):active,
  .home-example-speak:active,
  .home-more-options > summary:active {
    transform: translateY(2px);
    box-shadow: 0 0 0 var(--raise-edge), 0 1px 4px rgba(0, 0, 0, 0.08);
  }
}

/* The round buttons in the sticky row at the top of every screen — Back, and the "i"
   that opens a tip — plus the speaker on the quiz card. Small controls, so the edge is
   a single pixel: 3px under a 42px square reads as a shadow the button is falling off. */
.icon-btn {
  background-image: linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0) 45%, rgba(0, 0, 0, 0.045));
  box-shadow: 0 2px 0 var(--raise-edge), 0 2px 8px rgba(0, 0, 0, 0.06);
  transition: border-color 0.15s, transform 0.15s, box-shadow 0.15s;
}
.icon-btn:active {
  transform: translateY(2px);
  box-shadow: 0 0 0 var(--raise-edge), 0 1px 4px rgba(0, 0, 0, 0.08);
}
@media (hover: hover) {
  .icon-btn:hover {
    border-color: var(--primary);
    transform: translateY(-1px);
    box-shadow: 0 3px 0 var(--raise-edge), 0 4px 12px rgba(0, 0, 0, 0.08);
  }
  .icon-btn:active {
    transform: translateY(2px);
    box-shadow: 0 0 0 var(--raise-edge), 0 1px 4px rgba(0, 0, 0, 0.08);
  }
}

/* The three bands under Start Learning Now — Customize, Advanced Learning, Install —
   on the tiles' raise, since they are the same kind of control at another shape. Their
   press is deliberately left alone: the band is held rather than moved and the pill
   inside it sinks, which is the decision recorded at .learn-cta:active. */
.learn-cta {
  background-image: linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0) 45%, rgba(0, 0, 0, 0.045));
  box-shadow: 0 3px 0 var(--raise-edge), var(--shadow);
  transition: border-color 0.15s, background-color 0.15s, transform 0.15s,
    box-shadow 0.15s;
}
@media (hover: hover) {
  .learn-cta:hover {
    transform: translateY(-1px);
    box-shadow: 0 4px 0 var(--raise-edge), var(--shadow);
  }
}

/* Advanced Learning: the two word panels and the three transport circles.
   The panel is a button — pressing it restarts the pair from that language — so it
   takes the tiles' raise. Its two other states are left as they were and simply
   restated with the edge included: the panel being read keeps its accent glow, and the
   panel that steps back onto the page ground while the other is read stays flat, which
   is what says it is the one not being read. */
.learn-panel {
  background-image: linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0) 45%, rgba(0, 0, 0, 0.045));
  box-shadow: 0 3px 0 var(--raise-edge), var(--shadow);
  transition: border-color 0.2s, box-shadow 0.2s, background-color 0.2s, transform 0.15s;
}
.learn-panel.is-active {
  box-shadow: 0 3px 0 var(--raise-edge), 0 0 24px var(--learn-glow), var(--shadow);
}
.learn-panels:not(.is-idle) .learn-panel:not(.is-active) {
  background-image: none;
  box-shadow: none;
}
.learn-panel:active {
  transform: translateY(2px);
  box-shadow: 0 1px 0 var(--raise-edge), 0 2px 6px rgba(0, 0, 0, 0.08);
}
/* Repeat, Play and Skip. The sink replaces the 0.93 scale the two small circles had, so
   they answer a press the way every other control in the app now does; Play keeps the
   accent ring that ties it to the panel being read, with the edge under it. */
.learn-transport-glyph {
  background-image: linear-gradient(180deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0) 50%, rgba(0, 0, 0, 0.05));
  box-shadow: 0 2px 0 var(--raise-edge), 0 2px 8px rgba(0, 0, 0, 0.06);
}
.learn-toggle .learn-transport-glyph {
  box-shadow: 0 0 0 8px var(--learn-glow), 0 3px 0 var(--raise-edge), 0 5px 14px rgba(0, 0, 0, 0.10);
}
.learn-transport-btn:active .learn-transport-glyph {
  transform: translateY(2px);
  box-shadow: 0 0 0 var(--raise-edge), 0 1px 4px rgba(0, 0, 0, 0.08);
}
.learn-toggle:active .learn-transport-glyph {
  box-shadow: 0 0 0 8px var(--learn-glow), 0 1px 0 var(--raise-edge), 0 2px 6px rgba(0, 0, 0, 0.10);
}
@media (hover: hover) {
  .learn-transport-btn:hover .learn-transport-glyph {
    transform: translateY(-1px);
    box-shadow: 0 3px 0 var(--raise-edge), 0 4px 12px rgba(0, 0, 0, 0.08);
  }
  .learn-toggle:hover .learn-transport-glyph {
    box-shadow: 0 0 0 8px var(--learn-glow), 0 4px 0 var(--raise-edge), 0 6px 16px rgba(0, 0, 0, 0.10);
  }
  .learn-transport-btn:active .learn-transport-glyph {
    transform: translateY(2px);
    box-shadow: 0 0 0 var(--raise-edge), 0 1px 4px rgba(0, 0, 0, 0.08);
  }
  .learn-toggle:active .learn-transport-glyph {
    box-shadow: 0 0 0 8px var(--learn-glow), 0 1px 0 var(--raise-edge), 0 2px 6px rgba(0, 0, 0, 0.10);
  }
}

/* The language picker's rows. These are list items rather than buttons — 73 of them in
   a scrolling dialog — so they take the shallowest version of all this: the hover fill
   they already had, now with a soft shadow to lift the row off the list, and the chosen
   one on an accent ring so it is still findable after the list has been scrolled. No
   raised edge, because 73 lips in a column is a corduroy pattern and not a set of
   buttons. */
.lang-picker-option {
  transition: background 0.15s, box-shadow 0.15s, transform 0.15s;
}
.lang-picker-option.is-active {
  box-shadow: inset 0 0 0 1.5px var(--primary);
}
@media (hover: hover) {
  .lang-picker-option:hover {
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
  }
  .lang-picker-option.is-active:hover {
    box-shadow: inset 0 0 0 1.5px var(--primary), 0 2px 8px rgba(0, 0, 0, 0.08);
  }
}
.lang-picker-option:active { transform: translateY(1px); }

/* The "what to do now" panel for when no voice is installed */
.voice-help { margin-top: 10px; }
.voice-help summary {
  cursor: pointer;
  color: var(--primary);
  font-weight: var(--weight-bold);
  font-size: 0.85rem;
  padding: 8px 0;
  list-style-position: inside;
}
.voice-help summary:hover { text-decoration: underline; }
.voice-help-body {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 14px 16px;
  margin-top: 8px;
  color: var(--text);
  font-size: 0.82rem;
  line-height: 1.7;
}
.voice-help-body ol { padding-inline-start: 18px; margin: 6px 0 10px; }
.voice-help-body li { margin-bottom: 4px; }
.voice-help-body p { margin: 6px 0; }
.voice-help-body code {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 1px 8px;
  display: inline-block;
  font-size: 0.9em;
}
.chips { display: flex; flex-wrap: wrap; gap: 10px; }

/* ===== Help =====
   The one screen written for someone who has not used the app yet, so it is built out
   of short blocks with an icon each rather than as a wall of text. The icons repeat the
   ones on the buttons being explained, colours included, which is what lets a reader
   find the paragraph belonging to the button in front of them.

   The body text is muted and the lead of each block is not, so a reader skimming the
   headings and the bold openings gets the whole app without reading a full sentence. */

/* The badge sits at the top of the paragraph rather than in the middle of it: the text
   runs to seven lines on a phone, and a badge centred against that floats loose in the
   card instead of opening it. */
.help-hero {
  display: flex;
  align-items: flex-start;
  gap: 14px;
  padding: 16px 18px;
}
.help-hero-icon {
  width: 46px;
  height: 46px;
  border-radius: 14px;
  background: var(--badge-blue-bg);
  color: var(--badge-blue);
  font-size: 1.5rem;
  font-weight: var(--weight-heavy);
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
.help-hero p { font-size: 0.9rem; line-height: 1.65; }

/* The one block on the page that is not about how to use the app but about what it is
   worth. It is built like a .help-item so that it belongs to the page, and it is set
   apart by the amber badge alone — a red panel would read as a fault in the app. */
.help-disclaimer {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 12px;
  align-items: start;
  padding: 16px 18px;
  margin-bottom: 4px;
}
.help-disclaimer h2 { font-size: 1rem; margin: 0 0 6px; }
.help-disclaimer p { color: var(--muted); font-size: 0.85rem; line-height: 1.6; }
.help-disclaimer p + p { margin-top: 8px; }
.help-disclaimer strong { color: var(--text); }
.help-disclaimer a { color: var(--primary); }
.help-icon-warn {
  background: var(--badge-amber-bg);
  color: var(--badge-amber);
  border-color: transparent;
  font-weight: var(--weight-heavy);
  font-size: 1.05rem;
}

.help-steps, .help-bullets {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding-inline-start: 22px;
}
.help-steps li, .help-bullets li {
  color: var(--muted);
  font-size: 0.86rem;
  line-height: 1.65;
}
.help-steps strong, .help-bullets strong { color: var(--text); }
/* The list markers are the only numbers on the screen, and they mark the three steps
   that start the app: they are worth seeing. */
.help-steps li::marker { color: var(--primary); font-weight: var(--weight-heavy); }
.help-note { font-size: 0.82rem; line-height: 1.6; margin-top: 14px; }

.help-list { display: flex; flex-direction: column; gap: 10px; }
.help-item {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 16px;
  padding: 14px 16px;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 12px;
  align-items: start;
}
.help-icon {
  width: 34px;
  height: 34px;
  border-radius: 11px;
  background: var(--bg);
  border: 1px solid var(--border);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.95rem;
  flex-shrink: 0;
}
/* The same colours as the home-screen tiles the blocks describe. The border goes
   transparent rather than being removed, so the badge keeps the size of a plain one and
   the two kinds line up down the column. */
.help-icon-fav { background: var(--badge-green-bg); color: var(--badge-green); border-color: transparent; }
.help-icon-improve { background: var(--badge-orange-bg); color: var(--badge-orange); border-color: transparent; }
.help-icon-mine { background: var(--badge-purple-bg); color: var(--badge-purple); border-color: transparent; }
.help-icon-stats { background: var(--badge-blue-bg); color: var(--badge-blue); border-color: transparent; }
.help-icon-set { background: var(--badge-green-bg); color: var(--badge-green); border-color: transparent; }
.help-icon-learn { background: var(--badge-amber-bg); color: var(--badge-amber); border-color: transparent; }
.help-item h3 { font-size: 0.95rem; font-weight: var(--weight-bold); margin-bottom: 4px; }
.help-item p { color: var(--muted); font-size: 0.85rem; line-height: 1.6; }

.help-defs { padding: 18px; }
.help-defs dt { font-weight: var(--weight-bold); font-size: 0.92rem; }
.help-defs dd { color: var(--muted); font-size: 0.85rem; line-height: 1.6; margin-top: 5px; }
.help-defs dd + dt { margin-top: 16px; }

/* The keys on the shortcuts list, drawn as key caps so that "press N" cannot be read as
   the letter N in the middle of a sentence. Built only out of tokens that already follow
   the theme — it has no colour of its own to keep in step when the palette changes, and
   the heavier bottom edge is the whole of the cap. The same rule covers the loose <kbd>
   in the paragraph below the card, which is why it hangs off the screen and not off
   .help-keys. */
#screen-help kbd {
  display: inline-block;
  min-width: 1.9em;
  padding: 2px 8px;
  border: 1px solid var(--control-border);
  border-bottom-width: 2px;
  border-radius: 7px;
  background: var(--bg);
  color: var(--text);
  font-family: inherit;
  font-size: 0.85em;
  font-weight: var(--weight-bold);
  line-height: 1.4;
  text-align: center;
}
/* "1 to 9" is a key, a word and a key: the row wraps as a unit on a narrow screen
   instead of the caps sitting flush against the word between them. */
.help-keys dt { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.help-keys dd { margin-top: 7px; }

.help-text { color: var(--muted); font-size: 0.86rem; line-height: 1.65; margin-top: 12px; }
.help-text a { color: var(--primary); }
.help-start { margin-top: 28px; }

/* ===== Confirmation dialogs ===== */
.modal-overlay {
  position: fixed;
  inset: 0;
  background: var(--overlay);
  backdrop-filter: blur(3px);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  z-index: 100;
  animation: overlay-in 0.2s ease;
}
body.modal-open { overflow: hidden; }
@keyframes overlay-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}
/* The dialog itself scrolls, rather than the overlay behind it. Centring a flex item
   that is taller than its container pushes the item out of both ends, and the half
   above the top edge cannot be scrolled back into view — on a 320×568 screen that put
   the consent text and the delete-confirmation buttons out of reach. Capping the height
   to the viewport (less the overlay's padding) means the item always fits, so nothing
   overflows and the buttons are always reachable by scrolling inside the card.
   The consent banner needs the height cap for a second reason: its overlay is
   pointer-events: none so that the page behind stays usable, which leaves the overlay
   unable to receive a scroll gesture at all — the card has to be the scroller. */
.modal {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-modal);
  padding: 28px 22px 22px;
  width: 100%;
  max-width: 360px;
  text-align: center;
  animation: modal-in 0.22s ease;
  max-height: calc(100vh - 48px);
  /* dvh follows the mobile browser's collapsing address bar; the vh line above is the
     fallback for browsers that do not know it. */
  max-height: calc(100dvh - 48px);
  overflow-y: auto;
  /* A scroll that reaches the end of the dialog stops there instead of scrolling the
     page behind it. */
  overscroll-behavior: contain;
}
@keyframes modal-in {
  from { opacity: 0; transform: translateY(14px) scale(0.96); }
  to   { opacity: 1; transform: none; }
}
.modal-icon {
  width: 58px;
  height: 58px;
  border-radius: 18px;
  background: var(--error-soft);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.6rem;
  margin: 0 auto 14px;
}
.modal-title { font-size: 1.2rem; font-weight: var(--weight-heavy); margin-bottom: 8px; }
.modal-text { font-size: 0.92rem; line-height: 1.6; }
/* The operator's message is typed text, and an address or a long word in it has no
   place to break: on a phone it ran past the dialog's edge, and the dialog — which
   scrolls — scrolled sideways under it. It breaks inside the word now, and only where it
   must. */
.message-text { overflow-wrap: anywhere; }

/* The operator's message window, redrawn 25 September 2026 when the operator asked for
   a nicer window, a choice of look and a countdown. A band in the tone's colour carries
   a drawn icon and the heading; the operator's words stand under it in the page's own
   ink, at 1rem rather than the dialogs' 0.92, since reading them is the whole point of
   the window. The tones borrow the badge pairs, which are a fill and the ink on it in
   both themes, so every tone clears 4.5:1 without a colour of its own. Written on
   classes and not on #message-overlay, so that the preview on visits.html — which loads
   this file — is drawn by the same rules as the real thing. */
.modal.message-card {
  --message-ink: var(--badge-green);
  --message-wash: var(--badge-green-bg);
  max-width: 400px;
  padding: 0;
  overflow-x: hidden;
}
.message-card[data-tone="important"] { --message-ink: var(--badge-amber); --message-wash: var(--badge-amber-bg); }
.message-card[data-tone="good"] { --message-ink: var(--badge-blue); --message-wash: var(--badge-blue-bg); }
.message-head {
  background: var(--message-wash);
  color: var(--message-ink);
  padding: 24px 22px 18px;
  border-bottom: 1px solid var(--border);
}
.message-badge {
  width: 60px;
  height: 60px;
  margin: 0 auto 12px;
  border-radius: 50%;
  background: var(--card);
  color: var(--message-ink);
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 3px 0 var(--raise-edge), 0 8px 18px rgba(16, 24, 20, 0.10);
}
.message-icon {
  display: none;
  width: 34px;
  height: 34px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.message-card[data-tone="news"] .message-icon-news,
.message-card[data-tone="important"] .message-icon-important,
.message-card[data-tone="good"] .message-icon-good { display: block; }
.message-head .message-title { color: var(--message-ink); margin: 0; }
/* The heading takes the focus only so that a screen reader starts there; it is not a
   control, and the ring the browser drew round it on opening read as a text box. */
.message-title:focus, .message-title:focus-visible { outline: none; }
.message-body { padding: 20px 22px 22px; }
.message-text { color: var(--text); }
.message-text .modal-text { font-size: 1rem; line-height: 1.6; }
.message-card.message-large .message-text .modal-text { font-size: 1.22rem; line-height: 1.5; }
.message-card.message-bold .message-text { font-weight: var(--weight-bold); }
/* Start, not left: each paragraph carries dir="auto", so a Hebrew line starts at the
   right and an English one at the left, whichever the interface is. */
.message-card.message-start .message-text { text-align: start; }
.message-body .modal-actions { margin-top: 20px; }
/* Close wears the tone too, so an amber window does not end on a green button. Only the
   colour moves — the raise's sheen and edge stay — and the ink is the tone's own wash,
   which is the pale half of the pair in the light theme and the dark half in the dark
   one, so the label clears 6:1 on the button in both. */
.message-card[data-tone="important"] .modal-actions .btn-primary,
.message-card[data-tone="good"] .modal-actions .btn-primary {
  background-color: var(--message-ink);
  color: var(--message-wash);
}

/* The countdown: how long is left in words, a thin bar that empties, and the button
   that turns it off. The bar is a CSS animation of the window's own length, paused with
   the words (.paused) while a mouse rests on the window or the tab is hidden. */
.message-timer { margin-top: 18px; }
.message-timer-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}
.message-timer-text {
  font-size: 0.86rem;
  color: var(--muted);
  text-align: start;
  font-variant-numeric: tabular-nums;
}
.message-keep {
  flex: 0 0 auto;
  min-height: 36px;
  padding: 6px 14px;
  border: 1.5px solid currentColor;
  border-radius: 99px;
  background: var(--card);
  color: var(--message-ink);
  font: inherit;
  font-size: 0.86rem;
  font-weight: var(--weight-bold);
  cursor: pointer;
  box-shadow: 0 2px 6px rgba(16, 24, 20, 0.10);
}
@media (hover: hover) {
  .message-keep:hover { background: var(--message-wash); }
}
.message-timer-track {
  height: 5px;
  margin-top: 10px;
  border-radius: 99px;
  background: var(--track);
  overflow: hidden;
}
.message-timer-bar {
  height: 100%;
  border-radius: inherit;
  background: var(--message-ink);
  transform-origin: left center;
}
:root[dir="rtl"] .message-timer-bar { transform-origin: right center; }
.message-timer-bar.running {
  animation-name: message-countdown;
  animation-timing-function: linear;
  animation-fill-mode: forwards;
}
.message-timer.paused .message-timer-bar { animation-play-state: paused; }
@keyframes message-countdown {
  from { transform: scaleX(1); }
  to   { transform: scaleX(0); }
}
/* The blanket reduced-motion rule runs every animation in 0.01ms, which would empty the
   bar on the first frame; the words still count down without it. */
@media (prefers-reduced-motion: reduce) {
  .message-timer-track { display: none; }
}
@media (max-width: 380px) {
  .message-head { padding: 20px 16px 14px; }
  .message-body { padding: 16px 16px 16px; }
  .message-badge { width: 52px; height: 52px; margin-bottom: 10px; }
  .message-icon { width: 28px; height: 28px; }
}
.modal-actions {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-top: 22px;
}
.modal-actions .btn { padding: 14px; font-size: 1.02rem; }

/* The entry code's card, since 16 September 2026 — a card inside the home screen, under
   the heading, and not a dialog over the page: the operator asked for the check to feel
   like part of the app rather than a pop-up. While it is up, everything else on the
   home screen is inert (app.js) and dimmed here, so the card is the one live thing on
   the page; the footer stays as it is, because reading the legal texts needs no code.
   The four digits are the largest thing in it, because they are what the visitor is
   asked to copy, and the field under them is drawn to their size so the copy reads
   against the original. Tabular figures keep the spacing even; the leading padding on
   the digits answers the trailing letter-spacing, so the run sits centred rather than a
   third of a digit to the left. No animation of its own, so the reduced-motion rule
   above already covers it. */
.entry-code-card {
  margin: 0 0 18px;
  padding: 18px 16px 16px;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  text-align: center;
}
.entry-code-title { font-size: 1.2rem; font-weight: var(--weight-heavy); margin: 0 0 6px; }
.entry-code-text { font-size: 0.92rem; line-height: 1.6; margin: 0; }
.entry-code-confirm { margin-top: 14px; }
.entry-code-note { font-size: 0.82rem; line-height: 1.5; margin: 12px 0 0; }
body.entry-code-open #screen-home > :not(#entry-code-card) { opacity: 0.38; }
.entry-code-value {
  font-size: 2.4rem;
  font-weight: var(--weight-heavy);
  letter-spacing: 0.3em;
  padding-inline-start: 0.3em;
  font-variant-numeric: tabular-nums;
  text-align: center;
  margin: 14px 0 12px;
}
/* The field is nearly the digits' own size and wide enough for all four with air on
   either side. It was 7ch at a smaller size, and with the letter-spacing the fourth
   digit scrolled out of sight as it was typed — the operator reported it the same day.
   Sized in em of its own font so the four spaced digits (about 3.4em) always fit
   inside the 15rem, on the narrowest phone too; the left padding answers the trailing
   letter-spacing, so the typed run sits centred. */
.entry-code-input {
  display: block;
  box-sizing: border-box;
  width: 100%;
  max-width: 15rem;
  margin: 0 auto;
  padding: 8px 16px 8px calc(16px + 0.3em);
  border: 2px solid var(--control-border);
  border-radius: var(--radius-btn);
  background: var(--card);
  color: var(--text);
  font: inherit;
  font-size: 2rem;
  line-height: 1.3;
  font-weight: var(--weight-heavy);
  letter-spacing: 0.3em;
  font-variant-numeric: tabular-nums;
  text-align: center;
}
.entry-code-input[aria-invalid="true"] { border-color: var(--error); }
.entry-code-error {
  color: var(--error);
  font-size: 0.92rem;
  line-height: 1.5;
  margin: 10px 0 0;
}

/* ===== Toast message ===== */
#toast {
  position: fixed;
  bottom: 24px;
  right: 50%;
  transform: translate(50%, 20px);
  max-width: 440px;
  width: calc(100% - 40px);
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius-btn);
  box-shadow: var(--shadow);
  padding: 14px 18px;
  text-align: center;
  font-size: 0.92rem;
  line-height: 1.5;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.3s, transform 0.3s;
  z-index: 50;
}
#toast.visible { opacity: 1; transform: translate(50%, 0); }

/* ===== The updated word list message ===== */
/* It sits at the top of the screen rather than the bottom, so that it does not cover the
   toast and the quiz buttons. */
#update-bar {
  position: fixed;
  top: 0;
  right: 0;
  left: 0;
  z-index: 60;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 14px;
  padding-top: calc(10px + env(safe-area-inset-top, 0px));
  background: var(--card);
  border-bottom: 1px solid var(--border);
  box-shadow: var(--shadow);
  transform: translateY(-100%);
  transition: transform 0.3s;
}
#update-bar.visible { transform: translateY(0); }
.update-text { flex: 1; min-width: 0; font-size: 0.9rem; line-height: 1.4; }
/* width: auto cancels the width: 100% of .btn — without it the button pushes the rest of
   the row out */
.update-reload { flex: 0 0 auto; width: auto; padding: 9px 15px; font-size: 0.88rem; white-space: nowrap; }
.update-close { flex: 0 0 auto; width: 38px; height: 38px; }

@media (max-width: 380px) {
  .update-text { font-size: 0.82rem; }
  .update-reload { padding: 9px 11px; font-size: 0.82rem; }
  /* On the narrowest phones the overlay's own padding costs more than it buys: giving
     it back to the dialog is what lets the delete-confirmation text and its two buttons
     sit on a 320×568 screen without scrolling at all. */
  .modal-overlay { padding: 12px; }
  .modal { padding: 22px 16px 16px; max-height: calc(100vh - 24px); max-height: calc(100dvh - 24px); }
  .modal-actions { margin-top: 16px; }
  .modal-actions .btn { padding: 12px; font-size: 0.98rem; }
  /* Three answer buttons on the narrowest phones. Fitting them across is a question of
     width, which is why it is settled here and not with the keyboard rules above — those
     are about the height the keyboard leaves. The side padding goes before the type does,
     being the cheaper of the two: the labels stay readable and Check, Hint and "I don't
     know" still sit side by side at 320px. */
  .type-actions .btn { padding: 13px 8px; font-size: 0.92rem; }
}

/* ===== Consent to local storage ===== */
.modal-text a { color: var(--primary); }
.consent-localized {
  margin-top: 14px;
  padding: 12px 14px;
  border: 1px solid var(--control-border);
  border-radius: var(--radius-btn);
  background: var(--bg);
  font-size: 0.9rem;
  line-height: 1.55;
}
.consent-localized p + p { margin-top: 8px; }
.consent-localized .modal-fineprint { margin-top: 10px; }
/* The line of legal consent — kept apart from the body of the message so that it does
   not look like a trailing part of it */
.modal-fineprint {
  margin-top: 12px;
  padding-top: 11px;
  border-top: 1px solid var(--border);
  font-size: 0.82rem;
  line-height: 1.5;
  color: var(--muted);
}
.modal-fineprint a {
  color: var(--primary);
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
}
#consent-overlay {
  background: none;
  backdrop-filter: none;
  align-items: flex-end;
  pointer-events: none;
  padding: 12px;
}
#consent-overlay .modal {
  pointer-events: auto;
  max-width: 560px;
  padding: 18px;
  /* This overlay's padding is 12px rather than 24px, so the card may be that much taller */
  max-height: calc(100vh - 24px);
  max-height: calc(100dvh - 24px);
}
#consent-overlay .modal-icon {
  width: 42px;
  height: 42px;
  border-radius: 12px;
  font-size: 1.2rem;
  margin-bottom: 8px;
}
#consent-overlay .modal-actions {
  flex-direction: row;
  margin-top: 14px;
}
#consent-overlay .modal-actions .btn {
  flex: 1;
  padding: 11px;
  font-size: 0.95rem;
}
.consent-setting-status { line-height: 1.65; margin-bottom: 12px; }
/* The consent button's own label, repeated under it in the reader's language. Smaller
   than the button and quieter than the status above it: it is a gloss on a control, not
   a second control, and the button it explains is directly above it. */
.consent-setting-i18n {
  margin: 8px 0 0;
  font-size: 0.9rem;
  line-height: 1.5;
}

/* ===== Compact home header and secondary tools ===== */
#screen-home .home-header { margin: 4px 0 14px; }
#screen-home .home-brand-row { margin-bottom: 6px; }
#screen-home .logo-link { border-radius: 14px; }
#screen-home .logo { width: 48px; height: 48px; border-radius: 14px; }
#screen-home .home-brand { font-size: 1.3rem; }
#screen-home .home-titles h1 { font-size: 1.55rem; }
#screen-home .lang-change-btn { padding: 10px 12px; }
#screen-home .section-head { margin-top: 16px; }
#screen-home .section-head h2 { margin-bottom: 8px; }
#screen-home .more-title { margin-bottom: 8px; }
#app-footer .site-footer { margin-top: 16px; }

/* The tile is a row at every width — the icon beside the label and not above it — and
   is kept as short as its own text allows. Tiles stacked as icon-above-label cards cost
   the home screen far more height; as rows they leave room for the footer on the first
   screen.

   The label sets the floor, and it is reached at 320px — the narrowest phone still in
   use. There the tile is 139px wide, and after the padding, the badge and the gap the
   text column is about 78px: "Favourites" fills nearly all of it at this size. Giving
   the badge or the padding any more room, or the name any more size, wraps that word
   onto a second line — which makes the whole row taller than it was before it was
   shrunk, and costs more height than it saves. The numbers below were measured at 320,
   360, 375, 390, 412 and 430px with every label the app can show; do the same before
   changing any of them. */
#screen-home .tiles { gap: 10px; }
#screen-home .tile {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-areas: "icon name" "icon sub";
  column-gap: 9px;
  align-items: center;
  padding: 8px 10px;
  /* The edge is paid for out of this side of the padding, so the badge, the gap and
     the text column all keep the widths measured above. */
  padding-inline-start: 7px;
  border-inline-start: 3px solid var(--tile-accent);
  text-align: start;
}
#screen-home .tile-icon {
  grid-area: icon;
  width: 32px;
  height: 32px;
  border-radius: 11px;
  font-size: 1rem;
  margin-bottom: 0;
}
#screen-home .tile-name { grid-area: name; align-self: end; font-size: 0.92rem; min-width: 0; }
#screen-home .tile-sub { grid-area: sub; align-self: start; font-size: 0.74rem; min-width: 0; }

/* 320px, the narrowest screen in use. The tile is 139px wide there, and the text column
   is what is left of it: with the padding, badge and gap above it comes to 76px, and
   "Favourites" alone is 71 of them. Five pixels is not a margin — a device whose
   system font measures a little wider wraps the word. The badge and the gaps give up
   those pixels here instead, taking the column to 81px, which clears every label the
   app can show by eight or more. Nothing gets smaller: at this width the text is
   already as small as it should go. */
@media (max-width: 359px) {
  #screen-home .tile { padding: 8px 9px; padding-inline-start: 6px; column-gap: 8px; }
  #screen-home .tile-icon { width: 30px; height: 30px; font-size: 0.95rem; }
}

@media (max-width: 479px) {
  .lang-picker-dialog { padding: 14px 12px 6px; }
  .lang-picker-tab { font-size: 0.82rem; padding: 8px 6px; }
}

/* Short screens — a little more tightening to avoid scrolling */
@media (max-height: 720px) {
  #screen-home .home-header { margin: 0 0 10px; }
  #screen-home .home-brand-row { margin-bottom: 4px; }
  #screen-home .logo-link { border-radius: 12px; }
  #screen-home .logo { width: 40px; height: 40px; border-radius: 12px; }
  #screen-home .home-brand { font-size: 1.15rem; }
  #screen-home .home-titles h1 { font-size: 1.35rem; }
  /* Change is one row on the first screen. The languages themselves live in the
     modal, so this block no longer has to shrink a wall of chips. */
  #screen-home .lang-change-btn { padding: 8px 10px; }
  #screen-home .section-head { margin-top: 10px; }
  /* The wide button opens its section the way the heading above the cards opens
     that one, so the two tighten together on a short screen. */
  #screen-home .start-now { margin-top: 10px; }
  #screen-home .learn-cta { margin-top: 8px; padding: 7px 10px; padding-inline-start: 7px; }
  #screen-home .more-title { margin-top: 10px; margin-bottom: 6px; }
  /* Only the padding gives way here. The badge and the two labels are already at the
     size the 320px measurement fixed, and shrinking them further is what makes a label
     wrap — a taller row, on the screen that can least afford one. */
  #screen-home .tiles { gap: 8px; }
  #screen-home .tile { padding: 6px 9px; padding-inline-start: 6px; }
  #screen-home .tile-icon { width: 30px; height: 30px; font-size: 0.95rem; }
  #app-footer .site-footer { margin-top: 10px; }
}

/* Round/set fetch status: the secret bank is no longer in the page, so a failed
   request cannot fall back to words.js. The API returns the round as one JSON value,
   but the quiz can still occupy its final shape while that value is in flight. */
.bank-status {
  margin: 24px 0;
  text-align: center;
}
.bank-status p { margin: 0 0 12px; }
#quiz-bank-status { margin: 0 0 16px; }
/* The animated shell is the visible loading affordance. Keep the same message in the
   accessibility tree without letting its temporary line push the shell down and then
   make the hydrated question jump upwards. Error text remains fully visible. */
#screen-quiz.is-bank-loading #quiz-bank-status {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.quiz-loading-shell { display: none; }
#screen-quiz.is-bank-loading .quiz-loading-shell { display: block; }

/* These are the real controls from the previous/next question. display:none removes
   them from both pointer and accessibility navigation while the inert shell stands in
   exactly their part of the layout. The exit button and progress row remain real. */
#screen-quiz.is-bank-pending > .quiz-card,
#screen-quiz.is-bank-pending > .answers,
#screen-quiz.is-bank-pending > .type-answer,
#screen-quiz.is-bank-pending > .flash-actions,
#screen-quiz.is-bank-pending > .type-reveal,
#screen-quiz.is-bank-pending > .quiz-feedback,
#screen-quiz.is-bank-pending > .quiz-next,
#screen-quiz.is-bank-pending > .quiz-bottom,
#screen-quiz.is-bank-pending > .quiz-limit {
  display: none !important;
}

.quiz-loading-shape {
  position: relative;
  overflow: hidden;
  background: var(--track);
  border-color: var(--border);
}
.quiz-loading-shape::after {
  content: "";
  position: absolute;
  inset: 0;
  transform: translateX(-105%);
  background: linear-gradient(90deg, transparent, var(--skeleton-shine), transparent);
  animation: quiz-shell-shimmer 1.25s ease-in-out infinite;
}
.quiz-loading-star {
  position: absolute;
  top: 15px;
  inset-inline-end: 17px;
  width: 22px;
  height: 22px;
  border-radius: 50%;
}
.quiz-loading-word,
.quiz-loading-score {
  display: block;
  border-radius: 99px;
}
.quiz-loading-word {
  width: min(64%, 260px);
  height: 54px;
  margin: 0 auto;
}
.quiz-loading-details {
  white-space: nowrap;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
}
.quiz-loading-limit,
.quiz-loading-next { display: none; border-radius: 99px; }
#screen-quiz.is-bank-loading.quiz-timed .quiz-loading-limit {
  display: block;
  width: 180px;
  height: 20px;
  margin: 12px auto 2px;
}
.quiz-loading-choices {
  display: none;
  flex-direction: column;
  gap: 12px;
}
#screen-quiz.is-bank-loading:not(.quiz-typing):not(.quiz-flash) .quiz-loading-choices {
  display: flex;
}
.quiz-loading-answer-row { min-height: 58px; }
.quiz-loading-choice {
  flex: 1;
  min-width: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius-btn);
}
.quiz-loading-speak {
  flex: 0 0 52px;
  width: 52px;
  border: 1px solid var(--border);
  border-radius: var(--radius-btn);
}
.quiz-loading-type { display: none; }
#screen-quiz.is-bank-loading.quiz-typing .quiz-loading-type { display: block; }
.quiz-loading-flash { display: none; }
#screen-quiz.is-bank-loading.quiz-flash .quiz-loading-flash { display: block; }
.quiz-loading-input {
  display: block;
  height: 54px;
  border: 1px solid var(--border);
}
.quiz-loading-actions { margin-top: 10px; }
.quiz-loading-actions .quiz-loading-shape {
  min-height: 48px;
  border: 1px solid var(--border);
}
/* Mirrors the two reserved outcome lines the shell stands in for, at their real sizes:
   12px margin + the reveal's 33.6px (--answer-size 1.4rem × 1.5) + 12px margin + the
   feedback caption's 20.4px (0.85rem × 1.5em slot) = 78px. Nothing else couples this
   number to those rules, so quiz-shell-hydration.test.js re-derives it from the
   stylesheet — change either side and the test names the other. */
.quiz-loading-type-outcomes { height: 78px; }
#screen-quiz.is-bank-loading.quiz-manual .quiz-loading-next {
  display: block;
  width: 100%;
  height: 50px;
  margin-top: 10px;
}
.quiz-loading-score {
  width: 150px;
  height: 14px;
  margin: 10px auto 0;
}

@keyframes quiz-shell-shimmer {
  to { transform: translateX(105%); }
}
@media (prefers-reduced-motion: reduce) {
  .quiz-loading-shape::after { animation: none !important; }
}
#screen-learn.is-bank-pending #learn-play,
#screen-learn.is-bank-pending #learn-end {
  visibility: hidden;
}
.levels-status {
  margin: 10px 0 0;
  text-align: center;
}
.levels-status .btn { margin-top: 8px; width: auto; }
.bank-pending-note { margin: 0 0 12px; }

/* The listening band, under the Customize band since 6 September 2026 and under the
   level cards before that: Advanced Learning, which until
   5 September 2026 was a tile in the grid further down, in the grid's white and at the
   grid's size (the comment on it in index.html says why it moved). It takes the width
   of Start Learning Now above, so the two read as the two ways to begin, and the amber
   of its badge, so it is the second of them and not a rival to the green. The badge,
   the edge and the trailing pill are the one amber pair the level-2 number and the Help
   badge already use; the pill turns the pair round - the ink as fill, the fill as ink,
   5.8:1 in the light theme and 8.7:1 in the dark - and is the only solid amber on the
   screen, so an eye scanning for a button lands on it before it has read the name.
   Grid and not flex so the two lines of text can wrap in their own column while the
   badge and the pill stay centred on the whole; minmax(0, 1fr) for the reason .levels
   gives. The edge is paid for out of that side's padding, as the tiles pay for theirs. */
.learn-cta {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  column-gap: 10px;
  align-items: center;
  width: 100%;
  margin-top: 12px;
  padding: 10px 12px;
  padding-inline-start: 9px;
  background: var(--card);
  color: inherit;
  border: 1px solid var(--control-border);
  border-inline-start: 4px solid var(--badge-amber);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  text-align: start;
  transition: border-color 0.15s, background-color 0.15s;
  /* The pill's fill, ink and hover halo are three custom properties on the band, so the
     two coloured bands below change three values rather than three rules - and no rule
     sets the background shorthand, which would wipe the sheen the pill wears. */
  --go-fill: var(--badge-amber);
  --go-ink: var(--badge-amber-bg);
  --go-glow: var(--learn-cta-glow);
}
.learn-cta-icon {
  width: 40px;
  height: 40px;
  border-radius: 12px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.15rem;
  background: var(--badge-amber-bg);
  color: var(--badge-amber);
}
.learn-cta-text { min-width: 0; }
.learn-cta-name { display: block; font-size: 1.05rem; font-weight: var(--weight-heavy); line-height: 1.25; }
.learn-cta-sub { display: block; color: var(--muted); font-size: 0.8rem; line-height: 1.35; margin-top: 2px; }
.learn-cta-go {
  padding: 9px 14px;
  border-radius: 999px;
  background-color: var(--go-fill);
  color: var(--go-ink);
  font-size: 0.86rem;
  font-weight: var(--weight-bold);
  white-space: nowrap;
  /* Raised, since 6 September 2026: the operator said "Listen now", "Install" and
     "Open" did not look like buttons - a flat pill reads as a badge, the finding that
     had already raised the flat green rectangle above. So the pill takes what
     .btn.start-now took: a light sheen over the fill, a solid darker edge under it and
     a soft drop shadow, in black and white at low alpha so each band's own colour stays
     its own in both themes. The press sinks it onto the edge; hover lifts it and puts
     the halo it had before on top of the edge rather than in place of it. */
  background-image: linear-gradient(180deg, rgba(255, 255, 255, 0.22), rgba(255, 255, 255, 0) 55%, rgba(0, 0, 0, 0.08));
  border: 1px solid rgba(0, 0, 0, 0.22);
  box-shadow: 0 3px 0 rgba(0, 0, 0, 0.28), 0 4px 10px rgba(0, 0, 0, 0.16);
  text-shadow: 0 1px 0 rgba(0, 0, 0, 0.12);
  transition: box-shadow 0.2s, transform 0.15s;
}
/* Hover sits behind (hover: hover) for the reason given at .answer-btn: a touch screen
   has no pointer, and would leave the pill lifted on whichever band was last pressed. */
@media (hover: hover) {
  .learn-cta:hover { border-color: var(--badge-amber); }
  .customize-cta:hover { border-color: var(--badge-blue); }
  .install-cta:hover { border-color: var(--badge-purple); }
  .play-cta:hover { border-color: var(--badge-orange); }
  .learn-cta:hover .learn-cta-go {
    transform: translateY(-1px);
    box-shadow: 0 4px 0 rgba(0, 0, 0, 0.28), 0 7px 14px rgba(0, 0, 0, 0.18), 0 0 0 3px var(--go-glow);
  }
}
/* The band is held rather than shrunk, like a level card: the screen changes on release,
   and a last-frame scale read as a shake. The pill alone moves, down onto its edge, the
   way Start Learning Now does. After the hover block so that a mouse press sinks the
   pill rather than lifting it, the two rules being of one specificity. */
.learn-cta:active { background: var(--track); }
.learn-cta:active .learn-cta-go {
  transform: translateY(2px);
  box-shadow: 0 1px 0 rgba(0, 0, 0, 0.28), 0 2px 5px rgba(0, 0, 0, 0.12);
}
/* Three bands in a row - since the evening of 6 September 2026 in the order Customize,
   Advanced Learning, Install, at the operator's request (the comments on them in
   index.html say why each is where it is). The first stands the section's 12px under
   the box; each band after it sits 8px under the one before, closer to its neighbour
   than to anything else, so the three read as one group. A sibling rule rather than a
   margin on each coloured band, so the order can change again without a margin moving
   with the wrong band - which is what a margin on .customize-cta did when it went first. */
.learn-cta + .learn-cta { margin-top: 8px; }
/* The Customize band, first of the three since 6 September 2026 (the comment on it in
   index.html says why it left the tile grid, and then why it went first): the same
   shape, in the blue the chart and the level-3 number already use - the third colour of
   the three bands, after the green button and the amber band, and not a way to start a
   round, so not a rival to either. */
.customize-cta {
  border-inline-start-color: var(--badge-blue);
  --go-fill: var(--badge-blue);
  --go-ink: var(--badge-blue-bg);
  --go-glow: var(--badge-blue-bg);
}
.customize-cta .learn-cta-icon { background: var(--badge-blue-bg); color: var(--badge-blue); }
/* The Install band, last of the three since 6 September 2026 (the comment on it in
   index.html says why it left the tile grid): the same shape again, in the purple no
   other band wears, so that three bands in a row are three colours and not two blues
   touching. */
.install-cta {
  border-inline-start-color: var(--badge-purple);
  --go-fill: var(--badge-purple);
  --go-ink: var(--badge-purple-bg);
  --go-glow: var(--badge-purple-bg);
}
.install-cta .learn-cta-icon { background: var(--badge-purple-bg); color: var(--badge-purple); }
/* The Play and learn band, since 17 September 2026, at the operator's request that the
   games be prominent rather than a tile under More options: on the home screen between
   the Change Languages pill and More options, and again at the end of the Customize
   screen's Exercise card. The same shape, in the orange no other band wears, so four
   bands are four colours; the pill's fill, ink and halo move through the three custom
   properties the band declares, as the two coloured bands above do. */
.play-cta {
  border-inline-start-color: var(--badge-orange);
  --go-fill: var(--badge-orange);
  --go-ink: var(--badge-orange-bg);
  --go-glow: var(--badge-orange-bg);
}
.play-cta .learn-cta-icon { background: var(--badge-orange-bg); color: var(--badge-orange); }
/* The vocabulary-check band, since 18 September 2026, directly under Start Learning Now.
   The operator asked for a button that cannot be missed, in a colour nothing else on
   the page wears, so this is the one band that is filled: rose under the pale rose the
   other bands keep for their badge, and the pill the other way round. Three things the
   fill changes, each measured rather than assumed. The description cannot stay
   --muted, which is 1.2:1 on rose, so it takes the band's own ink a shade quieter. The
   pill cannot keep the 0.22 sheen its siblings wear: over the dark theme's deep pill
   that lightens the top of it until the pale ink stands at 3.6:1, so the sheen here is
   0.10. And the press and the busy state cannot fall back to --track, which is grey
   under pale pink text; they darken the fill instead. background-color, never the
   shorthand, so the band keeps the faint sheen every raised band has. The coloured
   edge the other bands wear would be rose on rose, so the border is one even line and
   the padding gives the three pixels back, which keeps the badge in the others'
   column. After .learn-cta:active and the busy rule on purpose: the selectors are of
   one specificity and the later one wins. */
.levelcheck-cta {
  padding-inline-start: 12px;
  background-color: var(--badge-rose);
  color: var(--badge-rose-bg);
  border: 1px solid rgba(0, 0, 0, 0.22);
  --go-fill: var(--badge-rose-bg);
  --go-ink: var(--badge-rose);
  --go-glow: var(--badge-rose-bg);
}
.levelcheck-cta .learn-cta-icon { background: var(--badge-rose-bg); color: var(--badge-rose); }
.levelcheck-cta .learn-cta-sub { color: inherit; opacity: 0.92; }
.levelcheck-cta .learn-cta-go {
  background-image: linear-gradient(180deg, rgba(255, 255, 255, 0.10), rgba(255, 255, 255, 0) 55%, rgba(0, 0, 0, 0.08));
}
@media (hover: hover) {
  .levelcheck-cta:hover { border-color: rgba(0, 0, 0, 0.3); }
}
.levelcheck-cta:active,
.levelcheck-cta[aria-busy="true"] {
  background-color: var(--badge-rose);
  background-image: linear-gradient(rgba(0, 0, 0, 0.14), rgba(0, 0, 0, 0.14));
}
@media (max-width: 359px) {
  .levelcheck-cta { padding-inline-start: 10px; }
}
/* Switched off on the Customize screen (display.levelcheck): theme.js sets the attribute
   before the first paint and app.js keeps it in step. */
:root[data-levelcheck="off"] #tile-levelcheck { display: none; }

/* ===== The vocabulary check's own screen =====
   The question is the home example's question - the same word line, the same four
   boxes, the same framed verdict - so it borrows that card's classes and adds only what
   the example has no use for: a line saying which question this is, the quiet
   "I don't know", and the result. The word is not a button here (nothing on this screen
   speaks), so it gives back the pointer the example's word takes. The suggested level
   is the one loud thing on the result, in the band's rose so the two ends of the check
   read as one; the caution under it is in the page's own ink and framed, never
   --muted, because it is the sentence that says what the number above it is not. */
.levelcheck-card { margin-top: 12px; padding: 16px; }
.levelcheck-card[aria-busy="true"] .home-example-skeleton { display: block; }
.levelcheck-card :is(.home-example-status, .levelcheck-progress):empty { display: none; }
/* How far through: a track that fills with every answer, cut into thirds where the level
   changes. The fill runs from the start of the reading direction, so it grows leftwards
   in Hebrew and Arabic; the two cuts sit at a third and two thirds, which are the same
   places either way round. */
.levelcheck-meter {
  position: relative;
  height: 6px;
  margin: 2px 0 10px;
  border-radius: 99px;
  background: var(--track);
  overflow: hidden;
}
.levelcheck-meter-fill {
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  width: 0;
  background: var(--badge-rose);
  transition: width 0.25s ease-out;
}
.levelcheck-meter::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg,
    transparent calc(100% / 3 - 1px), var(--card) calc(100% / 3 - 1px),
    var(--card) calc(100% / 3 + 1px), transparent calc(100% / 3 + 1px),
    transparent calc(200% / 3 - 1px), var(--card) calc(200% / 3 - 1px),
    var(--card) calc(200% / 3 + 1px), transparent calc(200% / 3 + 1px));
}
.levelcheck-progress {
  margin: 0;
  color: var(--muted);
  font-size: 0.85rem;
  line-height: 1.5;
  text-align: center;
}
.levelcheck-progress-level { color: var(--badge-rose); font-weight: var(--weight-bold); }
.levelcheck-word { cursor: default; }
.levelcheck-skip { margin-top: 10px; padding: 12px; font-size: 1rem; }
.levelcheck-next { margin-top: 12px; }
.levelcheck-result-title {
  margin: 0;
  color: var(--muted);
  font-size: 1rem;
  font-weight: var(--weight-medium);
  line-height: 1.4;
  text-align: center;
}
.levelcheck-level {
  margin: 4px 0 0;
  color: var(--badge-rose);
  font-size: clamp(1.7rem, 7vw, 2.2rem);
  font-weight: var(--weight-heavy);
  line-height: 1.25;
  text-align: center;
}
.levelcheck-level-desc { margin: 2px 0 0; text-align: center; }
/* What the suggestion rests on, in the page's own ink, then the score in all. */
.levelcheck-why {
  margin: 10px 0 0;
  font-size: 0.95rem;
  line-height: 1.5;
  text-align: center;
  overflow-wrap: anywhere;
}
.levelcheck-total {
  margin: 10px 0 0;
  font-size: 1.05rem;
  font-weight: var(--weight-bold);
  text-align: center;
}
/* One row a level: the score in words, and one mark per question in the order asked,
   filled where the answer was right. The marks are a picture of the same numbers, so
   they are hidden from assistive technology and nothing depends on their colour alone. */
.levelcheck-scores {
  display: grid;
  gap: 6px;
  margin: 12px 0 0;
  padding: 0;
  list-style: none;
}
.levelcheck-score {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 6px 12px;
  padding: 8px 12px;
  border: 1px solid var(--border);
  border-radius: 10px;
  font-size: 0.95rem;
  line-height: 1.4;
  overflow-wrap: anywhere;
}
.levelcheck-score.suggested {
  border-color: var(--badge-rose);
  box-shadow: inset 0 0 0 1px var(--badge-rose);
}
.levelcheck-score.suggested .levelcheck-score-text { font-weight: var(--weight-bold); }
.levelcheck-marks { display: inline-flex; gap: 4px; }
.levelcheck-mark {
  width: 12px;
  height: 12px;
  border-radius: 4px;
  border: 1px solid var(--control-border);
  background: transparent;
}
.levelcheck-mark.right { background: var(--badge-rose); border-color: var(--badge-rose); }
.levelcheck-caution {
  margin: 14px 0 0;
  padding: 10px 12px;
  background: var(--badge-rose-bg);
  border-inline-start: 3px solid var(--badge-rose);
  border-radius: 10px;
  font-size: 0.88rem;
  line-height: 1.5;
  overflow-wrap: anywhere;
}
.levelcheck-caution a { color: inherit; font-weight: var(--weight-bold); text-decoration: underline; }
.levelcheck-start { margin-top: 14px; }
.levelcheck-start-note {
  margin: 8px 0 0;
  font-size: 0.85rem;
  line-height: 1.5;
  text-align: center;
}

/* ===== The daily quiz =====
   Since 21 September 2026. A band on the home screen that opens a window, whose question
   is the home example's and the vocabulary check's — the same word line, four boxes and
   framed verdict — so the card borrows their classes and adds a heading, a count line,
   the result's list of words and the band. Amber, the one badge colour no band wore
   before, so the two quizzes on the home screen are not read as one. The window is the
   card's frame, so the card draws none of its own. */
.daily-card { text-align: start; }
.daily-title { margin: 0; font-size: 1.05rem; line-height: 1.4; }
.daily-sub {
  margin: 4px 0 0;
  color: var(--badge-amber);
  font-size: 0.85rem;
  font-weight: var(--weight-bold);
  line-height: 1.5;
}
.daily-card[aria-busy="true"] .home-example-skeleton { display: block; }
.daily-card :is(.home-example-status, .daily-progress, .daily-sub):empty { display: none; }
.daily-progress {
  margin: 10px 0 0;
  color: var(--muted);
  font-size: 0.85rem;
  line-height: 1.5;
  text-align: center;
}
.daily-word { cursor: default; }
.daily-skip { margin-top: 10px; padding: 12px; font-size: 1rem; }
.daily-next { margin-top: 12px; }
.daily-accept { margin: 12px 0 0; text-align: center; }
.daily-score {
  margin: 12px 0 0;
  font-size: 1.15rem;
  font-weight: var(--weight-heavy);
  text-align: center;
}
/* Every word of the quiz with its answer, in the order asked, and a mark: the list is
   what makes the score worth reading, and it is the whole of what the visitor takes
   away — nothing of it is kept. */
.daily-review {
  display: grid;
  gap: 6px;
  margin: 12px 0 0;
  padding: 0;
  list-style: none;
}
.daily-review-row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 8px;
  padding: 8px 12px;
  border: 1px solid var(--border);
  border-radius: 10px;
  font-size: 0.95rem;
  line-height: 1.4;
  overflow-wrap: anywhere;
}
.daily-review-row.right { border-color: var(--success); }
.daily-review-row.missed { border-color: var(--error); }
.daily-review-mark { font-weight: var(--weight-bold); }
.daily-review-row.right .daily-review-mark { color: var(--success); }
.daily-review-row.missed .daily-review-mark { color: var(--error); }
.daily-review-ask { font-weight: var(--weight-bold); }
.daily-review-sep { color: var(--muted); }
.daily-result-note {
  margin: 12px 0 0;
  font-size: 0.85rem;
  line-height: 1.5;
  text-align: center;
}
.daily-result-note a { color: var(--primary); font-weight: var(--weight-bold); text-decoration: underline; }
.daily-retry { margin-top: 12px; min-height: 44px; }
/* The window is wider than a confirmation, because four answers stand in two columns
   inside it, and it keeps the modal's own scroll for a long result on a short screen. */
.daily-modal { max-width: 480px; text-align: start; padding: 22px 18px 18px; }
.daily-close { margin-top: 14px; }
/* The band: the Play and learn band's shape, edged in amber. */
.daily-cta {
  border-inline-start-color: var(--badge-amber);
  --go-fill: var(--badge-amber);
  --go-ink: var(--badge-amber-bg);
  --go-glow: var(--badge-amber-bg);
}
.daily-cta .learn-cta-icon { background: var(--badge-amber-bg); color: var(--badge-amber); }
.help-icon-daily { background: var(--badge-amber-bg); color: var(--badge-amber); border-color: transparent; }
.levelcheck-retry,
.levelcheck-home { margin-top: 10px; padding: 12px; font-size: 1rem; }
/* 320px: the pill and the badge give up a few pixels so the description keeps a column
   wide enough to wrap in two or three lines rather than one word to a line. */
@media (max-width: 359px) {
  .learn-cta { column-gap: 8px; padding: 9px 10px; padding-inline-start: 7px; }
  .learn-cta-icon { width: 34px; height: 34px; font-size: 1rem; }
  .learn-cta-go { padding: 7px 11px; font-size: 0.8rem; }
}

/* ===== Tips: the short explanation a control carries =====
   Every control that is not self-explaining says what it does, in a bubble opened by the
   small "i" beside it, by hovering, or by reaching the control with the Tab key.

   This exists because the obvious mechanism is unusable here. The title attribute was
   removed from this app twice, and index.html still carries the two comments saying
   why: a title is a mouse tooltip, so it never reaches a phone or a keyboard at all,
   and some screen readers read it instead of the visible text it was meant to add to.
   Nothing about the elements below uses one.

   Three ways in, because no single one covers everybody. Hover is for a mouse. Focus is
   for a keyboard. And the "i" is the one that needs no gesture at all — it is drawn on
   the screen, it is 44px to the finger however small it looks, and it is a real button,
   so it is reachable by tab like everything else. It is also the only one a touch screen
   has: there is deliberately no press-and-hold, and the long comment above initTips in
   app.js says which already-fixed bug that gesture would bring back.

   The bubble is a singleton: one element, created once, moved to whichever control is
   asking. Two hundred controls do not get two hundred hidden divs, and there can never
   be two bubbles open to disagree with each other. */
/* The home screen carried a second Tooltips control until 5 September 2026 — a switch
   under the pair of languages, styled here as .tips-switch. It went when the operator
   asked for the home screen to hold no options; the chips on the Customize screen are
   the one control now, and they are styled with every other chip row. */
/* ===== Off until they are asked for =====
   "Tips" on the settings screen writes data-tips="on" onto the root element, and until it
   does there is nothing here to see: not a bubble, and not one of the small "i" marks
   that open them. The app looks and behaves exactly as it did before this feature
   existed.

   Hidden here as well as gated in the script, and the two are not redundant. The script
   refuses to open a bubble, which covers the behaviour; this covers the picture — the
   seventeen "i" marks written into index.html are in the document from the first paint,
   before app.js has been parsed at all, and without this rule they would be drawn on the
   first frame of every visit and then vanish. theme.js sets the attribute in the <head>
   for the same reason the colours are set there: to be earlier than that frame. */
:root:not([data-tips="on"]) .tip,
:root:not([data-tips="on"]) .tip-btn { display: none !important; }

.tip {
  position: fixed;
  /* Above the dialog overlay (100), the update bar (60), the toast (50) and the sticky
     screen-top row (20). A tip opened on a button inside a confirmation dialog has to
     be readable, and the dialog is the highest thing the app otherwise draws. */
  z-index: 300;
  left: 0;
  top: 0;
  /* min() and not a media query: the bubble has to fit a 320px phone with the page's
     own 16px gutters on both sides, and be no wider than a comfortable line anywhere
     else. */
  max-width: min(280px, calc(100vw - 32px));
  box-sizing: border-box;
  padding: 9px 13px;
  border-radius: 14px;
  background: var(--tip-bg);
  color: var(--tip-ink);
  border: 1px solid var(--tip-border);
  box-shadow: var(--tip-shadow);
  font-size: 0.84rem;
  font-weight: var(--weight-normal);
  line-height: 1.45;
  text-align: start;
  display: flex;
  gap: 8px;
  align-items: flex-start;
  /* Inert on a touch screen. A bubble that opens under a finger and then swallows the
     tap meant for the button beneath it is a worse fault than no explanation at all,
     and the app's own presses are taken from pointerup, which no ghost-click window
     would rescue. */
  /* The bubble never takes a press, on any device. It sits ten pixels from the control
     it explains, which on a column of answers or chips is inside the gap — so a bubble
     that could be clicked would swallow the press aimed at the button above it, and the
     app's own presses come from pointerup, which no ghost-click window would rescue.
     WCAG 1.4.13 asks that hover-revealed content can be reached with the pointer without
     vanishing, and that is answered in the script instead: it hit-tests the bubble's
     rectangle on pointermove and holds it open while the pointer is inside it. Nothing
     is lost by the element itself being inert. */
  pointer-events: none;
  animation: tip-in 0.14s ease;
}
/* The bubble is closed by setting its hidden attribute, and without this line that does
   nothing at all: the browser's own [hidden] rule is display: none, but it is a single
   attribute selector and .tip above is a class, which outranks it. The first bubble
   opened therefore stayed on the screen for the rest of the visit — Escape, a press
   elsewhere and switching the tips off all ran, all cleared the state behind it, and none
   of them made it go away. */
.tip[hidden] { display: none; }
@keyframes tip-in {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 1; transform: none; }
}
.tip-glyph {
  flex-shrink: 0;
  font-size: 0.95rem;
  line-height: 1.3;
  color: var(--tip-glyph);
}
.tip-text { min-width: 0; }

/* The arrow. A square turned 45° with two of its four borders taken off, so what is
   left is the two edges that face away from the bubble — which is the whole of the
   arrow that is ever seen. Drawn on the bubble itself rather than as another element,
   and it inherits both theme colours, so nothing here has to be told the theme changed.
   --tip-x is written by the script: the arrow sits under the centre of the control,
   which is not the centre of the bubble once the bubble has been pushed away from the
   edge of a narrow screen. */
.tip::after {
  content: "";
  position: absolute;
  width: 10px;
  height: 10px;
  left: var(--tip-x, 50%);
  margin-left: -5px;
  background: var(--tip-bg);
  border: 1px solid var(--tip-border);
  transform: rotate(45deg);
}
.tip[data-place="top"]::after {
  bottom: -6px;
  border-top: 0;
  border-left: 0;
}
.tip[data-place="bottom"]::after {
  top: -6px;
  border-bottom: 0;
  border-right: 0;
}

/* Which control the bubble belongs to. On a crowded screen — a row of chips, a grid of
   tiles — a bubble floating above the row does not say which of them it is explaining,
   and the pointer is not the answer on a touch screen, where there is no pointer left
   on the glass to look at. */
/* An outline and nothing else. The first version set border-radius: 4px here as well,
   which is the same specificity as every control's own radius and later in the file than
   all of them — so a chip lost its pill, a tile lost its 20px corners and an icon button
   lost its 13px, for as long as the bubble was open and then back again. An outline
   already follows the shape of the box it is drawn on. */
.tip-anchor-open {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
}

/* ===== The "i" that opens one =====
   An "i" and not a "?", which is this app's mark for the Help screen — on the home
   tile, on the help hero, and on the keyboard shortcut. Two different things must not
   share a glyph: "?" goes to the page that explains the app, and "i" explains the one
   control it stands beside. */
.tip-btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  flex-shrink: 0;
  padding: 0;
  border-radius: 50%;
  border: 1px solid var(--tip-btn-border);
  background: var(--tip-btn-bg);
  color: var(--tip-btn-ink);
  font-size: 0.72rem;
  font-weight: var(--weight-bold);
  font-style: italic;
  line-height: 1;
  vertical-align: middle;
  margin-inline-start: 7px;
  transition: background 0.15s, border-color 0.15s, color 0.15s;
}
/* 22px is what it looks like; 44px is what it is. WCAG 2.5.8 asks for a target of at
   least 24px and 2.5.5 for 44px where it can be had, and a mark this quiet has to be
   the second of those or it is decoration that cannot be pressed. The box is invisible
   and sits on top of nothing but the heading's own white space — these stand beside
   headings and at the end of rows, never between two other controls. */
.tip-btn::before {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 44px;
  height: 44px;
  transform: translate(-50%, -50%);
}
.tip-btn:hover {
  border-color: var(--primary);
  color: var(--primary);
}
.tip-btn[aria-expanded="true"] {
  background: var(--primary);
  border-color: var(--primary);
  color: var(--on-primary);
}
/* A heading and its "i" are one line. The mark goes at the end of the words rather than
   at the end of the row, so it is beside what it explains and not adrift on the far
   side of the screen.

   It is a wrapper and not a child of the heading, which matters more than it looks:
   half of these headings are the accessible name of the chip row beneath them, through
   aria-labelledby. A button inside one of them would have its own name folded into the
   heading's, and the settings row would be announced "Colours About colours". */
.tip-head {
  display: flex;
  align-items: center;
  margin-bottom: 12px;
}
/* The heading's bottom margin moves up to the wrapper. Left on the heading it becomes
   part of the box the "i" is centred against — flexbox centres a margin box, not a line
   of text — and the mark sits visibly below the words it belongs to. The top margin stays
   where it is: a flex item keeps it, and .settings-group-title's 26px is the space
   between one settings group and the next. */
.tip-head > h2,
.tip-head > h3 { margin-bottom: 0; }
/* The compact home screen sets its own tighter spacing under these two headings, and it
   does it through #screen-home — an id, which outranks the class rule above and left the
   heading's 8px in place with the wrapper's 12px added under it. Thirty pixels appeared
   on a screen whose whole section of this file exists to fit it into one. */
#screen-home .tip-head > h2 { margin-bottom: 0; }
#screen-home .tip-head { margin-bottom: 8px; }
/* The settings screen does the same thing to its block titles (#screen-settings
   .setting-block-title, 10px), and the first fix caught only the home screen — so five
   blocks there kept a 22px gap under a heading whose "i" then sat below the middle of
   the words. Both places, or neither. */
#screen-settings .tip-head > h3 { margin-bottom: 0; }
#screen-settings .tip-head { margin-bottom: 10px; }

/* A row of choices and the "i" that explains the whole row. The "i" cannot go inside the
   row itself: those are radio groups, and a button that is not one of the choices has no
   business among them — tests/direction-toggle.test.js counts the buttons in that row
   for exactly this kind of reason. */
.tip-row {
  display: flex;
  align-items: center;
  gap: 2px;
}
.tip-row > :first-child { flex: 1; min-width: 0; }
/* On the narrowest phone the row beside it is already at its limit: three language names
   share 288px, and the stylesheet lets them break mid-word to fit at all. The mark takes
   31px out of that with its margin, which is enough to push "Multiple" onto two pieces —
   so on that width it is drawn smaller and closer. It still clears the 24px minimum
   target of WCAG 2.5.8, and the invisible 44px box on .tip-btn::before is untouched. */
@media (max-width: 359px) {
  .tip-row .tip-btn {
    width: 19px;
    height: 19px;
    margin-inline-start: 2px;
    font-size: 0.66rem;
  }
}

/* The line that says the gesture exists. Press-and-hold is not discoverable — nothing
   on a screen announces it — so it is said once, in words, under the row of tiles that
   most rewards knowing it. */
/* The badge Help draws beside the paragraph about the mark. It is the mark itself, at
   the size of the other help badges and in their frame, so a reader matches the
   explanation to the thing on their screen by its shape — which is what every icon in
   that column is for. */
.help-icon-tip {
  font-style: italic;
  font-weight: var(--weight-bold);
  color: var(--tip-btn-ink);
}

/* ===== Responsive ===== */
@media (min-width: 560px) {
  .app { padding-top: 36px; }
  .quiz-word { font-size: 2.7rem; }
}

/* ===== The five games ===== */
/* The tile and the Help badge, in the purple the "My words" and FAQ tiles already use;
   the accent edge follows it like every other tile's. */
.tile-icon-game { background: var(--badge-purple-bg); color: var(--badge-purple); }
.tile[data-accent="game"] { --tile-accent: var(--badge-purple); }
.help-icon-game { background: var(--badge-purple-bg); color: var(--badge-purple); border-color: transparent; }
/* The vocabulary check's Help item wears the band's rose, so the two are one thing. */
.help-icon-check { background: var(--badge-rose-bg); color: var(--badge-rose); border-color: transparent; }

/* The play area comes first on the screen and the two option rows under it: with the
   rows first, a phone showed a title and two cards and no game until it was scrolled. */
/* The chooser above the play areas: which of the five games is on the screen. Five
   cards in one radio group, the level cards' shape but wider and shorter, because each
   is a picture and a name rather than a number and two lines. minmax(0, 1fr) rather
   than a bare 1fr: a grid track sized from its content refuses to go below the longest
   word in it, and one interface language with a long game name pushed the third card
   off the side of a phone. */
.game-picks {
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: 10px;
  margin: 0 0 14px;
}
.game-pick {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  width: 100%;
  min-height: 56px;
  padding: 10px 12px;
  background: var(--card);
  border: 1px solid var(--control-border);
  border-radius: var(--radius);
  background-image: linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0) 45%, rgba(0, 0, 0, 0.045));
  box-shadow: 0 3px 0 var(--raise-edge), var(--shadow);
  color: var(--text);
  font: inherit;
  text-align: center;
  cursor: pointer;
  transition: background-color 0.15s, border-color 0.15s, transform 0.15s, box-shadow 0.15s;
}
.game-pick:active {
  background: var(--track);
  transform: translateY(2px);
  box-shadow: 0 1px 0 var(--raise-edge), 0 2px 6px rgba(0, 0, 0, 0.08);
}
@media (hover: hover) {
  .game-pick:hover {
    border-color: var(--primary);
    transform: translateY(-1px);
    box-shadow: 0 4px 0 var(--raise-edge), var(--shadow);
  }
  .game-pick:active {
    transform: translateY(2px);
    box-shadow: 0 1px 0 var(--raise-edge), 0 2px 6px rgba(0, 0, 0, 0.08);
  }
}
.game-pick.active {
  border-color: var(--primary);
  box-shadow: 0 3px 0 var(--raise-edge), var(--shadow), inset 0 0 0 2px var(--primary);
}
.game-pick.active .game-pick-name { color: var(--primary); }
.game-pick-icon { font-size: 1.4rem; line-height: 1; }
.game-pick-name { font-weight: var(--weight-bold); }
/* Five across, in a column the app never lets grow past 520px, is ninety pixels a card,
   and a picture beside a name does not fit in ninety pixels in any language. So the
   picture stands above the name, which is the shape a narrow card has room for, and a
   name of two words takes two lines. */
.game-pick { flex-direction: column; gap: 4px; padding: 8px 6px; }
.game-pick-name { font-size: 0.85rem; line-height: 1.25; overflow-wrap: anywhere; }
@media (max-width: 560px) {
  /* Five cards side by side leave a translated name no room at all on a phone, so the
     chooser is rows of two there, each card a picture beside a name again, and the
     fifth card takes the whole of the last row rather than half of it, so the group
     ends square. Rows rather than a column of five: five cards stacked above the play
     area would push the game itself off a phone screen, which is the one thing the
     screen is for. */
  .game-picks { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
  .game-pick { flex-direction: row; min-height: 50px; gap: 8px; padding: 8px; }
  .game-pick-name { font-size: 0.9rem; }
  .game-pick:last-child:nth-child(odd) { grid-column: 1 / -1; }
}

.game-intro { margin: 18px 0 12px; font-size: 0.9rem; line-height: 1.5; }
.game-options { margin-bottom: 14px; }
.game-options .setting-block-title { font-size: 1rem; }

/* The play area is a night sky whatever the theme, so its colours are written here in
   full rather than taken from the tokens: the HUD is light text on that sky and every
   control on it has to read against it in both themes. The height is a slice of the
   viewport rather than a fixed number so a phone in portrait keeps the footer reachable
   below it; the engine measures the box on every resize. */
.game-stage {
  position: relative;
  width: 100%;
  height: clamp(360px, 68vh, 720px);
  /* And never taller than what is left under the sticky screen title: on a short screen
     — a phone turned sideways — the stage would otherwise reach past the bottom of the
     window, and the only way to see the ship would be to scroll its top edge, and the
     score written along it, behind that title. 320px is the engine's own floor for the
     box it measures, so the canvas is never asked to paint one smaller than that — and
     the box it measures is inside the 3px border below, so the floor here is 326px. At
     320 the inside was 314: the engine laid its board out for 320 anyway, squashed it
     into 314, and a tap near the foot of a phone held sideways landed on the square above. */
  height: max(326px, min(clamp(360px, 68vh, 720px), calc(100svh - 120px)));
  border-radius: 20px;
  overflow: hidden;
  /* A night sky set straight on the page's own colour looked like a hole in it, in the
     light theme especially. The frame is the game's palette — cyan to violet to pink —
     drawn as a gradient border: the sky is painted to the padding box and the gradient
     to the border box, so the 3px between them is the rim. The shadow lifts it off the
     page; the engine measures clientWidth/Height, which exclude the border. */
  border: 3px solid transparent;
  background: linear-gradient(#06031a, #06031a) padding-box,
              linear-gradient(135deg, #4df3ff, #7c3aed 55%, #ff7ce5) border-box;
  box-shadow: 0 0 0 1px rgba(124,58,237,0.25), 0 14px 34px rgba(6,3,26,0.35);
  color: #eef4ff;
  --game-accent: #4df3ff;
  user-select: none;
  -webkit-user-select: none;
  -webkit-tap-highlight-color: transparent;
}
/* The range is the same box as the space stage in every way but its colours: the sky
   there is a night, here it is a dusk over a field, and the rim and the accent follow
   it so the HUD drawn on top reads against the picture behind it. The canvas paints
   the scene itself — this background is what shows before the first frame. */
.game-stage.range-stage {
  background: linear-gradient(#241237, #241237) padding-box,
              linear-gradient(135deg, #ffb457, #d9556b 55%, #6b3fa0) border-box;
  box-shadow: 0 0 0 1px rgba(217,85,107,0.25), 0 14px 34px rgba(36,18,55,0.35);
  --game-accent: #ffcf7a;
}
/* Three lives, and not the space game's three ships: the same row, the same width, a
   target rather than a hull. */
.range-stage .game-life {
  width: 15px;
  height: 15px;
  border: 3px solid #ffcf7a;
  border-radius: 50%;
  filter: drop-shadow(0 0 6px #ffb457);
}
.range-stage .game-life.is-lost {
  border-color: rgba(255,255,255,0.16);
  filter: none;
}
/* One shell per shot in the magazine, on the weapon rail's own furniture. A spent one
   is not removed: the row has to keep its width, or the rail would jump on every shot. */
.range-shell {
  flex: 0 1 22px;
  min-width: 0;
  aspect-ratio: 0.5;
  border-radius: 4px 4px 2px 2px;
  background: linear-gradient(#ffe3a8, #d99a3f 45%, #8a5a1e);
  box-shadow: 0 0 10px rgba(255,196,110,0.55);
  transition: opacity 0.2s, background-color 0.2s, box-shadow 0.2s;
}
.range-shell.is-spent {
  background: rgba(255,255,255,0.14);
  box-shadow: none;
}
/* The brick game is the same box again, and the third scene in it: a room rather than
   a sky or a field, lit from behind the wall. The canvas paints the room itself — this
   background is what shows before the first frame. */
.game-stage.brick-stage {
  background: linear-gradient(#0d1b2e, #0d1b2e) padding-box,
              linear-gradient(135deg, #7cf5c8, #3b82f6 55%, #a855f7) border-box;
  box-shadow: 0 0 0 1px rgba(59,130,246,0.25), 0 14px 34px rgba(13,27,46,0.35);
  --game-accent: #7cf5c8;
}
/* Three lives, and neither a hull nor a target: the ball the bat is there to keep up. */
.brick-stage .game-life {
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: radial-gradient(circle at 35% 32%, #ffffff, #8ab6ff 70%, #3b82f6);
  filter: drop-shadow(0 0 6px #8ab6ff);
}
.brick-stage .game-life.is-lost {
  background: rgba(255,255,255,0.16);
  filter: none;
}
/* Ten pips for the wall, whatever the wave built: a four-row wave and an eight-row one
   have to give the rail the same width, or it would jump between waves. A spent pip is
   dimmed rather than removed, for the same reason the range keeps its spent shells. */
.brick-pip {
  flex: 0 1 14px;
  min-width: 0;
  height: 8px;
  border-radius: 2px;
  background: linear-gradient(#d8fff2, #7cf5c8 55%, #2fb894);
  box-shadow: 0 0 8px rgba(124,245,200,0.5);
  transition: opacity 0.2s, background-color 0.2s, box-shadow 0.2s;
}
.brick-pip.is-spent {
  background: rgba(255,255,255,0.14);
  box-shadow: none;
}
/* The garden is the same box a fourth time, and the fourth scene in it: grass inside a
   fence, seen from above, in daylight. The canvas paints the garden itself — this
   background is what shows before the first frame. */
.game-stage.garden-stage {
  background: linear-gradient(#1c3a22, #1c3a22) padding-box,
              linear-gradient(135deg, #d6ff7a, #3f9d3f 55%, #ff5d5d) border-box;
  box-shadow: 0 0 0 1px rgba(63,157,63,0.25), 0 14px 34px rgba(28,58,34,0.35);
  --game-accent: #d6ff7a;
}
/* Three lives, and this time a leaf: a rounded square with two corners kept, which is
   what a leaf is at 14px. */
.garden-stage .game-life {
  width: 14px;
  height: 14px;
  /* The base rule draws a ship out of borders; without this the leaf wore a cyan hull. */
  border: 0;
  border-radius: 1px 10px 1px 10px;
  background: linear-gradient(135deg, #d6ff7a, #3f9d3f);
  filter: drop-shadow(0 0 6px #9be15d);
}
.garden-stage .game-life.is-lost {
  background: rgba(255,255,255,0.16);
  filter: none;
}
/* Eight pips for the wave's fruit, lit one by one as it is eaten. An unlit pip is dimmed
   rather than absent, for the same reason the range keeps its spent shells: the rail has
   to keep its width. */
.garden-pip {
  flex: 0 1 14px;
  min-width: 0;
  height: 8px;
  border-radius: 4px;
  background: linear-gradient(#ffe3a8, #ff5d5d 55%, #b83232);
  box-shadow: 0 0 8px rgba(255,93,93,0.5);
  transition: opacity 0.2s, background-color 0.2s, box-shadow 0.2s;
}
.garden-pip.is-spent {
  background: rgba(255,255,255,0.14);
  box-shadow: none;
}
/* The checkers board is the same box a fifth time, and the fifth scene in it: a board on
   a table top, seen from above, under a lamp. The canvas paints the table itself — this
   background is what shows before the first frame. */
.game-stage.checkers-stage {
  background: linear-gradient(#1f130d, #1f130d) padding-box,
              linear-gradient(135deg, #ffe1a3, #c8323f 55%, #2f6b55) border-box;
  box-shadow: 0 0 0 1px rgba(200,50,63,0.25), 0 14px 34px rgba(31,19,13,0.35);
  --game-accent: #ffd27a;
}
/* Three lives, and this time one of the player's own pieces: a cream disc with the ridge
   a checker has. */
.checkers-stage .game-life {
  width: 15px;
  height: 15px;
  /* The base rule draws a ship out of borders; the disc wants one border, all round. */
  border: 2px solid #b8935a;
  border-radius: 50%;
  background: radial-gradient(circle at 35% 32%, #fffaf0, #ecd9b0 70%);
  filter: drop-shadow(0 0 6px #ffd27a);
}
.checkers-stage .game-life.is-lost {
  border-color: rgba(255,255,255,0.16);
  background: transparent;
  filter: none;
}
/* Twelve pips for the computer's twelve pieces, put out one by one as they are taken. A
   taken piece is dimmed rather than removed, for the same reason the range keeps its
   spent shells: the rail has to keep its width. */
.checkers-pip {
  flex: 0 1 12px;
  min-width: 0;
  aspect-ratio: 1;
  border-radius: 50%;
  background: radial-gradient(circle at 35% 32%, #ff9aa5, #c8323f 70%);
  box-shadow: 0 0 8px rgba(224,73,90,0.5);
  transition: opacity 0.2s, background-color 0.2s, box-shadow 0.2s;
}
.checkers-pip.is-spent {
  background: rgba(255,255,255,0.14);
  box-shadow: none;
}


/* touch-action: none is what lets a finger steer the ship without scrolling the page
   under it; the page can still be scrolled from anywhere outside the box. */
.game-canvas { display: block; width: 100%; height: 100%; touch-action: none; outline: none; }
.game-hud { position: absolute; inset: 0; pointer-events: none; }
.game-score-box { position: absolute; top: 12px; left: 14px; line-height: 1; }
.game-score {
  display: inline-block;
  /* Written out, because the statistics screen's round rows use the same class name
     and paint theirs in --primary: on the night sky that was dark green in the light
     theme, and the score was the hardest thing on the screen to read. */
  color: #ffffff;
  font-size: 26px;
  font-weight: 700;
  letter-spacing: 1px;
  font-variant-numeric: tabular-nums;
  text-shadow: 0 0 16px var(--game-accent);
  transform-origin: left center;
}
.game-score.is-bump { animation: game-bump 0.25s ease-out; }
@keyframes game-bump { 0% { transform: scale(1.18); } 100% { transform: scale(1); } }
.game-sub { margin-top: 6px; font-size: 12px; color: rgba(238,244,255,0.72); }
.game-next { font-variant-numeric: tabular-nums; }
.game-lives { position: absolute; top: 14px; right: 14px; display: flex; gap: 6px; }
.game-life {
  width: 0;
  height: 0;
  border-left: 8px solid transparent;
  border-right: 8px solid transparent;
  border-bottom: 18px solid #9fe3ff;
  filter: drop-shadow(0 0 6px #4df3ff);
  transition: border-color 0.3s, filter 0.3s;
}
.game-life.is-lost { border-bottom-color: rgba(255,255,255,0.14); filter: none; }
/* The two keys a phone cannot press, as buttons; the only part of the HUD that takes
   a pointer. */
.game-hud-btns { position: absolute; top: 40px; right: 10px; display: flex; gap: 6px; pointer-events: auto; }
.game-hud-btn {
  min-height: 32px;
  padding: 0 10px;
  border-radius: 10px;
  background: rgba(6,3,26,0.65);
  /* 0.45, not 0.28: the boundary is what says these are buttons, and it has to reach
     3:1 against the box behind it (WCAG 1.4.11). */
  border: 1px solid rgba(255,255,255,0.45);
  color: #ffffff;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 1px;
}
.game-hud-btn:focus-visible, .game-btn:focus-visible, .game-q-option:focus-visible,
.game-q-speak:focus-visible, .game-q-speak-option:focus-visible {
  outline: 2px solid #ffffff;
  outline-offset: 3px;
}
/* The rail spans the stage and centres what is in it, rather than being a shrink-to-fit
   box pushed to the middle with left:50% and a -50% transform. That older centring is
   what the slots would have shrunk against, and it is a trap: for an absolutely
   positioned box, the width available to shrink-to-fit is the containing block minus the
   left offset — so left:50% left the rail half the stage and no more. Fixed 30px chips
   never noticed, because they simply overflowed that box. Shrinkable ones did: on a
   desktop with room for the full 450px rail, the chips came out 13.75px wide. */
.game-rail { position: absolute; bottom: 10px; left: 10px; right: 10px; text-align: center; }
.game-wname {
  height: 20px;
  font-size: 14px;
  font-weight: 700;
  letter-spacing: 2px;
  color: var(--game-accent);
  text-shadow: 0 0 14px var(--game-accent);
  transition: color 0.3s;
}
.game-rail.is-flash .game-wname { animation: game-pop 0.55s cubic-bezier(.2,.9,.3,1.2); }
@keyframes game-pop { 0% { transform: scale(1.4); filter: brightness(2.2); } 100% { transform: scale(1); filter: brightness(1); } }
/* One slot per weapon, and the engine paints as many as WEAPONS holds — so the rail has
   to fit a count this file is not told. Until 7 September 2026 it was three hand-written
   sets of numbers, one per breakpoint, each sized for exactly ten; the thirteenth weapon
   would have come to 450px inside a 360px phone's 322px box, and because .game-rail is
   absolutely positioned and centred inside an overflow:hidden stage, the overspill is
   not a scrollbar or an error — the first and last weapons are simply clipped away, on a
   phone, silently.
   So the size is derived instead of listed: a 30px ideal that is free to shrink, square
   by its own ratio, inside a rail that spans the stage less 10px each side. Measured in
   Chrome, thirteen chips come out at the full 30px from a 520px viewport up, 22.8px on a
   414px phone, 18.6px on a 360px one and 15.5px at 320px, and the rail overflows at none
   of them. The dark pill is now as wide as the rail rather than hugging the chips — 12px
   wider than they need at the widest, which is why it was not worth a second cap to
   avoid. No future weapon can overflow this rail or need these numbers revisited. */
.game-slots {
  display: flex;
  gap: clamp(2px, 0.9vw, 4px);
  /* The rail's own width, not the chips'. `width: fit-content` reads more naturally and
     is wrong here: a flex item that cannot grow contributes only min(flex-basis,
     its own max-content) to the container's intrinsic width, and a chip's max-content is
     the two letters in it — so the box measured 239px and squeezed every chip to the
     13.75px that "PL" happens to occupy. A definite width takes the sizing away from the
     text, and justify-content keeps the chips centred in it. */
  width: 100%;
  justify-content: center;
  margin-top: 6px;
  padding: 5px;
  border-radius: 12px;
  background: rgba(6,3,26,0.55);
  border: 1px solid rgba(255,255,255,0.08);
}
.game-slot {
  flex: 0 1 30px;
  min-width: 0;
  aspect-ratio: 1;
  border-radius: 8px;
  display: grid;
  place-items: center;
  font-size: clamp(7px, 2.3vw, 10px);
  font-weight: 700;
  color: rgba(255,255,255,0.3);
  transition: color 0.3s, transform 0.3s, box-shadow 0.3s, background-color 0.3s;
}
.game-slot.is-next { color: rgba(255,255,255,0.7); }
.game-slot.is-on {
  color: #fff;
  background: var(--slot-bg);
  box-shadow: 0 0 14px var(--slot-c), inset 0 0 0 1px var(--slot-c);
  transform: translateY(-3px) scale(1.08);
}
.game-timer { height: 3px; margin: 6px 5px 0; background: rgba(255,255,255,0.1); border-radius: 2px; overflow: hidden; }
.game-timer i {
  display: block;
  height: 100%;
  width: 100%;
  background: var(--game-accent);
  box-shadow: 0 0 10px var(--game-accent);
  transform-origin: left center;
}
.game-msg {
  position: absolute;
  top: 34%;
  left: 50%;
  transform: translate(-50%, -50%);
  font-weight: 700;
  font-size: clamp(22px, 6vw, 40px);
  letter-spacing: 6px;
  color: #fff;
  opacity: 0;
  /* It used to be nowrap, and a message the length of "Life lost — 2 of 3 left" ran off
     both edges of a 360px phone at 22px and 6px of tracking — the two words that say
     what just happened were the two off the screen. Bounded and centred instead: the
     short messages the box was built for ("WAVE 2") are one line exactly as before. */
  max-width: calc(100% - 24px);
  text-align: center;
  text-wrap: balance;
  text-shadow: 0 0 30px var(--msg, #fff);
}
.game-msg.is-show { animation: game-msg 1.4s ease-out forwards; }
@keyframes game-msg {
  0% { opacity: 0; transform: translate(-50%, -50%) scale(0.6); }
  15% { opacity: 1; transform: translate(-50%, -50%) scale(1.06); }
  70% { opacity: 1; }
  100% { opacity: 0; transform: translate(-50%, -70%) scale(1); }
}
.game-pause {
  position: absolute;
  inset: 0;
  /* A label, not a lid: it comes after the HUD in the markup, so it sat over the
     Resume button, and a phone — with no P key — had no way back into a paused game.
     Every press now falls through it, to the button or to the canvas. */
  pointer-events: none;
  display: grid;
  place-items: center;
  background: rgba(2,1,8,0.55);
  font-weight: 700;
  font-size: 30px;
  letter-spacing: 8px;
  color: #fff;
  text-align: center;
}
.game-pause-hint {
  display: block;
  margin-top: 10px;
  font-size: 13px;
  font-weight: 400;
  letter-spacing: 1px;
  color: rgba(238,244,255,0.82);
}
/* The launch and game-over cards, and the question: each fills the box and scrolls
   inside it if a phone's box is shorter than the card. */
.game-overlay, .game-question {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  padding: 12px;
  overflow: auto;
  background: radial-gradient(ellipse at center, rgba(12,8,36,0.5), rgba(2,1,8,0.9));
}
/* The question is the one overlay that wants the reader's whole attention, so what is
   behind it is blurred and not merely darkened: at the old opacity the explosions went
   on moving under the word being asked. The gradient alone still covers the field where
   a browser has no backdrop filter. */
.game-question {
  background:
    radial-gradient(ellipse at 50% 40%, rgba(77,243,255,0.10), transparent 60%),
    radial-gradient(ellipse at center, rgba(12,8,36,0.72), rgba(2,1,8,0.94));
  backdrop-filter: blur(9px) saturate(115%);
  -webkit-backdrop-filter: blur(9px) saturate(115%);
  animation: game-q-veil 0.22s ease-out;
}
@keyframes game-q-veil { from { opacity: 0; } to { opacity: 1; } }
.game-card, .game-q-card {
  width: min(100%, 420px);
  padding: 20px 18px;
  border-radius: 18px;
  background: rgba(10,8,30,0.82);
  border: 1px solid rgba(255,255,255,0.12);
  box-shadow: 0 20px 60px rgba(0,0,0,0.5);
  text-align: center;
  color: #eef4ff;
}
/* The question card is lit at its rim rather than outlined in grey: the border is a
   gradient painted in the border box while the card's own fill stays in the padding
   box, which is the one way to give a rounded card a two-colour edge without adding
   an element a screen reader would then have to step over. */
.game-q-card {
  width: min(100%, 440px);
  padding: 22px 20px 20px;
  border-radius: 22px;
  border: 1px solid transparent;
  background:
    radial-gradient(130% 70% at 50% -12%, rgba(255,255,255,0.10), transparent 62%) padding-box,
    linear-gradient(180deg, rgba(19,15,46,0.95), rgba(7,5,22,0.97)) padding-box,
    linear-gradient(140deg, rgba(77,243,255,0.8), rgba(168,132,255,0.5) 46%, rgba(123,255,158,0.65)) border-box;
  box-shadow: 0 26px 70px rgba(0,0,0,0.62), 0 0 42px rgba(77,243,255,0.12);
  animation: game-q-rise 0.26s cubic-bezier(0.2, 0.8, 0.3, 1);
}
@keyframes game-q-rise {
  from { opacity: 0; transform: translateY(14px) scale(0.965); }
  to { opacity: 1; transform: none; }
}
/* The launch card used to open with the site's name in big gradient capitals. Inside
   the canvas box it was a picture of a title rather than a title: it could not shrink
   with the card, so "HELIUM3WORDS.COM" was painted with its last letter sliced off, and
   it repeated the <h1> standing directly above the play area. The heading carries the
   name now, as ordinary text over the game. */
/* The range names itself on its launch card, because the screen above it is titled for
   the pair of games and no longer for either one of them. */
.game-card-title {
  margin: 0 0 4px;
  font-size: 1.05rem;
  font-weight: 700;
  letter-spacing: 0.5px;
  color: var(--game-accent);
  text-shadow: 0 0 18px var(--game-accent);
}
.game-card-sub { margin: 12px 0 0; font-size: 0.95rem; color: rgba(238,244,255,0.82); line-height: 1.5; }
.game-card-sub:first-child { margin-top: 0; }
.game-controls {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 8px 14px;
  margin: 18px 0 0;
  text-align: start;
  font-size: 0.88rem;
  align-items: center;
}
.game-controls dt, .game-controls dd { margin: 0; }
.game-controls kbd {
  font-family: inherit;
  font-size: 11px;
  font-weight: 700;
  padding: 3px 7px;
  border-radius: 6px;
  border: 1px solid rgba(255,255,255,0.25);
  background: rgba(255,255,255,0.07);
}
.game-btn {
  display: inline-block;
  margin-top: 20px;
  padding: 13px 36px;
  border-radius: 999px;
  font-weight: 700;
  font-size: 1rem;
  letter-spacing: 2px;
  color: #06031a;
  background: linear-gradient(90deg, #4df3ff, #7bff9e);
  box-shadow: 0 0 24px rgba(77,243,255,0.4);
  transition: transform 0.15s, box-shadow 0.15s;
}
.game-btn:hover { transform: translateY(-2px); box-shadow: 0 0 40px rgba(77,243,255,0.7); }
.game-final {
  margin: 10px 0 0;
  font-size: 40px;
  font-weight: 700;
  color: #ffd166;
  text-shadow: 0 0 20px #ffd166;
  font-variant-numeric: tabular-nums;
}
.game-best { margin: 6px 0 0; font-size: 0.85rem; color: rgba(238,244,255,0.65); }
/* "QUESTION" is a label on the card, not a heading of its own weight, so it is drawn
   as one small lit pill instead of wide grey capitals. */
.game-q-title {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  margin: 0;
  padding: 5px 13px;
  border-radius: 999px;
  border: 1px solid rgba(77,243,255,0.34);
  background: rgba(77,243,255,0.10);
  font-size: 0.72rem;
  letter-spacing: 2px;
  text-transform: uppercase;
  color: rgba(222,247,255,0.92);
}
.game-q-title::before {
  content: "";
  flex: none;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: #4df3ff;
  box-shadow: 0 0 8px #4df3ff;
}
.game-q-meta { margin: 11px 0 0; font-size: 0.82rem; color: rgba(238,244,255,0.6); }
.game-q-word {
  margin: 8px 0 16px;
  font-size: clamp(1.65rem, 7vw, 2.35rem);
  font-weight: 800;
  line-height: 1.2;
  overflow-wrap: anywhere;
  text-shadow: 0 0 30px rgba(120,200,255,0.35);
}
/* A short rule under the word, fading at both ends: it separates what is asked from
   what can be pressed without drawing a line across the whole card. */
.game-q-word::after {
  content: "";
  display: block;
  width: 64px;
  height: 2px;
  margin: 15px auto 0;
  border-radius: 2px;
  background: linear-gradient(90deg, transparent, rgba(77,243,255,0.7), transparent);
}
/* The asked word as a button. It looks like the word it already was — no frame, the
   card's own type — so the card is unchanged for anyone who never presses it, and the
   speaker beside it is the only sign that it can be pressed at all. */
.game-q-speak {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  max-inline-size: 100%;
  padding: 4px 14px;
  border: 0;
  border-radius: 16px;
  background: none;
  color: inherit;
  font: inherit;
  line-height: inherit;
  text-shadow: inherit;
  cursor: pointer;
  overflow-wrap: anywhere;
  transition: background-color 0.15s;
}
@media (hover: hover) {
  .game-q-speak:hover { background: rgba(77,243,255,0.14); }
}
.game-q-speak:active { background: rgba(77,243,255,0.2); }
.game-q-speak-glyph { flex: 0 0 auto; }
/* Smaller than the word it belongs to, and not so faint that it stops reading as a
   control (WCAG 1.4.11 applies to the button's own edge; this one has none, so the
   glyph itself has to stay legible). */
.game-q-speak .game-q-speak-glyph { font-size: 0.5em; opacity: 0.85; }
/* An answer and its own speaker, side by side. The row carries the answers' direction,
   so the speaker sits at the end the language reads towards. */
.game-q-row { display: flex; align-items: stretch; gap: 8px; }
.game-q-row .game-q-option { flex: 1 1 auto; min-inline-size: 0; }
.game-q-speak-option {
  flex: 0 0 auto;
  width: 54px;
  min-height: 50px;
  border-radius: 14px;
  border: 1px solid rgba(255,255,255,0.45);
  background: rgba(255,255,255,0.06);
  color: #ffffff;
  font-size: 1.1rem;
  line-height: 1;
  cursor: pointer;
  transition: border-color 0.15s, background-color 0.15s, transform 0.15s;
}
@media (hover: hover) {
  .game-q-speak-option:hover { border-color: #4df3ff; background: rgba(77,243,255,0.16); }
}
.game-q-speak-option:active { transform: scale(0.95); }
/* Never faded with the answer beside it: it is never disabled either — hearing the
   options after the answer is given is the point of it — and a faded live control has
   no discernible edge. The faded answer already carries the "not chosen" signal. */
.game-q-options { display: grid; gap: 10px; }
.game-q-option {
  position: relative;
  min-height: 50px;
  /* Equal on both sides, so the mark at the end never shifts a centred answer. */
  padding: 12px 38px;
  border-radius: 14px;
  background: linear-gradient(180deg, rgba(255,255,255,0.10), rgba(255,255,255,0.045));
  border: 1px solid rgba(255,255,255,0.45);
  color: #ffffff;
  font-size: 1.05rem;
  font-weight: 600;
  line-height: 1.3;
  overflow-wrap: anywhere;
  transition: background-color 0.15s, border-color 0.15s, transform 0.15s, box-shadow 0.15s, opacity 0.2s;
}
.game-q-option:not(:disabled):hover {
  border-color: #4df3ff;
  background: linear-gradient(180deg, rgba(77,243,255,0.22), rgba(77,243,255,0.09));
  transform: translateY(-2px);
  box-shadow: 0 10px 26px rgba(0,0,0,0.45), 0 0 22px rgba(77,243,255,0.28);
}
.game-q-option:not(:disabled):active { transform: translateY(0); }
.game-q-option.is-right {
  border-color: #7bff9e;
  background: linear-gradient(180deg, rgba(123,255,158,0.30), rgba(123,255,158,0.13));
  box-shadow: 0 0 28px rgba(123,255,158,0.28);
}
.game-q-option.is-wrong {
  border-color: #ff6b6b;
  background: linear-gradient(180deg, rgba(255,107,107,0.28), rgba(255,107,107,0.12));
}
/* The mark says in shape what the colour says in green and red, for a reader who
   cannot tell the two borders apart. */
.game-q-option.is-right::after, .game-q-option.is-wrong::after {
  position: absolute;
  inset-inline-end: 13px;
  top: 50%;
  transform: translateY(-50%);
  font-size: 1rem;
  font-weight: 700;
}
.game-q-option.is-right::after { content: "\2713 "; color: #7bff9e; }
.game-q-option.is-wrong::after { content: "\2715 "; color: #ffb3b3; }
/* Once the question is answered the answers nobody chose step back, so the marked
   pair carries the eye; the marked two stay at full strength. */
.game-q-option:disabled { opacity: 0.55; }
.game-q-option.is-right:disabled, .game-q-option.is-wrong:disabled { opacity: 1; }
.game-q-result { margin: 14px 0 0; min-height: 1.4em; font-size: 0.95rem; line-height: 1.5; }
.game-q-result:not(:empty) {
  display: inline-block;
  padding: 7px 15px;
  border-radius: 999px;
  border: 1px solid transparent;
}
.game-q-result.is-right { color: #7bff9e; border-color: rgba(123,255,158,0.35); background: rgba(123,255,158,0.12); }
.game-q-result.is-wrong { color: #ffb3b3; border-color: rgba(255,107,107,0.35); background: rgba(255,107,107,0.10); }
.game-q-card .game-btn { margin-top: 14px; }
/* The slot sizes that used to live here, and in a second block at 339px, are gone: they
   were written for ten weapons and .game-slot derives its own size now. */
@media (max-width: 400px) {
  .game-score { font-size: 22px; }
}
@media (prefers-reduced-motion: reduce) {
  .game-rail.is-flash .game-wname, .game-msg.is-show, .game-score.is-bump,
  .game-question, .game-q-card { animation-duration: 0.01ms; }
  .game-btn, .game-slot, .game-q-option, .game-q-speak, .game-q-speak-option { transition: none; }
  .game-q-option:not(:disabled):hover { transform: none; }
}

/* ===== Interface language =====
   An arrow names the question language on its left and the answer language on its
   right. Mirroring that little phrase would reverse its meaning. The names inside
   keep their own lang/dir attributes, including in the native-name picker. */
.lang-change-pair,
#direction-toggle,
.quiz-direction,
.game-q-meta,
.home-brand {
  direction: ltr;
  unicode-bidi: isolate;
}

/* With the dialog's scroll lock, Chromium keeps the stable root gutter on the RTL
   side and offsets fixed overlays by that gutter's width. Release it only while an
   RTL dialog is open, so the backdrop covers the viewport and its card stays centred. */
:root[dir="rtl"]:has(body.modal-open) { scrollbar-gutter: auto; }

/* Translated labels may need another line. Let the text set each control's height,
   including at 320px, rather than letting a flex item's minimum width hide its end. */
[data-app] :is(.btn, .chip, .page-title, .tile-name, .tile-sub,
  .learn-cta-text, .home-install-howto-text, .lang-picker-native, .lang-picker-english) {
  min-inline-size: 0;
  overflow-wrap: anywhere;
}
.start-now > span { min-inline-size: 0; max-inline-size: 100%; }
.lang-change-action { flex-shrink: 1; flex-wrap: wrap; max-inline-size: 100%; }
/* On a phone the two halves of the pill rarely share a line, and wherever the wrap fell
   the dot between them was left alone at the end of the first line ("Tap to Change
   Languages ·") or at the head of the next ("· 73 Sprachen"), with the globe stranded on
   a line of its own above the words in the longer languages. So below 480px the count
   takes a line of its own and the dot goes; the words keep the globe's line and wrap
   beside it. The note that says the pair came from the browser is treated the same way. */
@media (max-width: 479px) {
  .lang-change-action > [data-ui] { flex: 1 1 0; min-inline-size: 0; }
  .lang-change-action .lang-change-sep,
  .lang-change-source-sep { display: none; }
  .lang-change-count,
  .lang-change-source { flex-basis: 100%; }
}
.type-actions .btn, .flash-actions .btn { white-space: normal; }
.chip { max-inline-size: 100%; }
.learn-cta-go { white-space: normal; max-inline-size: 7.5em; text-align: center; overflow-wrap: anywhere; }
#consent-overlay .modal-actions .btn { min-inline-size: 0; }

/* The game's geometry and weapon order remain tied to its canvas coordinates.
   Only text boxes mirror with the interface; question words still use markLang. */
.game-stage { direction: ltr; }
#screen-game .page-title { font-size: clamp(1.15rem, 5vw, 1.5rem); }
.game-card, .game-q-card { min-inline-size: 0; max-inline-size: 100%; overflow-wrap: anywhere; }
.game-controls { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
.game-controls dt, .game-controls dd { min-inline-size: 0; }
.game-controls kbd { display: inline-block; direction: ltr; unicode-bidi: isolate; }
.game-btn { max-inline-size: 100%; overflow-wrap: anywhere; }
.game-wname { height: auto; min-height: 20px; overflow-wrap: anywhere; }
.game-next { max-inline-size: 12em; line-height: 1.35; overflow-wrap: anywhere; }
:root[dir="rtl"] :is(.game-card, .game-q-card, .game-sub,
  .game-wname, .game-hud-btn, .game-msg, .game-pause) { direction: rtl; }
:root[dir="rtl"] :is(.game-wname, .game-hud-btn, .game-msg,
  .game-pause, .game-pause-hint, .game-btn, .game-q-title) { letter-spacing: normal; }

@media (max-width: 359px) {
  /* Below the score, the countdown gets its own row clear of the two HUD buttons.
     This leaves both controls where the canvas expects them without a text overlap. */
  .game-next { margin-top: 30px; max-inline-size: 19em; }
  .learn-cta-go { padding-inline: 10px; max-inline-size: 6.5em; }
  #screen-home .home-brand { font-size: 1.1rem; }
  /* A Hebrew label such as Statistics is one word: give it the tile's full width
     instead of breaking its final letter onto a line beside the icon. */
  :root[dir="rtl"] #screen-home .tile {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas: "icon" "name" "sub";
    justify-items: center;
    text-align: center;
  }
}
