/* ===== 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 loads this file too and never carries the attribute, so it keeps the dark
   palette either way — that page has no scripts at all by design (see its own CSP
   comment), and there is nothing there to read the preference. */
:root {
  color-scheme: dark;
  --bg: #0C0F12;
  --card: #15191E;
  --border: #252B32;
  --text: #F2F5F3;
  --muted: #98A2A9;
  --primary: #7EE787;
  --on-primary: #0B1F10;
  --success: #4ADE80;
  --success-bg: #10331F;
  --error: #FF5C5C;
  --error-bg: #3A1616;
  /* 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);
  /* ===== 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;
  --text: #12171A;
  --muted: #5A656C;
  --primary: #1B7A32;
  --on-primary: #FFFFFF;
  --success: #0F7A38;
  --success-bg: #E3F6E8;
  --error: #C42B2B;
  --error-bg: #FDE7E7;
  --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);
}

/* ===== 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"] .study-notice-icon,
:root[data-bold="on"] .notice-group-icon,
:root[data-bold="on"] .quiz-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; }

html, body {
  background: var(--bg);
  color: var(--text);
  font-family: 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); }
/* The same treatment for the install suggestion, which sits in the same corner. The
   two banners are never open together (see setConsentBannerVisible in app.js), so
   these two rules do not collide. */
body.install-banner-open { padding-bottom: var(--install-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;
}

/* 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; }
}

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 {
  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; }

.language-picker {
  display: grid;
  gap: 6px;
  margin-bottom: 6px;
}
.picker-label {
  color: var(--muted);
  font-size: 0.8rem;
  font-weight: var(--weight-bold);
}
/* Deliberately built like the direction toggle below it — same panel, same chip radius,
   same active colours — because the two rows are read together: language first, then
   which way round to practise it. */
.language-chips {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 6px;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 6px;
}
/* The twelve names do not fit on one row on a phone, so they wrap. Each chip is the width
   of its own name — stretching them to fill the row instead turns whichever language is
   left over onto the second row into a full-width bar that reads as a different kind of
   control altogether. Centring keeps the leftovers under the row above rather than
   hanging off one side, and nowrap stops the longest of them — "Português" — breaking in
   two, which would make its whole row taller. */
.lang-chip {
  flex: 0 0 auto;
  padding: 9px 12px;
  border-radius: 14px;
  font-size: 0.9rem;
  font-weight: var(--weight-bold);
  color: var(--text);
  white-space: nowrap;
  transition: background 0.2s, color 0.2s;
}
.lang-chip.active { background: var(--primary); color: var(--on-primary); }

/* The second row is the same control one step quieter: no filled panel behind it, and
   slightly smaller chips. Two identical panels stacked would read as one list that
   happened to wrap, which is the one thing this row must not look like — it is a
   separate question with a separate answer. Making it quieter is also what keeps it
   honest about its weight: almost everyone leaves it on English, and it should not take
   as much of a phone screen as the choice that everyone does make.
   The chips themselves keep the active colours of the row above, because a chosen
   partner is chosen in exactly the same sense. */
.partner-picker .language-chips {
  background: transparent;
  padding: 4px;
}
.partner-picker .lang-chip {
  padding: 7px 10px;
  font-size: 0.85rem;
}

.direction-toggle {
  display: flex;
  gap: 6px;
  background: var(--card);
  border: 1px solid var(--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; }

.levels { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; }
.level-card {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 12px;
  background: var(--card);
  border: 1px solid var(--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); }
.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; }
.level-name { font-size: 1.05rem; font-weight: var(--weight-heavy); }
.level-desc { color: var(--muted); font-size: 0.76rem; margin-top: 2px; }

.more-title { margin-top: 16px; }
/* Six tiles: two columns on a narrow screen, three from 480px up. Both divide the six
   exactly, so no tile is left alone on a row of its own. There is no wider breakpoint,
   and the six do not go on one line: .app is capped at 520px however wide the window
   is, and six 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. */
.tiles { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; }
@media (min-width: 480px) {
  .tiles { grid-template-columns: repeat(3, 1fr); }
}
.tile {
  background: var(--card);
  border: 1px solid var(--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); }
.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); }
/* 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; }

/* ===== "For learning only" =====
   The short form of the disclaimer, on the screens where a translation is being read.
   It is quiet on purpose — muted, small, out of the way of the practice — but it is
   text on the page and not a tooltip: a title attribute shows on hover alone, which
   means it does not exist on a phone, and this app is used on phones. The title beside
   it carries the longer wording as a bonus for a desktop reader, never as the notice
   itself. */
.study-notice {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  color: var(--muted);
  font-size: 0.76rem;
  line-height: 1.5;
  margin-top: 14px;
}
/* One badge for the fine print wherever it appears — the home screen's notice and the
   grouped notices beside a word list share it, so the two screens cannot drift apart. */
.study-notice-icon,
.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;
}
.study-notice a {
  color: var(--muted);
  white-space: nowrap;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  transition: color 0.15s;
}
.study-notice a:hover { color: var(--primary); }

.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); }
.site-footer {
  text-align: center;
  color: var(--muted);
  font-size: 0.8rem;
  margin-top: 36px;
  letter-spacing: 0.03em;
}
.legal-links {
  display: flex;
  justify-content: center;
  flex-wrap: wrap;
  gap: 8px 14px;
  margin-bottom: 10px;
  /* 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;
}
.legal-links a {
  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;
  margin-bottom: 22px;
}
.icon-btn {
  width: 42px;
  height: 42px;
  border-radius: 13px;
  background: var(--card);
  border: 1px solid var(--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(--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 — inside the block the two
     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 {
  padding: 17px 18px;
  border-radius: var(--radius-btn);
  background: var(--card);
  border: 1px solid var(--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;
}
/* 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); }
.answer-btn.correct {
  background: var(--success-bg);
  border-color: var(--success);
  color: var(--success);
  font-weight: var(--weight-bold);
}
.answer-btn.wrong {
  background: var(--error-bg);
  border-color: var(--error);
  color: var(--error);
  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(--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. Neither of these two buttons holds text worth selecting, so taking the
   selection away leaves a press as the only thing a hold can mean. */
.answer-btn,
.answer-speak {
  -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(--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); }
.type-input.correct {
  background: var(--success-bg);
  border-color: var(--success);
  color: var(--success);
  font-weight: var(--weight-bold);
}
.type-input.wrong {
  background: var(--error-bg);
  border-color: var(--error);
  color: var(--error);
  font-weight: var(--weight-bold);
}
.type-actions { display: flex; gap: 10px; }
.type-actions .btn { flex: 1; padding: 13px; }

/* 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.

   :not(:empty) carries the spacing, so the line costs nothing on a question answered
   right: an empty <p> is zero-height, and the element stays in the DOM between
   questions rather than being hidden, which is what keeps role="status" announcing. */
.type-reveal {
  text-align: center;
  font-size: 1rem;
  font-weight: var(--weight-bold);
  color: var(--success);
}
.type-reveal:not(:empty) { margin-top: 12px; }

/* The line that reported the result of an answer — .quiz-feedback, with a green and a
   red variant — is gone: the answer buttons and the typing field carry the result in
   their own colours, and whether an answer was right is not said in words. It reserved
   32px whether it had anything to say or not, so the quiz screen is that much shorter
   than it was. .type-reveal above is not that line coming back — it names the answer to
   a missed typed question, which no colour on that screen can, and it reserves nothing
   until it has something to say.

   The continue button for manual advancing took its place below the answers, which is
   where the hand already is after choosing one. */
/* 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); }

.quiz-next { margin-top: 10px; padding: 13px; font-size: 1.05rem; }
.quiz-bottom { text-align: center; margin-top: 8px; font-size: 0.9rem; }

/* ===== 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 action buttons 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. */
.results-actions {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-top: 20px;
}
.results-actions .btn { padding: 11px 14px; font-size: 1rem; }
.results-actions-top { margin: 16px 0 0; }

.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;
}
.btn:active { transform: scale(0.98); }
.btn-primary { background: var(--primary); color: var(--on-primary); }
.btn-ghost {
  background: var(--card);
  border: 1px solid var(--border);
  color: var(--text);
}
.btn-danger {
  background: var(--error-bg);
  border: 1px solid var(--error);
  color: var(--error);
}
#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;
  margin-bottom: 20px;
}
.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, 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(--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 */
#mywords-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; }
/* The card headings are h2 — h1 is the screen title, and jumping to h3 would skip a
   level in the heading hierarchy (WCAG 1.3.1). 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; }
.voice-diag { font-size: 0.78rem; margin-top: 12px; line-height: 1.5; }
.voice-diag:empty { display: none; }

/* 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 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 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-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; }

.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; }
}

/* ===== The suggestion to install the app =====
   It sits at the bottom of the screen, in the same card shape as the consent banner —
   the two banners never appear together, so they share the same place. The updated word
   list message sits at the top of the screen instead and does not collide with them. */
#install-bar {
  position: fixed;
  right: 12px;
  left: 12px;
  bottom: calc(12px + env(safe-area-inset-bottom, 0px));
  z-index: 55;
  margin: 0 auto;
  max-width: 560px;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px;
  padding: 14px 16px;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-modal);
  transform: translateY(calc(100% + 24px));
  transition: transform 0.3s;
}
#install-bar.visible { transform: translateY(0); }
/* The toast sits at the bottom of the screen too, so it is lifted above the banner for
   as long as the banner is open */
body.install-banner-open #toast { bottom: calc(12px + var(--install-banner-h, 0px)); }
.install-bar-icon {
  flex: 0 0 auto;
  width: 40px;
  height: 40px;
  border-radius: 12px;
  background: var(--success-bg);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.15rem;
}
.install-text { flex: 1 1 190px; font-size: 0.9rem; line-height: 1.5; }
/* The two buttons wrap together, otherwise on a narrow screen each would drop onto a
   line of its own */
.install-actions { flex: 1 1 auto; display: flex; gap: 10px; }
/* width: auto cancels the width: 100% of .btn */
.install-actions .btn { flex: 1 1 auto; width: auto; padding: 11px 14px; font-size: 0.95rem; }

@media (max-width: 380px) {
  .install-text { font-size: 0.85rem; }
  .install-actions .btn { padding: 11px 10px; font-size: 0.88rem; }
  /* 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; }
}

/* ===== The install card on the settings screen ===== */
.install-status { line-height: 1.65; margin-bottom: 12px; }
#install-card .btn + .btn { margin-top: 10px; }
/* The heading takes focus when arriving from the suggestion at the bottom of the screen,
   so it is not given a focus ring except under real keyboard navigation — exactly like
   the screen headings. */
#install-title:focus { outline: none; }
#install-title:focus-visible { outline: 3px solid var(--primary); outline-offset: 4px; }
.install-steps { margin-bottom: 14px; font-size: 0.88rem; }
.install-steps-title { font-weight: var(--weight-bold); margin-bottom: 8px; }
.install-steps ol { padding-left: 20px; display: grid; gap: 6px; }
.install-steps li { line-height: 1.55; }

/* ===== Consent to local storage ===== */
.modal-text a { color: var(--primary); }
/* 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; }

/* ===== Compact home screen — everything fits on one screen without scrolling ===== */
#screen-home .home-header { margin: 4px 0 14px; }
#screen-home .logo { width: 48px; height: 48px; border-radius: 14px; }
#screen-home .home-titles h1 { font-size: 1.55rem; }
#screen-home .language-picker { gap: 4px; }
#screen-home .lang-chip { padding: 8px 10px; }
#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. Six tiles stacked cost the home screen about
   230px; as rows they cost about 150px, and that difference is the footer being on the
   screen rather than below it.

   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 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; }
#screen-home .tile-sub { grid-area: sub; align-self: start; font-size: 0.74rem; }

/* 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) {
  /* Every row of chips is height the level tiles below them need, and at this width the
     names run to three rows — four at 320px. Smaller text and tighter padding fit about
     four names to a row, which is what keeps the tiles on screen without scrolling.
     Measured with all twelve names at 320, 360 and 390px: the tiles stay in view. */
  #screen-home .language-chips { gap: 4px; padding: 4px; }
  #screen-home .lang-chip { padding: 6px 8px; font-size: 0.8rem; }
}

/* Short screens — a little more tightening to avoid scrolling */
@media (max-height: 720px) {
  #screen-home .home-header { margin: 0 0 10px; }
  #screen-home .logo { width: 40px; height: 40px; border-radius: 12px; }
  #screen-home .home-titles h1 { font-size: 1.35rem; }
  #screen-home .picker-label { font-size: 0.75rem; }
  /* The chip rows are the height this screen pays for showing every language, so on a
     short screen they are tightened rather than folded back into a drop-down. The
     padding matches the narrow-width block above deliberately, and the font size is left
     alone here: this block comes later in the file, so setting it would overrule the
     narrow-width size on a phone that is both narrow and short — the very screen that
     can least afford the extra rows.
     What gives way first is the space *between* the two pickers, not the chips inside
     them. Shrinking the chips again would take them under the size a thumb can reliably
     hit, and the second row is a choice like any other even if few people change it. */
  #screen-home .language-chips { gap: 4px; padding: 4px; }
  #screen-home .lang-chip { padding: 6px 8px; }
  #screen-home .language-picker { margin-bottom: 2px; gap: 3px; }
  #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; }
}

/* ===== Responsive ===== */
@media (min-width: 560px) {
  .app { padding-top: 36px; }
  .quiz-word { font-size: 2.7rem; }
}
