/* ===== English vocabulary words — helium3words.com ===== */
/* ===== The palette =====
   Dark is the default and stays the default. Nothing here reads prefers-color-scheme: a
   device set to light does not turn the app light 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.
   Light 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).

   Every colour on a screen is a token declared here, and the light 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 light theme cannot reach, and it would sit
   there as a dark patch on a light page. A new colour is named here first and given a
   light 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 dark palette. */
:root {
  color-scheme: dark;
  --bg: #0C0F12;
  --card: #15191E;
  --border: #252B32;
  /* 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.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 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". White on this fill clears 14.9:1. */
  --danger-btn-bg: #3A1616;
  --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. */
  --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;
  /* A starred word, and the empty groove behind a chart bar or a progress bar */
  --star: #FFC94A;
  --track: #2A313A;
  --overlay: rgba(4, 6, 8, 0.72);
  --radius: 20px;
  --radius-btn: 16px;
  --shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
  --shadow-modal: 0 16px 48px rgba(0, 0, 0, 0.55);
  /* Soft halo on the Advanced Learning panel that is being read aloud. The colour is
     the accent at a low alpha, so light theme can darken the same token without a
     separate rule at the call site. */
  --learn-glow: rgba(126, 231, 135, 0.22);
  /* 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: #1D242B;
  /* ===== 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;
}

/* ===== Light =====
   Not an inversion — a light page needs ink of its own. The 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, and the
   badge pairs are turned round: a pale fill with dark ink on it rather than the reverse.
   --on-primary goes to white, because what it names is whatever sits on top of
   --primary, and that is now a deep green. */
:root[data-theme="light"] {
  color-scheme: light;
  --bg: #F5F7F6;
  --card: #FFFFFF;
  --border: #DBE2DF;
  /* The light-theme control edge clears 3:1 against --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;
  --primary: #1B7A32;
  --on-primary: #FFFFFF;
  --success: #0F7A38;
  --success-bg: #E3F6E8;
  --error: #C42B2B;
  --error-bg: #FDE7E7;
  /* White ink cannot sit on the pale tint, so here the delete button is filled with
     the red itself: white on #C42B2B clears 5.4:1. */
  --danger-btn-bg: #C42B2B;
  --danger-btn-ink: #FFFFFF;
  --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;
  --star: #9A6B00;
  --track: #DDE3E1;
  --overlay: rgba(16, 22, 26, 0.45);
  --shadow: 0 8px 24px rgba(16, 24, 20, 0.10);
  --shadow-modal: 0 16px 48px rgba(16, 24, 20, 0.22);
  --learn-glow: rgba(27, 122, 50, 0.16);
  --btn-hover-bg: #EDF2EF;
}

/* ===== 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%;
}

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;
}

/* 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 {
  position: absolute;
  top: -60px;
  left: 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 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 {
  display: flex;
  align-items: center;
  gap: 14px;
  margin: 10px 0 22px;
}
.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;
}
.home-titles h1 { font-size: 1.9rem; font-weight: var(--weight-heavy); line-height: 1.15; }
.home-sub { color: var(--primary); font-weight: var(--weight-bold); font-size: 0.9rem; }

/* 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;
  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;
  transition: background 0.2s, color 0.2s, border-color 0.2s;
}
.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);
}
.lang-change-action {
  flex-shrink: 0;
  color: var(--primary);
  font-size: 0.85rem;
  font-weight: var(--weight-bold);
}

/* 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;
}
.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;
  padding: 13px 6px;
  border-radius: 14px;
  font-size: 1rem;
  font-weight: var(--weight-bold);
  color: var(--text);
  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. */
.levels { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
.level-card {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 12px;
  background: var(--card);
  border: 1px solid var(--control-border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  text-align: left;
  transition: transform 0.15s, border-color 0.15s;
}
.level-card:active { transform: scale(0.98); }
.level-card:hover { border-color: var(--primary); }
/* 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; }
.level-num {
  width: 40px;
  height: 40px;
  border-radius: 12px;
  font-size: 1.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-num-4 { background: var(--badge-purple-bg); color: var(--badge-purple); }
.level-info { flex: 1; 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: 1.05rem; font-weight: var(--weight-heavy); }
.level-desc { display: block; color: var(--muted); font-size: 0.76rem; margin-top: 2px; }

.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. The nine tiles fill the three columns exactly.
   A browser that has just installed the app only makes the install tile invisible, so
   its cell stays and nothing shifts; opened from the installed icon the tile is out of
   the layout altogether and Report takes that cell, which leaves the empty one at the
   end of the row where it costs no height. */
.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;
  transition: transform 0.15s, border-color 0.15s;
}
.tile:active { transform: scale(0.96); }
.tile:hover { border-color: var(--primary); }
/* Eight tiles are buttons and the ninth (Report) is a link, because it leaves the app
   for the contact clause on the legal page. It takes back the two things a link
   carries and a button does not - the underline and the visited colour - and is
   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); }
.tile-icon-stats { background: var(--primary); color: var(--on-primary); }
.tile-icon-set { background: var(--badge-green-bg); color: var(--badge-green); }
.tile-icon-learn { background: var(--badge-amber-bg); color: var(--badge-amber); }
.tile-icon-install { background: var(--badge-blue-bg); color: var(--badge-blue); }
.tile-icon-report { background: var(--badge-orange-bg); color: var(--badge-orange); }
/* 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); }
.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. */
/* The tile grid and the how-to panel under it. The panel is positioned against this
   wrapper rather than sitting in the flow, so that showing it does not push the notice
   and the footer down the page. */
.home-more {
  position: relative;
  overflow-anchor: none;
}
/* Hidden without being removed: the cell keeps its place in the grid, so the tiles
   below and the footer under them do not jump when the app is already installed. */
.home-install.is-installed {
  visibility: hidden;
  pointer-events: none;
}
.home-install-howto {
  position: absolute;
  left: 0;
  right: 0;
  top: 100%;
  z-index: 2;
  margin-top: 8px;
  padding: 8px 10px;
  border-radius: 10px;
  background: var(--card);
  border: 1px solid var(--border);
  color: var(--muted);
  font-size: 0.76rem;
  line-height: 1.5;
  box-shadow: var(--shadow);
}
@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;
  opacity: 0.85;
}
/* 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;
  opacity: 0.85;
}
#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 {
  display: inline-block;
  padding: 4px 0;
  color: var(--muted);
  text-decoration: none;
  transition: color 0.15s;
}
.legal-links a:hover { color: var(--primary); }

/* ===== 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: right; }

.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;
  right: 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;
}
.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 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 and the border carry the verdict. The wording on them stays ordinary
   ink — white on dark, dark on light — so a green or red letter does not sit on
   a green or red ground. */
.answer-btn.correct {
  background: var(--success-bg);
  border-color: var(--success);
  color: var(--text);
  font-weight: var(--weight-bold);
}
.answer-btn.wrong {
  background: var(--error-bg);
  border-color: var(--error);
  color: var(--text);
  font-weight: var(--weight-bold);
}
.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); }
.answer-btn.faded + .answer-speak { opacity: 0.35; }

/* 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,
.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(--text);
  -webkit-text-fill-color: var(--text);
  font-weight: var(--weight-bold);
}
.type-input.wrong {
  background: var(--error-bg);
  border-color: var(--error);
  color: var(--text);
  -webkit-text-fill-color: var(--text);
  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);
}
#screen-quiz.quiz-typing .type-reveal {
  margin-top: 12px;
  line-height: 1.5;
  min-height: 1.5em;
}

/* 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. */
.quiz-feedback {
  text-align: center;
  font-size: 1rem;
  font-weight: var(--weight-bold);
  color: var(--text);
}
#screen-quiz.quiz-typing .quiz-feedback {
  margin-top: 12px;
  line-height: 1.5;
  /* A fixed slot, not min-height: the ✓/✗ glyphs paint a hair taller than an empty
     line, and that extra pixel is enough to shove the footer. */
  height: 1.5em;
  overflow: hidden;
}

/* 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,
  #screen-quiz.quiz-typing.typing-focus .quiz-feedback { margin-top: 6px; }
}

.quiz-next { margin-top: 10px; padding: 13px; font-size: 1.05rem; }
.quiz-next.is-pending {
  visibility: hidden;
  pointer-events: none;
}
.quiz-bottom { text-align: center; margin-top: 8px; font-size: 0.9rem; }

/* ===== 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; }
.learn-panels {
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.learn-panel {
  background: var(--card);
  border: 1px solid var(--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;
  opacity: 0.58;
  transition: border-color 0.2s, box-shadow 0.2s, opacity 0.2s;
}
.learn-panel.is-active {
  opacity: 1;
  border-color: var(--primary);
  box-shadow: 0 0 24px var(--learn-glow), var(--shadow);
}
.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; }
/* Dimming the panel that is not being read only means something while a word is being
   read. Paused - and on arrival, which is now the same thing - the pair stands whole. */
.learn-panels.is-idle .learn-panel { opacity: 1; }

/* ----- 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); }
.score-pct { color: var(--muted); margin-top: 4px; }

.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 .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);
  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); }

#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. */
.results-actions {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-top: 20px;
}
.results-actions .btn { min-height: 44px; padding: 11px 14px; font-size: 1rem; }
.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-direction: column;
  gap: 10px;
  padding: 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.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 — the
   filled ones by a halo in the accent, the bordered ones by lifting a shade off the
   page and taking the accent edge. */
@media (hover: hover) {
  .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);
  }
  .btn-danger:not(:disabled):hover { box-shadow: 0 0 0 3px var(--error-bg); }
}
#favorites-practice { margin-top: 20px; }

/* 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; }
.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; }
.level-stat-nums { display: flex; gap: 16px; font-size: 0.82rem; }
.level-stat-nums .ok { color: var(--success); }
.level-stat-nums .bad { color: var(--error); }

.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: right; }

/* ===== 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-bg);
  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; }
#mywords-practice { flex-shrink: 0; }

/* ===== 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; }
#screen-settings .notice-group { margin: 22px 0 0; }

/* 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-left: 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-left: 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(--primary); color: var(--on-primary); 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-bg);
  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; }
.modal-actions {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-top: 22px;
}
.modal-actions .btn { padding: 14px; font-size: 1.02rem; }

/* ===== 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 screen — everything fits on one screen without scrolling ===== */
#screen-home .home-header { margin: 4px 0 14px; }
#screen-home .logo-link { border-radius: 14px; }
#screen-home .logo { width: 48px; height: 48px; border-radius: 14px; }
#screen-home .home-titles h1 { font-size: 1.55rem; }
#screen-home .lang-change-btn { padding: 10px 12px; }
#screen-home .dir-btn { padding: 10px 6px; font-size: 0.95rem; }
#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;
  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; 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 .logo-link { border-radius: 12px; }
  #screen-home .logo { width: 40px; height: 40px; border-radius: 12px; }
  #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 .dir-btn { padding: 8px 6px; }
  #screen-home .section-head { margin-top: 10px; }
  #screen-home .level-card { padding: 9px 10px; }
  #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; }
  #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 rest of the quiz is hidden while this
   is shown so a blank question is not clickable underneath. */
.bank-status {
  margin: 24px 0;
  text-align: center;
}
.bank-status p { margin: 0 0 12px; }
#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 .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 {
  visibility: hidden;
}
#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; }

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