:root {
  color-scheme: dark;
  --bg-color: #121212;
  --surface-color: #242424;
  --text-color: #ffffff;
  --accent-green: #4ade80;
  --accent-pink: #f472b6;
  --border-light: #aaaaaa;
  --focus-color: #fdfd96;
  --on-accent: #000000;
  --ui-scale: 1.25;
  --passage-scale: 2.5;
  --passage-font: system-ui, -apple-system, "Segoe UI", Arial, sans-serif;
  --passage-weight: 600;
}

:root[data-theme="light"] {
  color-scheme: light;
  --bg-color: #f5f5f5;
  --surface-color: #dddddd;
  --text-color: #000000;
  --border-light: #444444;
  --focus-color: #9f9f03;
  --accent-green: #097732;
  --accent-pink: #800d48;
  --on-accent: #ffffff;
}

* {
  box-sizing: border-box;
}

/* The root stays a fixed 16px on purpose - --ui-scale is a text-size
   control, not a page-zoom control. Every component font-size is written
   as calc(Xrem * var(--ui-scale)) so text grows without dragging
   container padding/gaps/dialog widths along with it. */
html {
  font-size: 16px;
  /* Inherited properties, so this themes every scrollbar in the app (the
     page itself, the typing box, dialogs) at once, and updates live with
     the dark/light toggle since these reference the same theme-swapped
     custom properties everything else uses - without this, the browser's
     default scrollbar stays a fixed light-gray that clashes hard with
     the near-black dark theme. */
  scrollbar-color: var(--border-light) var(--surface-color);
  scrollbar-width: thin;
}

body {
  margin: 0;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  background-color: var(--bg-color);
  color: var(--text-color);
  font-family: system-ui, -apple-system, sans-serif;
  line-height: 1.6;
  font-size: calc(1rem * var(--ui-scale));
}
.site-header,
.site-footer {
  flex-shrink: 0;
}

[hidden] {
  display: none !important;
}

a {
  color: var(--focus-color);
  text-decoration: underline;
}
a:hover,
a:focus-visible {
  color: var(--focus-color);
  text-decoration: underline;
}

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  background: var(--focus-color);
  color: #000000;
  padding: 0.75rem 1.25rem;
  font-weight: bold;
  z-index: 1000;
  border-radius: 0 0 8px 0;
  text-decoration: none;
}
.skip-link:focus {
  left: 0;
}
.skip-link:focus-visible {
  color: #000000;
}

*:focus-visible {
  outline: 3px solid var(--focus-color);
  outline-offset: 2px;
}

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  border: 0;
  white-space: nowrap;
}

/* --- Header --- */
.site-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.75rem;
  padding: 1.25rem 1.5rem;
  border-bottom: 2px solid var(--border-light);
}

.back-link-bar {
  padding: 0.75rem 1.5rem;
}
/* Named back-link-app (not back-link) deliberately - must not collide
   with the root site's .back-link class, which gets a history-aware
   click handler elsewhere; this link always goes to a fixed
   destination (this app's own info page) and should stay a plain link. */
.back-link-app {
  display: inline-block;
  color: var(--accent-green);
  font-weight: bold;
  font-size: calc(0.9rem * var(--ui-scale));
  text-decoration: none;
}
.back-link-app:hover,
.back-link-app:focus-visible {
  text-decoration: underline;
}

.site-title {
  /* Comes after .header-controls in the DOM (so the speech toggle inside
     it is the very first Tab stop on the page, letting a screen reader
     user silence in-app speech before hearing anything else) - this order
     value restores the original left-to-right visual position without
     touching tab order, which always follows DOM order regardless of
     this property. */
  order: 1;
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  color: var(--text-color);
  font-family: inherit;
  font-size: calc(1.75rem * var(--ui-scale));
  font-weight: 800;
  cursor: pointer;
}
.site-title:hover,
.site-title:focus-visible {
  color: var(--accent-green);
}
.site-title .accent-pink { color: var(--accent-pink); }

.header-controls {
  order: 2;
  display: flex;
  align-items: center;
  gap: 0.75rem;
  flex-wrap: wrap;
}
.header-controls #settings-btn {
  padding: 0.4em 0.9em;
  font-size: calc(0.875rem * var(--ui-scale));
}

/* gap + padding (not overflow: hidden) - overflow: hidden would clip a
   focus ring on the last button, since it needs to render outside the
   button's own box but was being clipped by the group's rounded corner. */
.theme-toggle-group {
  display: flex;
  gap: 4px;
  border: 2px solid var(--border-light);
  border-radius: 8px;
  padding: 3px;
}
.theme-toggle-btn {
  background-color: transparent;
  color: var(--text-color);
  border: none;
  border-radius: 6px;
  padding: 0.4em 0.9em;
  font-size: calc(0.875rem * var(--ui-scale));
  font-weight: bold;
  cursor: pointer;
}
.theme-toggle-btn:hover,
.theme-toggle-btn:focus-visible {
  background-color: var(--border-light);
  color: var(--bg-color);
}
.theme-toggle-btn[aria-checked="true"] {
  background-color: var(--accent-pink);
  color: var(--on-accent);
}

.speech-toggle-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  background: transparent;
  border: 2px solid var(--border-light);
  border-radius: 8px;
  color: var(--text-color);
  padding: 0.4em 0.9em;
  font-size: calc(0.875rem * var(--ui-scale));
  font-weight: bold;
  cursor: pointer;
}
.speech-toggle-btn:hover,
.speech-toggle-btn:focus-visible {
  background-color: var(--border-light);
  color: var(--bg-color);
}
.speech-toggle-btn[aria-pressed="true"] {
  background-color: var(--accent-green);
  color: var(--on-accent);
  border-color: var(--accent-green);
}

/* The Settings dialog's own Theme section is a duplicate of the header's
   inline toggle, only meant to be reachable on narrow viewports where the
   header hides its copy (see the max-width: 960px block below) - hidden
   here by default so desktop doesn't show the same control twice. */
.settings-section-theme { display: none; }
@media (max-width: 960px) {
  .header-controls > .theme-toggle-group,
  .header-controls > #speech-quick-toggle {
    display: none;
  }
  .settings-section-theme { display: block; }
}

/* --- Buttons --- */
.btn {
  display: inline-block;
  background-color: transparent;
  color: var(--text-color);
  border: 2px solid var(--accent-green);
  padding: 0.625em 1.25em;
  border-radius: 8px;
  font-weight: bold;
  text-decoration: none;
  font-size: calc(1rem * var(--ui-scale));
  cursor: pointer;
  font-family: inherit;
}
.btn:hover,
.btn:focus-visible {
  background-color: var(--accent-green);
  color: var(--on-accent);
}
.btn-primary {
  background-color: var(--accent-green);
  color: var(--on-accent);
}
.btn-primary:hover,
.btn-primary:focus-visible {
  filter: brightness(1.1);
}
.btn-secondary {
  border-color: var(--border-light);
}
.btn-secondary:hover,
.btn-secondary:focus-visible {
  background-color: var(--border-light);
  color: var(--bg-color);
}
.btn-danger {
  border-color: var(--accent-pink);
}
.btn-danger:hover,
.btn-danger:focus-visible {
  background-color: var(--accent-pink);
  color: var(--on-accent);
}
.btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}
.btn-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  align-items: center;
}
.start-row {
  margin-top: 3.5rem;
}

/* --- Layout --- */
main {
  max-width: 900px;
  width: 100%;
  margin: 0 auto;
  padding: 2.5rem 1.5rem 4rem;
}
/* While playing, the whole page is capped to the viewport height and
   forbidden from scrolling itself - the back-link and footer are hidden
   to free up vertical space, and the typing box below is the only thing
   that scrolls internally, so the sidebar, current line, and controls
   are always all visible at once without paging the whole site. */
body.is-playing {
  height: 100vh;
  overflow: hidden;
}
body.is-playing .site-footer,
body.is-playing .back-link-bar {
  display: none;
}
body.is-playing main {
  max-width: 1800px;
  flex: 1;
  display: flex;
  flex-direction: column;
  min-height: 0;
  padding-top: 1.25rem;
  padding-bottom: 1.25rem;
  padding-left: 2rem;
  padding-right: 2rem;
}
body.is-playing #screen-game {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-height: 0;
}

.screen h1 {
  font-size: calc(2.25rem * var(--ui-scale));
  margin-top: 0;
  margin-bottom: 0.5rem;
}
.screen h2 {
  color: var(--focus-color);
  font-size: calc(1.5rem * var(--ui-scale));
  margin-bottom: 0.75rem;
}
.screen-intro {
  font-size: calc(1.125rem * var(--ui-scale));
  color: var(--border-light);
  max-width: 640px;
  margin: 0 0 2rem;
}

/* --- feature-section (How to Play) --- */
.feature-section {
  background-color: var(--surface-color);
  border: 2px solid var(--border-light);
  border-radius: 10px;
  margin-bottom: 1.75rem;
  padding: 0.25rem 1.5rem;
}
.feature-section summary {
  cursor: pointer;
  font-size: calc(1.25rem * var(--ui-scale));
  line-height: 1;
  font-weight: bold;
  padding: 1.1em 0;
  color: var(--accent-pink);
  list-style: none;
  display: flex;
  align-items: center;
  gap: 0.5rem;
}
.feature-section summary:hover,
.feature-section summary:focus-visible {
  color: var(--focus-color);
}
.feature-section summary::-webkit-details-marker { display: none; }
.feature-section summary::before {
  content: "+";
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.1em;
  height: 1.1em;
  flex-shrink: 0;
  font-size: 1.4em;
  line-height: 1;
  color: var(--accent-green);
}
.feature-section[open] summary::before { content: "\2212"; }
.feature-section[open] summary {
  border-bottom: 2px solid var(--border-light);
  margin-bottom: 0.5rem;
}
.feature-section ul {
  padding-left: 1.5rem;
  padding-bottom: 1.25rem;
}
.feature-section li { margin-bottom: 0.6rem; font-size: calc(1rem * var(--ui-scale)); }
.feature-section kbd,
.shortcuts-list kbd {
  background-color: var(--bg-color);
  border: 2px solid var(--focus-color);
  color: var(--text-color);
  padding: 0.1em 0.45em;
  border-radius: 4px;
  font-family: monospace;
  font-size: 0.9em;
}
.shortcuts-list {
  list-style: none;
  margin: 0;
  padding: 0;
}
.shortcuts-list li {
  padding: 0.75em 0;
  border-bottom: 2px solid var(--border-light);
  font-size: calc(1rem * var(--ui-scale));
}
.shortcuts-list li:last-child {
  border-bottom: none;
  padding-bottom: 0;
}

/* --- Choice picker (theme and test length) --- */
.choice-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0.9rem;
  margin: 1.25rem 0 2rem;
}
.choice-card {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  text-align: left;
  width: 100%;
  background-color: var(--surface-color);
  color: var(--text-color);
  border: 2px solid var(--border-light);
  border-radius: 10px;
  padding: 1em 1.25em;
  font-family: inherit;
  cursor: pointer;
}
.choice-card:hover {
  border-color: #ffffff;
  background-color: rgba(255, 255, 255, 0.15);
}
.choice-card[aria-checked="true"] {
  border-color: var(--accent-green);
  border-width: 3px;
  background-color: color-mix(in srgb, var(--accent-green) 15%, var(--surface-color));
}
/* Selected-state color coding by choice group, so at a glance the age
   group/level picker (yellow), mode picker (green, the shared default
   above), and test length picker (pink) are visually distinct from one
   another - unselected cards stay neutral gray in every group. */
/* Name-only cards (no description line), so when the grid stretches a row
   to match its tallest card - a wrapped "Middle School" next to a
   single-line "Younger" - the label would sit pinned to the top of the
   short ones. Centring keeps the row visually level. */
#band-grid .choice-card,
#age-grid .choice-card {
  justify-content: center;
}
#age-grid .choice-card[aria-checked="true"],
#band-grid .choice-card[aria-checked="true"] {
  border-color: var(--focus-color);
  background-color: color-mix(in srgb, var(--focus-color) 15%, var(--surface-color));
}
#length-grid .choice-card[aria-checked="true"] {
  border-color: var(--accent-pink);
  background-color: color-mix(in srgb, var(--accent-pink) 15%, var(--surface-color));
}
.choice-card .choice-name {
  font-size: calc(1.2rem * var(--ui-scale));
  font-weight: 800;
}
.choice-card .choice-desc {
  font-size: calc(0.95rem * var(--ui-scale));
  color: var(--border-light);
}
.choice-card[aria-checked="true"] .choice-desc {
  color: var(--text-color);
}
/* Theme pickers (Sentences/Paragraphs/Words) are a native <select> rather
   than a choice-grid of radio cards - with dozens of themes per tier, a
   dropdown is far more scannable than a long grid of buttons, and a
   native select already comes with fully accessible keyboard behavior
   (arrow keys change the value, typing a letter jumps to a matching
   option) with no extra ARIA wiring needed - the global arrow-key
   nav-item handler explicitly steps aside for SELECT elements (see the
   ArrowUp/ArrowDown branch in the keydown listener) so it never fights
   the browser's own behavior here. Font size matches .choice-card
   .choice-name exactly, so the text reads the same size it did as cards. */
/* The native select arrow is swapped for a bolder, yellow (--focus-color)
   chevron - appearance:none drops the OS-drawn one, and the replacement is
   a data-URI SVG rather than a currentColor/var() reference, since neither
   resolves inside an SVG used as a background-image. Two copies (dark vs
   light) keep it in sync with --focus-color per theme. */
.theme-select {
  display: block;
  width: 100%;
  margin: 1.25rem 0 2rem;
  padding: 0.7em 2.5em 0.7em 1em;
  font-size: calc(1.2rem * var(--ui-scale));
  font-family: inherit;
  font-weight: 800;
  color: var(--text-color);
  background-color: var(--surface-color);
  border: 2px solid var(--border-light);
  border-radius: 10px;
  appearance: none;
  -webkit-appearance: none;
  -moz-appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23fdfd96' stroke-width='3.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 1.35em center;
  background-size: 1.1em;
}
:root[data-theme="light"] .theme-select {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%239f9f03' stroke-width='3.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
}
/* #band-grid, #mode-grid, and #length-grid get an extra step most other
   choice-grids don't need: 1 column when narrow (the shared default
   above), 2 once a pair of cards actually has room to sit comfortably
   side by side, 4 once there's enough room for a full row. 540px (well
   under the 961px "desktop" breakpoint) is where two ~200px cards plus
   their gap and the page's own side padding actually starts to fit -
   anything higher was dropping to 1 column while there was still
   clearly enough room for 2.
   #age-grid has its own 3-column jump at the same breakpoint instead -
   it holds exactly 3 short-text cards (Kids/Adults/Custom), so there's
   no useful 2-column middle step the way the 4-item grids have; it goes
   straight from 1 column to a full row of 3. Set via its own ID rule (not
   folded into the .choice-grid generic 2-column rule below) so its
   higher specificity keeps it at 3 columns even past 961px, where that
   generic rule would otherwise drop it back to 2. */
@media (min-width: 540px) {
  #band-grid,
  #mode-grid,
  #length-grid {
    grid-template-columns: repeat(2, 1fr);
  }
  #age-grid {
    grid-template-columns: repeat(3, 1fr);
  }
}
@media (min-width: 961px) {
  .choice-grid {
    grid-template-columns: repeat(2, 1fr);
  }
  .theme-select {
    width: 50%;
  }
  #band-grid,
  #mode-grid,
  #length-grid {
    grid-template-columns: repeat(4, 1fr);
  }
  #custom-source-grid {
    grid-template-columns: repeat(3, 1fr);
  }
}

/* --- Custom text panel --- */
.custom-text-panel {
  background-color: var(--surface-color);
  border: 2px solid var(--accent-green);
  border-radius: 10px;
  padding: 1.25rem 1.5rem;
  margin: -1rem 0 2rem;
}
.custom-text-panel label {
  display: block;
  font-weight: bold;
  font-size: calc(1.05rem * var(--ui-scale));
  margin-bottom: 0.4rem;
}
.custom-text-panel textarea {
  width: 100%;
  min-height: 8rem;
  resize: vertical;
  padding: 0.75em;
  font-size: calc(1rem * var(--ui-scale));
  font-family: inherit;
  color: var(--text-color);
  background-color: var(--bg-color);
  border: 2px solid var(--border-light);
  border-radius: 8px;
}
.custom-text-panel .field-hint {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
}
.custom-text-panel h3 {
  font-size: calc(1.1rem * var(--ui-scale));
  color: var(--accent-pink);
  margin: 1.5rem 0 0.75rem;
}
.save-text-row {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.6rem;
  margin-top: 0.75rem;
}
.save-text-row input[type="text"] {
  width: 100%;
  max-width: 24rem;
  padding: 0.6em 0.75em;
  font-size: calc(1rem * var(--ui-scale));
  font-family: inherit;
  color: var(--text-color);
  background-color: var(--bg-color);
  border: 2px solid var(--border-light);
  border-radius: 8px;
}
.custom-text-panel input[type="file"] {
  display: block;
  width: 100%;
  color: var(--text-color);
  font-size: calc(0.95rem * var(--ui-scale));
  padding: 0.5em 0;
}
.custom-text-panel input[type="file"]::file-selector-button {
  background-color: var(--accent-green);
  color: var(--on-accent);
  border: none;
  border-radius: 8px;
  padding: 0.55em 1.1em;
  margin-right: 0.9em;
  font-weight: bold;
  font-size: calc(0.95rem * var(--ui-scale));
  font-family: inherit;
  cursor: pointer;
}
.custom-text-panel input[type="file"]::file-selector-button:hover {
  filter: brightness(1.1);
}

.saved-texts-section {
  margin-top: 1.5rem;
}
#saved-texts-empty {
  margin-bottom: 1.5rem;
}
.saved-texts-list {
  list-style: none;
  margin: 0 0 0.5rem;
  padding: 0;
  max-height: 12rem;
  overflow-y: auto;
  border: 2px solid var(--border-light);
  border-radius: 8px;
}
.saved-text-item {
  display: flex;
  align-items: stretch;
  border-bottom: 2px solid var(--border-light);
}
.saved-text-item:last-child {
  border-bottom: none;
}
.saved-text-name {
  flex: 1 1 auto;
  text-align: left;
  background: transparent;
  border: none;
  color: var(--text-color);
  padding: 0.65em 0.9em;
  font-size: calc(0.95rem * var(--ui-scale));
  font-family: inherit;
  cursor: pointer;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.saved-text-name:hover,
.saved-text-name:focus-visible {
  background-color: color-mix(in srgb, var(--accent-green) 18%, transparent);
  color: var(--accent-green);
}
.saved-text-delete {
  flex-shrink: 0;
  background: transparent;
  border: none;
  border-left: 2px solid var(--border-light);
  color: var(--accent-pink);
  width: 2.75em;
  font-size: calc(1.1rem * var(--ui-scale));
  cursor: pointer;
}
.saved-text-delete:hover,
.saved-text-delete:focus-visible {
  background-color: var(--accent-pink);
  color: var(--on-accent);
}

/* --- Game screen: sidebar HUD + typing canvas (mirrors Falling Words'
   game-hud + fall-stage layout) --- */
.game-play-area {
  flex: 1;
  display: flex;
  flex-direction: row;
  align-items: stretch;
  gap: 1rem;
  min-height: 0;
}
.game-hud {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 1rem;
  width: 16rem;
  flex-shrink: 0;
  overflow-y: auto;
  background-color: var(--surface-color);
  border: 2px solid var(--border-light);
  border-radius: 10px;
  padding: 1rem;
}
.gauge-svg-wrap svg {
  width: 100%;
  height: auto;
  display: block;
}
#end-gauge-needle {
  transform-origin: 100px 100px;
  transition: transform 0.35s cubic-bezier(0.34, 1.2, 0.64, 1);
}
.hud-stats {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 0.6rem;
  font-size: calc(1.05rem * var(--ui-scale));
  font-weight: bold;
}
/* Label and value sit on one line, label left / value right, to keep each
   stat compact - important now that the whole game screen is height
   -constrained to the viewport instead of letting the page scroll. */
.hud-stat {
  display: flex;
  flex-direction: row;
  justify-content: space-between;
  align-items: baseline;
  gap: 0.75rem;
}
.hud-stats .hud-label {
  color: var(--border-light);
  font-weight: normal;
  font-size: calc(0.9rem * var(--ui-scale));
  white-space: nowrap;
}
.hud-value {
  font-size: calc(1.25rem * var(--ui-scale));
  font-weight: 800;
  white-space: nowrap;
}
.hud-value-time { color: var(--focus-color); }
.hud-value-wpm { color: var(--accent-green); }
.hud-value-accuracy { color: var(--accent-pink); }

.game-hud .btn-row {
  flex-direction: column;
  flex-wrap: nowrap;
  align-items: stretch;
}
.game-hud .btn-row .btn {
  width: 100%;
  text-align: center;
}

/* Stats Position: "Across the top". The sidebar becomes a single full-width
   row above the passage - same markup, same HUD, just re-flowed - which
   buys the passage the full width of the screen. Below 960px the layout
   already stacks, so this only really changes the desktop picture. */
.game-play-area.stats-top {
  flex-direction: column;
}
.game-play-area.stats-top .game-hud {
  width: auto;
  flex-direction: row;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 1rem 1.5rem;
  overflow-y: visible;
  padding: 0.75rem 1rem;
}
.game-play-area.stats-top .hud-stats {
  flex-direction: row;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.5rem 1.75rem;
  flex: 1 1 auto;
}
.game-play-area.stats-top .hud-stat {
  flex-direction: row;
  justify-content: flex-start;
  gap: 0.4rem;
}
.game-play-area.stats-top .game-hud .btn-row {
  flex-direction: row;
  flex: 0 0 auto;
}
.game-play-area.stats-top .game-hud .btn-row .btn {
  width: auto;
}

@media (max-width: 960px) {
  .game-play-area {
    flex-direction: column;
  }
  .game-hud {
    width: 100%;
    flex-direction: row;
    flex-wrap: wrap;
    justify-content: space-between;
  }
  .hud-stats {
    flex-direction: row;
    flex-wrap: wrap;
  }
  .game-hud .btn-row {
    flex-direction: row;
  }
  .game-hud .btn-row .btn {
    flex: 1 1 9rem;
    width: auto;
  }
}

/* --- Typing box: a big, high-contrast canvas for the passage text,
   sized the same way Falling Words sizes its falling word - a fixed
   2.25rem base multiplied by a much larger, dedicated passage-scale
   variable, independent of the interface text size. --- */
/* Deliberately NOT display:flex/align-items:center - a scrollable flex
   container that vertically centers its content can't be scrolled all
   the way to its own start in most browsers (the centering auto-margins
   eat into the overflow region), which fights the auto-scroll-to-current
   -line behavior below. Plain block flow + text-align:center on the text
   itself gets the same centered look without that scroll bug. */
.typing-box {
  position: relative;
  flex: 1 1 auto;
  min-height: 6rem;
  background-color: var(--surface-color);
  border: 2px solid var(--border-light);
  border-radius: 12px;
  padding: 1.5rem 2rem;
  overflow-y: auto;
  /* Extra bottom padding only (never top - that's the gap that was just
     fixed above) gives scrollIntoView({block:'center'}) enough headroom
     to center the LAST line of a passage too, the same as any line in
     the middle. Without it, scrollTop simply can't go past the natural
     end of the content, so the final line would end up stuck low in the
     box instead of centered. Generous vh-based value so it stays enough
     even on a tall viewport where the box itself is tall. */
  padding-bottom: 45vh;
}
/* Letters and Words modes show a single short, non-wrapping item, so they
   get the same centered "canvas" treatment Falling Words uses for its
   single falling word - safe here (unlike the general case above) since
   content this short never needs the scroll-to-center-the-current-line
   behavior, so the tall bottom padding would just be wasted space. */
.typing-box.centered-mode {
  display: flex;
  align-items: center;
  justify-content: center;
  padding-bottom: 1.5rem;
}
/* Words mode only - a word too wide for the box at the current text size
   scrolls sideways on its own single line rather than wrapping/hyphenating
   (see the JS comment where this class is toggled). min-width:0 is the
   standard flexbox fix that lets a nowrap flex child actually shrink to
   the row's available width instead of overflowing it, which is what
   makes overflow-x:hidden below have a bounded box to clip/scroll within.
   Left-aligned rather than the usual centered text, so an overflowing
   word starts flush at its first character - where typing begins -
   instead of looking cut off on both sides before anything is typed. */
.typing-box.words-scroll .typing-text {
  white-space: nowrap;
  text-align: left;
  min-width: 0;
  max-width: 100%;
  overflow-x: hidden;
}
/* Extra size boost for Letters/Words mode, Kids-Younger band only - see
   the JS comment where this class is toggled for why it's scoped so
   narrowly. Multiplies on top of the user's own passage-scale setting
   rather than a fixed size, so it still respects that preference. */
.typing-text.jumbo {
  font-size: calc(2.25rem * var(--passage-scale) * 1.5);
}
.typing-text {
  /* .typing-text is a <p>, whose default user-agent margin (~1em) is
     relative to its OWN font-size - at this huge passage-scale font size,
     that default margin alone was a large, size-scaling gap above the
     text. Zero it out; .typing-box's padding already provides spacing. */
  margin: 0;
  font-size: calc(2.25rem * var(--passage-scale));
  font-family: var(--passage-font);
  font-weight: var(--passage-weight);
  line-height: 1.4;
  letter-spacing: 0.01em;
  word-spacing: 0.08em;
  text-align: center;
  overflow-wrap: break-word;
  word-break: normal;
  hyphens: auto;
  -webkit-hyphens: auto;
  /* Default white-space collapses a trailing space at a line-wrap point
     to zero width, so the "current" highlight (background + underline)
     has nothing to paint when the character due to be typed next is one
     of those trailing spaces. pre-wrap preserves every space's box while
     still wrapping normally at whitespace, so the highlight always has
     something visible to render. */
  white-space: pre-wrap;
}
.typing-text .char {
  color: var(--border-light);
}
.typing-text .char.typed {
  color: var(--accent-green);
}
.typing-text .char.current {
  color: var(--text-color);
  text-decoration: underline;
  text-decoration-color: var(--focus-color);
  text-decoration-thickness: 5px;
  text-underline-offset: 2px;
  background-color: color-mix(in srgb, var(--focus-color) 22%, transparent);
  border-radius: 2px;
}
/* A mistake always happens on the character that's also current, so this
   has to come after .char.current in source order - otherwise the
   current-highlight's color/background would silently win the cascade
   (equal specificity, later rule wins) and the pink mistake flash would
   never actually be visible. */
.typing-text .char.mistake {
  color: var(--on-accent);
  background-color: var(--accent-pink);
  border-radius: 2px;
}
.typing-box.finished-flash {
  animation: ts-finished-flash 0.6s ease;
}
@keyframes ts-finished-flash {
  0%, 100% { border-color: var(--border-light); }
  40% { border-color: var(--accent-green); }
}

.game-hint {
  flex-shrink: 0;
  font-size: calc(0.95rem * var(--ui-scale));
  color: var(--border-light);
  text-align: center;
  margin: 0.5rem 0;
}

/* --- Pause overlay & modal backdrop --- */
.modal-backdrop {
  position: fixed;
  inset: 0;
  background-color: rgba(0, 0, 0, 0.6);
  z-index: 40;
}
/* Confirm, Settings, Shortcuts, and Clear History can each open on top of
   an already-open base dialog (Pause or History) rather than only over the
   plain page - their backdrops need a higher z-index than a base dialog
   panel, or that backdrop can never actually darken it, leaving both
   panels looking equally "on top" at once. Stacking two semi-transparent
   backdrops like this also darkens the base panel further, which is the
   visual cue that only the front dialog is currently active. */
#confirm-backdrop,
#settings-backdrop,
#shortcuts-backdrop,
#clear-history-backdrop {
  z-index: 55;
}

.pause-panel,
.settings-dialog,
.confirm-panel {
  position: fixed;
  background-color: var(--surface-color);
  color: var(--text-color);
  border: 2px solid var(--border-light);
  border-radius: 12px;
  padding: 1.75rem;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4);
  max-height: 90vh;
  overflow-y: auto;
}
.pause-panel { z-index: 50; }
#history-dialog { z-index: 50; }
.confirm-panel { z-index: 65; }
.settings-dialog { z-index: 65; }
/* Welcome borrows .settings-dialog's looks but not its stacking. It's a
   BASE dialog like Pause and History - Settings and Keyboard Shortcuts
   both open on top of it from its own buttons - so it sits below the
   z-index 55 backdrop group above, letting those backdrops darken it and
   show which panel is actually active. Its own backdrop stays at the
   plain .modal-backdrop 40. */
#welcome-dialog { z-index: 50; }
/* The change log opens FROM Settings, so it has to clear both the z-index
   65 dialog group and the 55 backdrop group - its own backdrop sits above
   Settings so it visibly darkens it, same cue every other stacked dialog
   in here gives. */
#changelog-backdrop { z-index: 70; }
#changelog-dialog { z-index: 75; }

.pause-panel,
.confirm-panel {
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: min(28rem, 92vw);
  text-align: center;
}
.pause-panel h2,
.confirm-panel h2 {
  margin-top: 0;
  font-size: calc(1.6rem * var(--ui-scale));
}
.pause-panel .btn-row,
.confirm-panel .btn-row {
  justify-content: center;
  margin-top: 1.25rem;
}
.pause-panel .btn-row {
  flex-direction: column;
  flex-wrap: nowrap;
  align-items: stretch;
}
.pause-panel .btn-row .btn {
  width: 100%;
}

.settings-dialog {
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: min(34rem, 94vw);
  /* Non-scrolling header + separately-scrolling body, rather than a
     sticky header inside the whole scrolling panel - a real, confirmed
     Chromium rendering bug lets scrolled content paint above a
     `position: sticky` header when the header's own scroll ancestor is
     itself `position: fixed` (as this panel is), even though hit-testing
     and layout are both otherwise correct. This sidesteps the bug rather
     than working around it. */
  padding: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.settings-dialog-header {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  border-bottom: 2px solid var(--border-light);
  padding: 1.75rem 1.75rem 0.9rem;
}
.settings-dialog-header h1,
.settings-dialog-header h2 {
  margin: 0;
  font-size: calc(1.6rem * var(--ui-scale));
  font-weight: bold;
  line-height: 1.3;
}
.settings-dialog-body {
  overflow-y: auto;
  flex: 1 1 auto;
  min-height: 0;
  padding: 1.25rem 1.75rem 1.75rem;
}
/* Welcome dialog reuses .settings-dialog wholesale (same panel chrome,
   same non-scrolling header over a separately-scrolling body) and adds a
   matching non-scrolling footer, so the "Don't show this again" checkbox
   and Done button stay visible no matter how far the list is scrolled. */
.welcome-dialog-footer {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 1rem;
  border-top: 2px solid var(--border-light);
  padding: 1.1rem 1.75rem 1.5rem;
}
.welcome-intro {
  margin: 0 0 1rem;
  font-size: calc(1rem * var(--ui-scale));
}
.welcome-links-lead {
  margin: 1.25rem 0 0.75rem;
  font-size: calc(0.9rem * var(--ui-scale));
  color: var(--border-light);
}
/* stretch, not the .btn-row default of centre: "Keyboard Shortcuts" wraps
   to two lines at most widths and "Settings" doesn't, so centred alignment
   left one button visibly shorter than the other. */
.welcome-btn-row {
  align-items: stretch;
}
.welcome-btn-row .btn {
  flex: 1 1 12rem;
  text-align: center;
}
.check-row {
  display: flex;
  align-items: center;
  gap: 0.6rem;
}
.check-row label {
  font-weight: bold;
  font-size: calc(1rem * var(--ui-scale));
  cursor: pointer;
}
/* A native checkbox won't take a border at all - the browser draws its own
   hairline and ignores the property - so the box is drawn here instead.
   Same 2px var(--border-light) edge every button, select and dialog in the
   app already uses, which is all the extra weight this needed. */
.check-input {
  appearance: none;
  -webkit-appearance: none;
  /* Own font-size, so the tick below can be sized in em. It was sized in
     PERCENT before, which a centred grid item resolves against a track
     that has no definite size - the box collapsed and all that rendered
     was the corner where its two borders met, i.e. a dot. */
  font-size: calc(1.4rem * var(--ui-scale));
  width: 1em;
  height: 1em;
  flex-shrink: 0;
  margin: 0;
  cursor: pointer;
  border: 2px solid var(--border-light);
  border-radius: 5px;
  background-color: var(--bg-color);
  display: inline-grid;
  place-content: center;
}
.check-input::before {
  content: "";
  width: 0.52em;
  height: 0.28em;
  border-left: 0.14em solid var(--on-accent);
  border-bottom: 0.14em solid var(--on-accent);
  transform: rotate(-45deg) translate(0.02em, -0.06em);
  opacity: 0;
}
.check-input:checked {
  background-color: var(--accent-green);
  border-color: var(--accent-green);
}
.check-input:checked::before { opacity: 1; }
.check-input:hover:not(:disabled) { border-color: var(--text-color); }
.check-input:disabled { cursor: default; opacity: 0.5; }

/* Checkboxes that depend on the switch above them. Indented under it and
   dimmed (not hidden) while that switch is off, so the list never reflows
   and the settings stay readable either way. */
/* A dependent control that isn't a checkbox - the next-character delay -
   gets the same indent and rule as .check-sublist so sub-options all read
   as belonging to the switch above them. */
.depends-nextchar {
  margin-top: 0.6rem;
  padding-left: 1.5rem;
  border-left: 2px solid var(--border-light);
}
.check-sublist {
  list-style: none;
  margin: 0.6rem 0 0;
  padding: 0 0 0 1.5rem;
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  border-left: 2px solid var(--border-light);
}
.auth-signin-hint {
  margin: 0.6rem 0 0;
}
.changelog-link {
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  color: var(--focus-color);
  text-decoration: underline;
  cursor: pointer;
}
.changelog-link:hover,
.changelog-link:focus-visible {
  color: var(--focus-color);
}
/* One <details> per release. The summary carries the <h2> itself so the
   heading outline stays h1 > h2 with nothing nested inside a release -
   screen-reader heading navigation walks the versions directly. */
.changelog-entry {
  border-bottom: 2px solid var(--border-light);
  padding-bottom: 0.9rem;
  margin-bottom: 0.9rem;
}
.changelog-entry:last-child {
  border-bottom: none;
  margin-bottom: 0;
  padding-bottom: 0;
}
.changelog-entry > summary {
  cursor: pointer;
  list-style: none;
  display: flex;
  align-items: center;
  gap: 0.5em;
}
.changelog-entry > summary::-webkit-details-marker { display: none; }
.changelog-entry > summary::before {
  content: "+";
  color: var(--accent-green);
  font-weight: 800;
  font-size: calc(1.3rem * var(--ui-scale));
  line-height: 1;
  flex-shrink: 0;
}
.changelog-entry[open] > summary::before { content: "\2212"; }
.changelog-entry > summary h2 {
  margin: 0;
  font-size: calc(1.15rem * var(--ui-scale));
  color: var(--accent-pink);
}
.changelog-entry h3 {
  font-size: calc(1rem * var(--ui-scale));
  color: var(--accent-green);
  margin: 1rem 0 0;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}
.changelog-entry ul {
  margin: 0.5rem 0 0;
  padding-left: 1.4rem;
}
.changelog-entry li {
  margin-bottom: 0.55rem;
  font-size: calc(0.95rem * var(--ui-scale));
}
.changelog-entry li:last-child { margin-bottom: 0; }
/* No border of its own: the last settings section already draws a
   bottom border above this, and adding a top border here stacked the two
   into a visible double line. */
.settings-version {
  margin: 0.25rem 0 0;
  font-size: calc(0.85rem * var(--ui-scale));
  color: var(--border-light);
  text-align: center;
}

.icon-btn {
  background: transparent;
  border: 2px solid var(--border-light);
  color: var(--text-color);
  border-radius: 50%;
  width: 2.5em;
  height: 2.5em;
  font-size: calc(1.25rem * var(--ui-scale));
  line-height: 1;
  cursor: pointer;
  flex-shrink: 0;
}
.icon-btn:hover,
.icon-btn:focus-visible {
  background-color: var(--border-light);
  color: var(--bg-color);
}

/* Gap above a section heading (the previous section's margin-bottom) and
   gap below its content (this section's padding-bottom) are deliberately
   the same number, so every divider line sits evenly between the two
   sections it separates. */
.settings-section {
  margin-bottom: 1.25rem;
  padding-bottom: 1.25rem;
  border-bottom: 2px solid var(--border-light);
}
.settings-section:last-child {
  border-bottom: none;
  margin-bottom: 0;
  padding-bottom: 0;
}
.settings-section h3 {
  font-size: calc(1.15rem * var(--ui-scale));
  color: var(--accent-pink);
  margin-top: 0;
  margin-bottom: 1rem;
}
/* Display and Text to Speech are <details>/<summary> instead of a plain
   h3 - long enough sections that collapsing them by default keeps the
   Settings dialog scannable. Selector is scoped to summary specifically
   (not "h3, summary" together) so the Theme section's real <h3> - not a
   <details>, deliberately not made collapsible - keeps its plain heading
   look instead of picking up the expand/collapse triangle. */
.settings-section summary {
  cursor: pointer;
  font-size: calc(1.15rem * var(--ui-scale));
  font-weight: bold;
  color: var(--accent-pink);
  margin-top: 0;
  margin-bottom: 1rem;
  list-style: none;
  display: flex;
  align-items: center;
  gap: 0.5em;
}
.settings-section summary::-webkit-details-marker { display: none; }
.settings-section summary::before {
  content: "+";
  display: inline-block;
  color: var(--accent-green);
  font-weight: 800;
  transform: scale(1.3);
}
.settings-section[open] summary::before {
  content: "\2212";
}
/* A collapsed <details> section is nothing but its summary line, so the
   summary's own bottom margin (which exists to separate the heading from
   the fields underneath it) would otherwise stack on top of the section's
   bottom padding and push the divider much further below the heading text
   than the matching gap above it. Nothing follows the summary while it's
   closed, so that margin is dropped entirely. */
details.settings-section:not([open]) > summary {
  margin-bottom: 0;
}
.field {
  margin-bottom: 1.1rem;
}
.field:last-child { margin-bottom: 0; }
.field label {
  display: block;
  font-weight: bold;
  font-size: calc(1rem * var(--ui-scale));
  margin-bottom: 0.35rem;
}
.field-hint {
  font-size: calc(0.85rem * var(--ui-scale));
  color: var(--border-light);
  margin: 0.15rem 0 0.5rem;
}
.field select,
.field input[type="range"] {
  width: 100%;
}
.field select {
  padding: 0.6em;
  font-size: calc(1rem * var(--ui-scale));
  border: 2px solid var(--border-light);
  border-radius: 8px;
  background-color: var(--bg-color);
  color: var(--text-color);
  font-family: inherit;
}
.field-value {
  font-weight: bold;
  color: var(--accent-green);
}
input[type="range"] {
  accent-color: var(--accent-green);
  height: 1.5rem;
}
.switch-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}
.switch-row label {
  margin-bottom: 0;
}
.switch-btn {
  background: transparent;
  border: 2px solid var(--border-light);
  border-radius: 999px;
  color: var(--text-color);
  padding: 0.4em 1em;
  font-size: calc(1rem * var(--ui-scale));
  font-weight: bold;
  cursor: pointer;
  min-width: 5em;
  flex-shrink: 0;
}
.switch-btn[aria-checked="true"] {
  background-color: var(--accent-green);
  color: var(--on-accent);
  border-color: var(--accent-green);
}

/* --- End screen --- */
#screen-end {
  text-align: center;
}
#screen-end .btn-row {
  justify-content: center;
}
.results-list {
  list-style: none;
  max-width: 26rem;
  margin: 0 auto 1.75rem;
  padding: 0;
  background-color: var(--surface-color);
  border: 2px solid var(--border-light);
  border-radius: 10px;
  overflow: hidden;
}
.results-list li {
  display: flex;
  justify-content: space-between;
  padding: 0.9em 1.25em;
  font-size: calc(1.1rem * var(--ui-scale));
  border-bottom: 2px solid var(--border-light);
}
.results-list li:last-child { border-bottom: none; }
.results-list .stat-label { font-weight: normal; color: var(--border-light); }
.results-list .stat-value { font-weight: 800; }

.end-gauge {
  max-width: 9rem;
  margin: 0 auto 1.5rem;
}

/* Resets the narrower .results-list max-width/auto-centering meant for
   the end screen - the history dialog is already its own narrow modal,
   so this list should just fill it edge to edge like before. */
.history-list {
  max-width: none;
  margin: 0 0 1.25rem;
}
.history-list .history-entry {
  display: block;
  padding: 0.9em 1.1em;
  cursor: default;
}
/* history-entry-top is an <h3> now (see script.js) so each entry has a
   real heading a screen reader can jump between - reset the browser's
   default heading font-size/margin so it renders exactly like the plain
   div it used to be, just with proper heading semantics underneath. */
.history-entry-top {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 0.5rem;
  margin: 0 0 0.4rem;
  font-size: 1em;
  font-weight: 800;
}
.history-entry-mode {
  color: var(--accent-green);
}
.history-entry-date {
  color: var(--border-light);
  font-weight: normal;
  font-size: calc(0.85rem * var(--ui-scale));
}
.history-entry-stats {
  display: flex;
  flex-wrap: wrap;
  gap: 0.9rem;
  font-size: calc(0.95rem * var(--ui-scale));
  color: var(--border-light);
}
.history-entry-stats strong {
  color: var(--text-color);
}
.encouragement {
  font-size: calc(1.3rem * var(--ui-scale));
  font-weight: bold;
  color: var(--accent-pink);
  margin-bottom: 1.75rem;
}

/* --- Footer --- */
.site-footer {
  border-top: 2px solid var(--border-light);
  padding: 1.5rem;
  text-align: center;
  font-size: calc(0.875rem * var(--ui-scale));
}
.site-footer p { margin: 0.4rem 0; }
