/*
  PURPOSE: Visual design for the LINC Keyboarding Practice prototype.
  WHY: Uses large touch-friendly controls, simple spacing, and low visual clutter for CLB 1L/2L learners.
*/

@font-face {
  font-family: "Inter Local";
  src: url("fonts/Inter-latin.woff2?v=cb3460489bf1") format("woff2");
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Andika Local";
  src: url("fonts/andika-latin-400-normal.woff2?v=cb3460489bf1") format("woff2");
  font-style: normal;
  font-weight: 400;
  font-display: swap;
}

@font-face {
  font-family: "Andika Local";
  src: url("fonts/andika-latin-700-normal.woff2?v=cb3460489bf1") format("woff2");
  font-style: normal;
  font-weight: 700;
  font-display: swap;
}

@font-face {
  font-family: "Comic Neue Local";
  src: url("fonts/comic-neue-latin-400-normal.woff2?v=cb3460489bf1") format("woff2");
  font-style: normal;
  font-weight: 400;
  font-display: swap;
}

@font-face {
  font-family: "Comic Neue Local";
  src: url("fonts/comic-neue-latin-700-normal.woff2?v=cb3460489bf1") format("woff2");
  font-style: normal;
  font-weight: 700;
  font-display: swap;
}

@font-face {
  font-family: "Atkinson Hyperlegible Local";
  src: url("fonts/atkinson-hyperlegible-latin-400-normal.woff2?v=cb3460489bf1") format("woff2");
  font-style: normal;
  font-weight: 400;
  font-display: swap;
}

@font-face {
  font-family: "Atkinson Hyperlegible Local";
  src: url("fonts/atkinson-hyperlegible-latin-700-normal.woff2?v=cb3460489bf1") format("woff2");
  font-style: normal;
  font-weight: 700;
  font-display: swap;
}

:root {
  --page-bg: #f4f7fb;
  --card-bg: #ffffff;
  --text-main: #172033;
  /* Darkened from #465061, which read 6.86:1 on the settings drawer — just under
     the 7:1 AAA target. This clears it on every surface with room to spare. */
  --text-muted: #424c5b;
  --border: #d8e0ec;
  --field-border: #7387ab;
  --primary: #2e39a6;
  /* One hint colour for every marker, in every activity. Deliberately neutral:
     yellow means "wrong", green means "correct", blue means "press me" — a
     marker that only says "look here" must not borrow any of those. The solid
     edge carries the signal; the fill stays pale so text over it stays legible. */
  --hint-bg: #dddfe2;
  --hint-border: #424c5b;
  --primary-soft: #e9ebf7;
  /* The CLB level tiles sit on the white landing card, so a white fill leaves
     them reading as panels rather than buttons. Stronger than --primary-soft,
     which is the hover step above it. */
  --level-tile-bg: #dde2f5;
  --level-tile-bg-strong: #cbd3ef;
  /* CLB 2 gets its own hue so the two tiles are not a matched pair. Colour is
     the second cue only — the dot count under each number is the first, so a
     colour-blind learner still tells them apart. */
  --level-tile-bg-2: #dbeee0;
  --level-tile-bg-2-strong: #c4e2ce;
  --space-cue: #5679c4;
  --focus-blue: #4351c9;
  --keyboard-key: #eef1f5;
  --success-bg: #d7efdc;
  --success-text: #10532c;
  /* 7.32:1 on white, not the 4.79:1 #3f7f57 gave. These tokens are not just
     borders: they fill the round feedback icon under a white glyph and colour
     the check mark on the results list, which is text on a white card. The
     TIES house target is 7:1 (AAA), so do not "restore" the lighter value. */
  --success-border: #2f6041;
  --warning-bg: #fff3d9;
  --warning-text: #5f3700;
  /* 7.27:1 on white; #9a6200 measured 5.10:1. Same reason as --success-border. */
  --warning-border: #7a4d00;
  --mode-subtitle: #4a4f58;
  --mode-tile-tint: #e9ebf7;
  --sample-border: #c3c9ee;
  /* The chip's own border, not --sample-border: the chip sits on white with a
     white face, so its outline is the only thing drawing the box. At #c3c9ee it
     measured 1.63:1 and a lone "." read as a speck. This is --text-muted rather
     than a new colour: 8.69:1, well past the 3:1 a box outline needs, and
     neutral so the chip stays paper next to the keycap's blue-grey key. The
     boxes that use --sample-border sit on a tinted fill and do not have this
     problem. */
  --chip-border: #424c5b;
  /* These colour the named word in a task instruction, so they are read as text
     and must clear the 7:1 target, not just AA. Teal was 6.09:1 and green
     6.32:1 on white; darkened to 8.14 and 8.25. Purple already cleared at 9.11. */
  --accent-keyboard: #0c5768;
  --accent-typing: #31581f;
  --accent-fix: #5e3a6e;
  --accent: var(--primary);
  --surface-tint: #fbfcfe;
  /* The arrow that points at the blinking cursor in a demo. Its own token
     because it is a teaching cue, not the semantic success green. */
  --cursor-arrow: #1f9d4d;
  --demo-text: #8a8a8a;
  --demo-ink: #222;
  --scrim: rgba(23, 32, 51, 0.58);
  --focus-glow: rgba(28, 63, 163, 0.14);
  --shadow: 0 10px 30px rgba(20, 34, 61, 0.08);
  --shadow-action-primary: 0 4px 12px rgba(28, 63, 163, 0.3);
  --shadow-action-secondary: 0 4px 12px rgba(20, 34, 61, 0.12);
  --shadow-control: 0 2px 5px rgba(20, 34, 61, 0.12);
  --radius-lg: 22px;
  --radius-md: 16px;
  --radius-pill: 999px;
}

/* THE DISPLAY SCALE. Classroom interactive panels run their host PC at 250-300%
   Windows scaling, which reports a SHORT css viewport, not a large one: the
   measured board is 1280x585 where a desktop is 1562x911. Width is only 18%
   off; height is 36% off, and that is what makes the card feel oversized —
   the layout is drawn for ~911px of height and gets 585px.

   So the scale is keyed on viewport HEIGHT, and every size that decides how
   much room the layout takes is in rem below, which means this one line moves
   all of them together. Fitted to the two measured devices: 911px tall
   computes to 16.00px (the 100% ceiling clamps, so desktop is unchanged) and
   585px tall computes to 10.73px — 67% of today, matching what an instructor
   currently gets by setting browser zoom to 67% by hand.

   The gate cannot catch a desktop or a tablet. A panel is driven by a PC with
   a mouse AND a touch surface, so it is the only thing reporting both pointer
   types: a plain desktop has no coarse pointer, an iPad has no fine one, and
   phones and portrait tablets are under 1000px. Percentages rather than px so
   a learner's own browser font-size setting still applies at both ends.

   Not zoom/transform: those break pointer position and text sharpness on a
   touch panel, which is the one device this exists for. */
@media (min-width: 1000px) and (any-pointer: coarse) and (any-pointer: fine) {
  html {
    font-size: clamp(67%, calc(1.62vh + 1.25px), 100%);
  }
}

* {
  box-sizing: border-box;
}

/* Opening Settings makes the page taller than the viewport, and the scrollbar
   that appears used to shove the header buttons and the card 8px left. Reserve
   the gutter so nothing moves. */
html {
  scrollbar-gutter: stable;
}

body {
  margin: 0;
  background: var(--page-bg);
  color: var(--text-main);
  font-family: "Inter", "Inter Local", sans-serif;
}

body[data-activity="start"] {
  --accent: var(--accent-keyboard);
  --page-bg: color-mix(in srgb, var(--accent-keyboard) 6%, #ffffff);
}

body[data-activity="more"] {
  --accent: var(--accent-typing);
  --page-bg: color-mix(in srgb, var(--accent-typing) 6%, #ffffff);
}

body[data-activity="fix"] {
  --accent: var(--accent-fix);
  --page-bg: color-mix(in srgb, var(--accent-fix) 6%, #ffffff);
}

/* Pointer images. Named --pointer-*, NOT --cursor-*: --cursor-arrow was already
   taken, above, by the green teaching arrow in the Text Cursor demo, and a
   second meaning on that name silently turned that arrow black.

   The arrow and the I-beam are built in script.js (applyCursorSettings) because
   size and colour are learner choices and a data URI cannot be tinted by a CSS
   variable. The hand is a real Windows large-link bitmap, so it cannot be tinted
   or resized at run time either — instead each colour is a pre-tinted copy, and
   Normal size falls back to the system hand rather than shrinking a bitmap.
   The trailing keyword is the fallback if an image fails; the numbers before it
   are the hotspot, the pixel that actually clicks.
   Touch screens have no cursor, so none of this reaches a finger. */
:root {
  --pointer-hand-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAD8ElEQVR42u2be0gUQRzHD4QgCIThF1aIguRIhiGKYKUpWWYYhiSKUSIVhvRHYgmBdWEvS0IMS3pYhmKYYmWIFokRSg9LtIdiCJIYkpiiYYkhTHyXXTk8Tw29273d/cLAsT/uxvncjPN7ncWyRDHi2Yx4LyP+nRHPtRhJjHjxgbRjoq+vXwwO/hBZ2acFI15ilMVHRMckiunpaWGrlNQMQNhvBACV1TVPxWy9efsBAFqMAKCrp6fXDsDo6BgAjBgBwIhwoLXem4QRAIw5AuDrF2oCMAGYAEwAJgATgAnAwAAYcU95nGLEX8sDrz2NAiDIa91GkXfuqmh73yENvMYz2GZ91mpGfIXeAJSU3qu0s+GZEjIjakTg5B8QroApZcRDdQGAvDaIyclJOxuewcaIxwcEbpGiR0XlFdVSHMGIB7o9AIwFdkj9s+fNdrbbdypgK5bnSGfE3zHieDbGiOcz4j66AIAtPzuZAo2P/1IWmxAStkO0t3+ceV5YdFPZPSvdHkBgUORCO6TxRdMrO1vumXzYst0eQFBw1EIApufaIcg/IgmrewCO3g8o8o2xwpAAbB0tE4AJQKMAvH2DjQ3Azz9M4A53pLDwXSI2LlnM9/7gkO3uCwD1ApTKHKmz84vo6v7q0I54ofJBrWsBMOIHGfEyBCooey0FgLO1rAAY8TVY9NHMHIk66O+OT8UEHYx4hhEAFF8pKLab5NPnbpG4Lx0TVTHiUboF4CgoUVT7qF7y2xnxAl0CwH/c+QBAExO/xaEjWZi0nBHfrLcjUIVveTGqevhYuuMZ8Rw9AYjCMWh+2bqoyb/1D4iY2CShGwAyhER8aG9v36L+gKmpv+L8xUKhGwAyhOPw3tDgoHU5zRNkxC8kJKZJ37AhAcgQitDtpWUhSepsP7/k+o27mlw80uqM+B9nA1iP7OtcaWu1hVuIEe9zRbQXj7M2X9SmhpApRsbYVSFvJuLy4Z8jmgFw6fI1ALC6Mu7PQ3SolZtBjlQjLK4UbgaEy2prYGBQqQx5qJEBKkFpSk2hvI4eA7VSYD6g39DQpMrikWKTAzH1coGMeBzK12rcDCdOnhWa+D0CbgYkSIaGhl22+JbWNm2Uw2wgWBESI1HiCscHHSSMeIrWGiQKtkXvdepOQE/A1sg9UgJGq10iVhwHpYlhOYUyOMJzdIVovVUmHecTHtpyOUuIQeSQN9viDpLb3IpQ8rJtevpfIRWflHxY6h9CLGJxNyltb0iqwF9YzI7AOX9S1yjw67OFijHuBCKOEa+D44LiCjy4svtVMwPHBUkX2GTXtgZ5SYvexIivYsR3yi2yt2yGFd+0bPOwmDJlypQpU6roH4GcyVqRfhvyAAAAAElFTkSuQmCC") 17 0, pointer;
  /* Same Windows bitmap, its white fill remapped to the swatch colour with the
     black outline and the anti-aliased edge left intact (scratchpad/tint_hand.py).
     Files, not base64: two more inline blobs would add ~4KB to every page load
     for something most learners never switch to. The service worker caches
     /images/ at runtime, so they still work offline after first use. */
  --pointer-hand-yellow: url("images/cursor-hand-yellow.png") 17 0, pointer;
  /* Normal size still needs a real file: CSS cannot resize a bitmap cursor.
     Half the 64px one, so the hotspot halves with it. */
  --pointer-hand-yellow-small: url("images/cursor-hand-yellow-small.png") 8 0, pointer;
  --pointer-arrow: auto;
  --pointer-hand: pointer;
  --pointer-text: text;
}

body {
  cursor: var(--pointer-arrow);
}

/* cursor inherits, so the arrow on body would otherwise replace the I-beam the
   typing box gets for free — and Text Cursor mode is entirely about seeing
   exactly which two letters you are clicking between. */
.typing-area {
  cursor: var(--pointer-text);
}

button {
  font: inherit;
}

button:hover:not(:disabled):not([aria-disabled="true"]) {
  filter: brightness(0.97);
}

button:active:not(:disabled):not([aria-disabled="true"]) {
  filter: brightness(0.9);
}

/* Full width on purpose: this padding IS the window-edge inset the header
   buttons sit on, so Home and Help reach the corners of the browser window.
   The 1100px reading measure moved OFF the shell and ONTO each view's content
   blocks below — any NEW direct child of a view runs full-bleed until it is
   given the same three properties. */
.app-shell {
  width: 100%;
  margin: 0 auto;
  padding: 1.5rem max(1rem, env(safe-area-inset-right, 0px)) 2.25rem
    max(1rem, env(safe-area-inset-left, 0px));
}

.intro-view {
  position: relative;
  /* No max-width: the header has to span the whole shell so Home and Help reach
     the window edge. Both views are full width, so the buttons still land on
     the same x on every screen. Global chrome that moves between screens has to
     be re-found each time, and this audience pays for that. */
  width: 100%;
  margin: 0 auto;
  min-height: calc(100vh - 3.75rem);
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
}

/* Scoped to .intro-view so it outranks the later .app-header block, which
   defines its own margin-bottom at the same specificity. */
.intro-view .intro-header {
  /* Buttons only, and NOT part of the centred group below: the lockup carries
     the auto margin now, so Home and Help stay at the top of the screen instead
     of riding down to the middle with the title. */
  display: flex;
  align-items: center;
  justify-content: space-between;
  min-height: auto;
  margin-bottom: 0;
  padding: 0;
}

/* A deliberate override of .hidden, which is display:none. Screen 1 has no Home,
   but removing it lets Help slide to the left of the top bar. visibility:hidden
   holds the space and still takes the button out of the tab order and the
   accessibility tree. */
.intro-view .home-button.hidden {
  display: inline-flex;
  visibility: hidden;
}

.intro-heading {
  text-align: center;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  /* width:100% is MANDATORY beside max-width here: this is a flex item, and
     max-width + auto side margins alone collapse it to its content width. */
  width: 100%;
  max-width: 68.75rem;
  /* auto top here and auto bottom on the card split any leftover room equally
     above and below, so the title and the card read as one vertically centred
     group. Must stay a shorthand: a later `margin: 0 auto` silently wiped the
     two longhands and the group collapsed to the top of the page. */
  margin: auto auto 36px;
}

/* Optically centred, not geometrically. A horizontal lockup has asymmetric mass:
   centring its bounding box puts the TITLE TEXT half the mark plus the gap right
   of page centre, so the card, footer and header buttons all read as shifted
   left. The margin-right pulls the row back by exactly that half, so the title
   text — the thing the eye actually reads as the object — lands on page centre
   with everything else, and the mark hangs into the left margin as an accent.
   Keep this in sync with .intro-brand-icon width and the gap below. */
.intro-title-row {
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: center;
  gap: 28px;
  margin-right: calc(min(72px, 16vw) + 28px);
}

.intro-heading h1 {
  margin: 0;
}

.intro-brand-logo {
  display: block;
  width: min(160px, 50vw);
  height: auto;
}

.intro-brand-icon {
  flex: 0 0 auto;
  display: block;
  width: min(72px, 16vw);
  height: auto;
}

.intro-app-title {
  margin: 0;
  font-size: clamp(1.75rem, 3.75vw, 2.5rem);
  line-height: 1.15;
  /* Centred, so the two lines it wraps to read as one block under the logo
     rather than hanging off its left edge. */
  text-align: center;
  text-wrap: balance;
}

/* Screen 2 hands the hero over to a small lockup at the top of the card, so the
   level choice stops looking like Start and starts looking like the run. The
   heading is only visually hidden, never display:none — the intro's one <h1>
   lives in it — so the card has to take over the auto top margin it was
   carrying or the whole group jumps to the top of the page. */
.intro-view.level-step .intro-heading {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.intro-view.level-step .mode-card {
  margin-top: auto;
}

.intro-card-title {
  display: none;
  margin: 0;
  color: var(--text-muted);
  font-size: 1rem;
  font-weight: 600;
}

.intro-view.level-step .intro-card-title {
  display: block;
}

/* One white panel holds the instruction, Listen and the action they describe,
   so the sentence plainly belongs to the button. It keeps the shared
   .mode-card fill, border, radius and shadow. */
.intro-view .mode-card {
  /* width is explicit because the auto side margins would otherwise stop this
     flex item stretching to it. */
  width: min(37.5rem, 100%);
  margin-left: auto;
  margin-right: auto;
  margin-top: 0;
  margin-bottom: auto;
  /* Top is deliberately larger than the sides: the instruction row sat tight
     against the card edge. The gap is what separates it from Start. */
  padding: 2.75rem 2rem 2.5rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2.5rem;
}

/* The legend is hidden until a mode is completed, so the logo keeps itself on
   the right with margin-left rather than relying on space-between.
   The card's margin-bottom: auto drops this strip to the bottom of any leftover
   room; the 24px top margin is the minimum gap when there is none. */
.intro-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  /* width:100% is MANDATORY beside max-width — flex item, see .intro-heading. */
  width: 100%;
  max-width: 68.75rem;
  /* -24px bottom pulls the strip into most of .app-shell's 36px bottom padding,
     which is shared with the activity view and so must not be reduced itself. */
  margin: 24px auto -24px;
  padding-top: 14px;
  border-top: 1px solid var(--border);
}

.footer-links {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0 0.125rem;
}

.footer-link {
  min-height: 3rem;
  border: 0;
  border-radius: 0.75rem;
  background: transparent;
  padding: 0.5rem;
  color: var(--primary);
  font: inherit;
  font-weight: 700;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  cursor: var(--pointer-hand);
}

.footer-link:hover {
  background: var(--primary-soft);
}

.footer-link:active {
  background: color-mix(in srgb, var(--primary) 16%, var(--card-bg));
}

.footer-link-separator {
  color: var(--text-muted);
}

.linc-suite-logo-mark {
  width: min(120px, 40vw);
  margin-left: auto;
}

.linc-suite-logo {
  display: block;
  width: 100%;
  height: auto;
}

/* The landing page's only spoken cue says what to do now; Help explains how the
   page works, so the two do not overlap. */
.intro-instruction-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 12px;
  /* Part of the card now, so the card's own gap and padding place it. */
  margin: 0;
}

.intro-instruction-icon {
  flex: 0 0 auto;
  width: auto;
  height: 34px;
}

/* The row gap pulls the icon against the text; the audio button's extra margin
   keeps it clearly separate from the sentence. */
.intro-instruction-row .audio-button {
  margin-left: 0.5rem;
}

.intro-instruction {
  margin: 0;
  font-size: 1.375rem;
  line-height: 1.25;
}

/* Free Practice takes the empty 4th cell of the Fix row. No number and no group
   tint — a white surface with the action-blue border marks it as the open-ended
   option rather than a twelfth numbered step. */
.free-practice-tile {
  grid-template-columns: minmax(0, 1fr);
  border-color: var(--primary);
  background: var(--card-bg);
}

.free-practice-tile-description {
  grid-column: 1;
  grid-row: 2;
  /* start, not end: it belongs to the title above it, not to the tile's floor
     the way the other tiles' status pills do. */
  align-self: start;
  font-size: 1rem;
  line-height: 1.25;
}

/* The level choice is two tiles, not two bars — white with a blue outline in the
   rounded-rectangle shape LINCtype used for its activity tiles, set apart by a
   wide gap so the two levels read as a real choice rather than a button row.
   They wrap and stack on a phone. */
.intro-levels {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 2.5rem;
}

.level-button {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  width: 15rem;
  min-height: 8.75rem;
  border-radius: 14px;
  border: 2px solid var(--primary);
  background: var(--level-tile-bg);
  color: var(--primary);
  padding: 1rem;
  font-size: 2.5rem;
  font-weight: 700;
  line-height: 1.2;
  cursor: var(--pointer-hand);
}

/* One dot for CLB 1, two for CLB 2 — a language-free count that repeats the
   number for a learner who cannot read it yet. Decorative, so a pseudo-element
   keeps it out of the accessible name. */
.level-button::after {
  content: "●";
  font-size: 1.25rem;
  letter-spacing: 0.4rem;
  /* letter-spacing pads the right of the last dot too — pull the row back into
     the centre. */
  margin-right: -0.4rem;
}

.level-button[data-level="2"]::after {
  content: "●●";
}

.level-button[data-level="2"] {
  background: var(--level-tile-bg-2);
}

@media (hover: hover) and (pointer: fine) {
  .level-button:hover:not(:disabled) {
    background: var(--level-tile-bg-strong);
  }

  .level-button[data-level="2"]:hover:not(:disabled) {
    background: var(--level-tile-bg-2-strong);
  }
}

.level-button:active:not(:disabled) {
  background: var(--level-tile-bg-strong);
  box-shadow: inset 0 0 0 2px var(--primary);
}

.level-button[data-level="2"]:active:not(:disabled) {
  background: var(--level-tile-bg-2-strong);
}

/* Home, Settings, Help and the logo all sit in normal flow and scroll with the
   card. They used to be position: fixed, which let the card slide underneath
   them; nothing is pinned now, so there is no band to reserve and nothing to
   scroll under. */
.activity-view {
  /* No max-width — see .intro-view. The 1100px cap lives on .settings-drawer
     and .task-card so the header can reach the window edge. */
  width: 100%;
  margin: 0 auto;
  min-height: calc(100vh - 3.75rem);
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
}

.app-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 12px;
  margin-bottom: 18px;
}

.activity-header {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
}

/* Keep Settings in the page flow. A sticky drawer covered the task card while
   the learner scrolled, making the content appear to disappear behind it. */
.activity-view.settings-open .settings-drawer {
  position: relative;
  top: auto;
}

.activity-brand {
  display: flex;
  align-items: center;
}

.header-actions {
  display: flex;
  align-items: center;
  gap: 16px;
  margin-left: auto;
}

.settings-control {
  position: relative;
  display: inline-flex;
}

/* Anchored to the control, not to a coordinate: Settings is fixed top-right on
   desktop and shrinks to an icon under 640px, so the arrow has to travel with
   it. Drawn in the hint colour, not amber: it appears on Show hint, so it means
   "look here", and amber is reserved for "not right yet". */
.settings-hint-arrow {
  position: absolute;
  top: calc(100% + 8px);
  left: 50%;
  width: 28px;
  height: 64px;
  transform: translateX(-50%);
  pointer-events: none;
  z-index: 5;
}

.settings-hint-arrow svg {
  display: block;
  width: 100%;
  height: 100%;
  fill: none;
  stroke: var(--hint-border);
  stroke-width: 5;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* The Text Size and Font arrows sit inside the drawer, not on the fixed header
   button, so they only have the gap before the task card below to grow into.
   That gap is too short on mobile once the groups stack — shrink to fit. */
@media (max-width: 640px) {
  .text-size-controls .settings-hint-arrow,
  .font-control .settings-hint-arrow {
    top: calc(100% + 4px);
    width: 20px;
    height: 44px;
  }
}

.activity-view .activity-header {
  margin-bottom: 1.125rem;
  padding-top: 0;
}

.settings-unavailable-tip {
  position: absolute;
  z-index: 10;
  top: 100%;
  right: 0;
  width: max-content;
  max-width: min(280px, calc(100vw - 32px));
  padding: 8px 10px;
  border-radius: 6px;
  background: var(--text-main);
  color: var(--surface-tint);
  font-size: 0.875rem;
  font-weight: 600;
  line-height: 1.35;
  opacity: 0;
  visibility: hidden;
  pointer-events: auto;
  transform: translateY(-4px);
  transition: opacity 160ms ease-out, transform 160ms ease-out, visibility 160ms;
}

.settings-control.is-unavailable:not(.tooltip-suppressed):hover .settings-unavailable-tip,
.settings-control.is-unavailable:not(.tooltip-suppressed):focus-within .settings-unavailable-tip {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
}

.tooltip-suppressed .settings-unavailable-tip,
.tooltip-suppressed .tool-label {
  opacity: 0 !important;
  visibility: hidden !important;
  transition: none !important;
}

h1,
h2,
p {
  margin-top: 0;
}

h1 {
  margin-bottom: 0;
  font-size: clamp(2rem, 4vw, 3.25rem);
  line-height: 1.05;
}

h2 {
  margin-bottom: 14px;
  font-size: 1.25rem;
}

/* The instruction is the one thing the learner must read, so it outranks every
   other label on the card. */
#taskTitle {
  font-size: 2.5rem;
  line-height: 1.3;
  margin: 0;
}

/* Audio sits beside the instruction or target and drops below it when the two no
   longer fit. Its middle aligns with the middle of the instruction. */
.task-heading-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 14px 20px;
  margin: 42px 0 24px;
}

.task-heading-row #taskTitle {
  flex: 0 1 auto;
}

/* Words and Sentences empty this row: the title is hidden and the audio button
   sits beside the sample instead. Keep a gap above the word, just not one
   sized for a line of text that is not there. */
.task-heading-row.is-collapsed {
  margin: 0 0 16px;
}

.task-heading-row .audio-button {
  flex: 0 0 auto;
}

/* The task title is already bold, so the named word needs colour + an underline. */
#taskTitle strong {
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-thickness: 2px;
}

/* The arrow sits over the named letter: right for after, left for before. Its
   wider box stays centred on the letter without changing the spoken text. */
#taskTitle strong.cursor-target,
#titleDemoInstruction strong.cursor-target {
  position: relative;
  display: inline-block;
  padding-top: 0.58em;
  line-height: 1;
}

.cursor-target-arrow {
  position: absolute;
  top: 0;
  left: 50%;
  width: 1.2em;
  height: 0.58em;
  overflow: visible;
  fill: none;
  stroke: var(--accent);
  stroke-width: 3;
  stroke-linecap: round;
  stroke-linejoin: round;
  transform: translateX(-50%);
  pointer-events: none;
}

.cursor-target-before .cursor-target-arrow {
  transform: translateX(-50%) scaleX(-1);
}

/* A key named in an instruction is drawn as the key itself. Everything is in em
   so keycaps follow the text size and 200% zoom. */
.keycap {
  display: inline-block;
  padding: 0.1em 0.5em;
  border: 2px solid var(--field-border);
  border-bottom-width: 5px;
  border-radius: 8px;
  background: var(--keyboard-key);
  color: var(--text-main);
  font-family: inherit;
  font-size: 0.9em;
  font-weight: 700;
  line-height: 1.3;
  white-space: nowrap;
}

/* The real space bar has nothing printed on it: a wide blank key. Its own
   margin, not a template space, so a blank key never leaves stray whitespace
   sitting in front of the sentence's closing period. */
.keycap-space {
  width: 5em;
  margin-left: 0.3em;
}

.keycap-space::before {
  content: "\00a0";
}

/* A character to type is paper, not a key: white, flat, and evenly bordered so
   nothing about it says "press". Sized like the keycap so lines do not reflow. */
.type-chip {
  display: inline-block;
  min-width: 1.2em;
  padding: 0.1em 0.5em;
  border: 2px solid var(--chip-border);
  border-radius: 8px;
  background: var(--card-bg);
  color: var(--text-main);
  font-family: inherit;
  font-size: 0.9em;
  font-weight: 700;
  line-height: 1.3;
  text-align: center;
  white-space: nowrap;
}

/* When the chip is the entire instruction it stops being an inline mark and
   becomes the thing on screen. The wider minimum keeps a lone "." from reading
   as an empty box. */
.task-title-solo-chip .type-chip {
  min-width: 1.5em;
  padding: 0.08em 0.34em;
  font-size: 1.7em;
}

.key-combination-hint {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  min-height: 4rem;
  /* The negative top tucks it under the instruction in the modes whose sample
     box is hidden — there the sample row collapses to nothing above it. */
  margin: -0.25rem 0 1.5rem;
  padding-top: 0.75rem;
}

/* Capital Letters is the only mode that shows a sample, so it is the only one
   where the keys can sit beside the word instead of under it. The sample box is
   taller than a keycap, so the row does not grow — revealing the hint moves
   nothing below it, which is the whole point. */
.sample-row:has(.sample-box:not(.hidden)) .key-combination-hint {
  min-height: 0;
  margin: 0;
  padding-top: 0;
}

/* The modes with no sample leave the hint alone in the row, where its bottom
   margin no longer collapses with the divider's the way it did as a sibling.
   Drop it and let the divider's own margin space it — 2px shy of where it sat. */
.sample-row:not(:has(.sample-box:not(.hidden))) .key-combination-hint {
  margin-bottom: 0;
}

.key-hint-keycap {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 68px;
  height: 64px;
  border: 2px solid var(--field-border);
  border-bottom-width: 6px;
  border-radius: 10px;
  background: var(--keyboard-key);
  color: var(--text-main);
  padding: 6px 14px;
  font-size: 1.5rem;
  font-weight: 700;
  line-height: 1;
}

.key-hint-keycap.is-wide {
  min-width: 112px;
}

.key-hint-keycap.is-space {
  min-width: 168px;
}

.key-hint-keycap.has-shifted-label {
  display: grid;
  grid-template-rows: 1fr 1fr;
  justify-items: center;
  padding-block: 5px;
}

.key-hint-shifted-label,
.key-hint-base-label {
  line-height: 1;
}

.key-hint-plus {
  color: var(--text-muted);
  font-size: 2rem;
  font-weight: 700;
  line-height: 1;
}

.mode-card,
.task-card {
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow);
  padding: 1.125rem;
  /* The 1100px reading measure lives here now, not on .activity-view, so the
     header can reach the window edge. width:100% is MANDATORY beside max-width:
     .task-card is a flex item, and max-width + margin-inline:auto alone collapse
     it to its content width. */
  width: 100%;
  max-width: 68.75rem;
  margin-inline: auto;
  margin-bottom: 1rem;
}

.task-divider {
  border: none;
  height: 1px;
  background: var(--border);
  margin: 1.375rem 0 1rem;
}

/* The card's own 18px padding left only 19px above the mode name while the space
   under Hear message came to 48px (36px here + .action-row's 12px margin), so
   the block sat high and looked top-cropped. Both gaps are now 36px:
   18px card padding + 18px here on top, 24px + 12px action-row margin below. */
.completion-screen {
  text-align: center;
  /* No padding of its own on top any more: the card's own 18px puts this
     screen's topline exactly where the task screen's sits, and the height it
     gives back is what pays for the gap under that line. */
  padding-bottom: 1.125rem;
}

.completion-mode-label {
  margin: 0 0 8px;
  color: var(--accent);
  font-size: 1.25rem;
  font-weight: 700;
  line-height: 1.3;
}

.completion-headline {
  font-size: 2.125rem;
  font-weight: 700;
  color: var(--text-main);
  line-height: 1.2;
  margin-bottom: 8px;
}

.completion-subtext {
  font-size: 1.25rem;
  color: var(--text-muted);
  margin-bottom: 24px;
}

.completion-icon {
  width: 44px;
  height: 44px;
  margin: 0 auto 14px;
}

.completion-icon svg {
  width: 100%;
  height: 100%;
}

.completion-check-icon circle {
  fill: var(--success-border);
}

.completion-check-icon path {
  fill: none;
  stroke: var(--card-bg);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.completion-screen.is-success .completion-subtext {
  color: var(--success-text);
  font-weight: 700;
}

/* The gap below the audio button is the only separator between the message and
   the two choices. It replaces a <hr> that added a seventh element to a screen
   already carrying too many. */
.completion-screen .audio-button {
  margin: 0 auto 1.5rem;
}

/* The teacher's seven-row summary. Left-aligned because it is read, not
   scanned — the rest of the completion screen is centred. Capped at the
   dialog measure: across the full 1100px card the mark sat ~800px from the
   instruction it belonged to, which is too far to pair a row with its result. */
.results-list {
  list-style: none;
  max-width: 34rem;
  /* The headline is the instruction and the list is what it points at, so they
     need a beat between them. The hidden subtext used to supply this gap; the
     Listen button beside the headline now supplies part of it too, which is
     what pays for the trim that keeps all seven rows on one screen. */
  margin: 1.5rem auto 1.5rem;
  padding: 0;
  text-align: left;
}

.result-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 1rem;
  padding: 0.5rem 0;
  border-top: 1px solid var(--border);
}

.result-instruction {
  /* Below this width the mark or the typed answer wraps to its own line rather
     than squeezing the instruction into a one-word column. */
  flex: 1 1 8rem;
  font-size: 1.25rem;
  color: var(--text-main);
}

.result-answer {
  font-size: 1.25rem;
  font-weight: 700;
  color: var(--text-main);
  /* Learner text, so it can be anything — let it wrap rather than push the row
     wider than the card. */
  overflow-wrap: anywhere;
}

.result-mark {
  flex: none;
  font-size: 1.75rem;
  font-weight: 700;
  line-height: 1;
}

.result-mark.is-correct {
  color: var(--success-border);
}

.result-mark.is-wrong {
  color: var(--warning-border);
}

.editor-toolbar {
  padding: 18px 0 12px;
  margin-top: 32px;
  background: var(--card-bg);
  border-top: 1px solid var(--border);
}

.settings-drawer {
  position: relative;
  background: var(--mode-tile-tint);
  border: 1px solid var(--sample-border);
  border-radius: var(--radius-lg);
  padding: 0.375rem 1.125rem 1.125rem;
  /* Deliberately the CARD's width, not the header's: the controls are packed
     left with a fixed gap and do not spread, so a wider box is just empty
     space. width:100% is MANDATORY beside max-width — flex item, see
     .task-card. */
  width: 100%;
  max-width: 68.75rem;
  margin-inline: auto;
  margin-bottom: 1rem;
}

.settings-drawer[hidden] {
  display: none;
}

.settings-drawer .editor-toolbar {
  margin-top: 0;
  padding-top: 0;
  border-top: none;
  background: transparent;
}

.settings-close-button {
  position: absolute;
  top: 14px;
  right: 18px;
  z-index: 1;
}

.action-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
}

.primary-button,
.secondary-button,
.audio-button {
  min-height: 3rem;
  border-radius: var(--radius-pill);
  border: 2px solid var(--primary);
  padding: 0.625rem 1.125rem;
  cursor: var(--pointer-hand);
  font-weight: 700;
}

.mode-button {
  cursor: var(--pointer-hand);
}

.secondary-button,
.audio-button,
.space-toggle-button {
  background: var(--card-bg);
  color: var(--primary);
}

.audio-button {
  min-height: 3.75rem;
  padding: 0.875rem 1.625rem;
  font-size: 1.25rem;
}

.primary-button {
  background: var(--primary);
  color: var(--card-bg);
}

#guidedActions #checkButton:not(.hidden),
#guidedActions #skipButton:not(.hidden),
#guidedActions #hintButton:not(.hidden),
#guidedActions #nextTaskButton:not(.hidden),
.intro-actions .primary-button,
.completion-actions .primary-button:not(.hidden),
.completion-actions .secondary-button:not(.hidden) {
  flex: 1;
  min-height: 5.25rem;
  padding: 1.375rem;
  border-radius: 0.75rem;
  font-size: 1.75rem;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.875rem;
  box-shadow: var(--shadow-action-primary);
}

/* The landing card is 600px wide and a Start button spanning all of it reads as
   a bar, not a button. The row itself stays full width so the button has
   something for its max-width to resolve against on a narrow card. */
.intro-actions {
  width: 100%;
  justify-content: center;
}

.intro-actions .primary-button {
  flex: 0 0 auto;
  width: 20rem;
  max-width: 100%;
}

#guidedActions #skipButton:not(.hidden),
#guidedActions #hintButton:not(.hidden),
.completion-actions .secondary-button {
  box-shadow: var(--shadow-action-secondary);
}

/* Completion actions stack instead of sharing a row. Side by side at equal
   width they read as two equal choices, which contradicts the one-primary rule;
   stacked, "More practice" is plainly the main path and the alternative sits
   below it, quieter and shorter. */
.completion-actions {
  flex-direction: column;
  /* The card runs the full 1100px activity width, so an unconstrained stack
     produces two very wide, very short bars. 520px matches the dialog width
     already used elsewhere and keeps both actions in one glance. */
  max-width: 520px;
  margin-inline: auto;
}

.completion-actions .primary-button,
.completion-actions .secondary-button {
  flex: none;
  width: 100%;
}

.completion-actions .secondary-button {
  min-height: 4.25rem;
  font-size: 1.5rem;
}

#guidedActions #checkButton:not(.hidden) {
  flex: 0.95;
}

#guidedActions #hintButton:not(.hidden) {
  flex: 1.05;
}

#guidedActions #hintButton:disabled {
  box-shadow: none;
}

.hint-icon-hide {
  display: none;
}

#hintButton[aria-pressed="true"] .hint-icon-show {
  display: none;
}

#hintButton[aria-pressed="true"] .hint-icon-hide {
  display: block;
}

.mode-button:focus-visible,
.level-button:focus-visible,
.primary-button:focus-visible,
.secondary-button:focus-visible,
.audio-button:focus-visible,
.home-button:focus-visible,
.help-button:focus-visible,
.settings-button:focus-visible,
.back-button:focus-visible,
.footer-link:focus-visible,
.space-toggle-button:focus-visible,
.tool-button:focus-visible {
  outline: 4px solid var(--focus-blue);
  outline-offset: 3px;
  box-shadow: 0 0 0 2px var(--card-bg);
}

.home-button,
.help-button,
.settings-button {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  min-height: 3rem;
  border: 2px solid var(--primary);
  border-radius: 0.75rem;
  background: var(--surface-tint);
  color: var(--primary);
  padding: 0.625rem 1rem;
  font-size: 1rem;
  font-weight: 700;
  cursor: var(--pointer-hand);
  box-shadow: var(--shadow-control);
}

.home-button:hover:not(:disabled),
.help-button:hover:not(:disabled),
.settings-button:hover:not(:disabled):not([aria-disabled="true"]) {
  background: var(--primary-soft);
}

.settings-button.is-active {
  background: var(--primary);
  color: var(--card-bg);
}

.settings-button.is-active:hover:not(:disabled) {
  background: var(--primary);
}

.settings-button[aria-disabled="true"] {
  border-color: var(--border);
  background: var(--keyboard-key);
  color: var(--text-muted);
  cursor: not-allowed;
  box-shadow: none;
}

/* The gear and the word "Settings" inline in a sentence. Deliberately carries
   none of .settings-button's border, fill or shadow: it names the control, it
   is not a copy of it, and a copy is what learners were tapping. */
/* Stays plain inline so "Settings" sits on the sentence's own baseline. An
   inline-flex wrapper here dropped the word 5px below the words either side of
   it. Only the gear is shifted, and only to centre it on the text. */
/* No side margins: the sentence already supplies its own space before this, and
   a right margin put a visible gap between "Settings" and the period. */
.inline-settings-name {
  white-space: nowrap;
  /* The same blue as the control it names, so the sentence and the button in the
     corner read as the same thing. Action blue on a few inline words is what the
     Restrained Blue Rule allows; it is naming an action, not tinting a surface. */
  color: var(--primary);
  font-weight: 700;
}

.inline-settings-name .settings-icon {
  margin-right: 0.2em;
  vertical-align: middle;
}

.home-icon,
.help-icon,
.settings-icon,
.check-icon,
.hint-icon {
  width: 1.45rem;
  height: 1.45rem;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  flex-shrink: 0;
}

.task-topline {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  flex-wrap: wrap;
  margin-bottom: 1.125rem;
}

.back-button {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  min-height: 3rem;
  border: none;
  background: none;
  color: var(--text-muted);
  font-size: 1rem;
  font-weight: 600;
  padding: 0.375rem 0.25rem;
  cursor: var(--pointer-hand);
}

.back-button:hover:not(:disabled) {
  color: var(--text-main);
  text-decoration: underline;
}

.back-arrow {
  font-size: 1.2rem;
  line-height: 1;
}

.task-status {
  margin: 0 0 0 auto;
  color: var(--text-muted);
  font-size: 1rem;
  font-weight: 600;
}

.activity-mode-label {
  margin: 0;
}

/* The listening mark from the landing page, shrunk to sit beside the activity
   name wherever that name appears. Decorative: the words are right next to it. */
.title-brand-icon {
  width: 32px;
  height: auto;
  vertical-align: middle;
  margin-right: 8px;
}

/* The card's own topline: the activity on the left, the kind of task on the
   right. Two stacked centred lines cost a row of height the results list needs,
   and this row mirrors the task screen the teacher just watched. */
.completion-kicker {
  display: flex;
  align-items: center;
  /* space-between until 2026-08-31, when it held the label and the
     "Skill-Using Task" subtitle at opposite ends. With the subtitle gone it is
     one child, and space-between left it pinned to the card's left edge while
     the headline, list and Finish all sit on centre. */
  justify-content: center;
  flex-wrap: wrap;
  gap: 0.5rem 1.5rem;
  /* The topline is a label, not part of the message. It needs air under it or
     the headline reads as a second line of the same block. Widened from 1.5rem
     2026-08-31 — once the topline centred, it sat directly over "Show your
     teacher." and the two read as one stacked title. */
  margin-bottom: 2.75rem;
  /* Air above the topline so it is not jammed against the card's top edge. */
  margin-top: 1.5rem;
}

.completion-kicker .completion-mode-label {
  margin: 0;
}

/* "Show your teacher." is an instruction, so it gets the same Listen treatment
   every instruction has. Beside the headline, not under it: the button belongs
   to that sentence, and a row of its own would cost the height the topline
   just saved. */
.completion-headline-row {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 0.75rem 1.25rem;
  margin-bottom: 8px;
}

.completion-headline-row .completion-headline {
  margin: 0;
}

/* The button was a centred block under the message, and those auto side margins
   would eat this row's free space and split the pair to opposite ends. */
.completion-headline-row .audio-button {
  margin: 0;
}

.task-status .task-count::before {
  content: "·";
  margin: 0 8px;
}

.audio-button.active {
  background: var(--primary);
  color: var(--card-bg);
}

.audio-button[aria-busy="true"] {
  background: var(--primary-soft);
  color: var(--text-main);
  cursor: progress;
}

.task-count {
  margin: 0;
  color: var(--text-muted);
  font-weight: 700;
}

.task-progress-track {
  display: flex;
  gap: 0.25rem;
  height: 0.375rem;
  margin-bottom: 1.375rem;
}

.task-progress-segment {
  flex: 1;
  height: 100%;
  border-radius: var(--radius-pill);
  background: var(--border);
  transition: background-color 200ms ease-out;
}

.task-progress-segment.is-complete {
  background: var(--accent);
}

.sample-row {
  display: flex;
  align-items: center;
  gap: 1rem;
  margin-top: 0.75rem;
}

#guidedPanel .sample-row {
  flex-wrap: wrap;
}

#guidedPanel .sample-box {
  flex-shrink: 0;
}

.sample-row .audio-button {
  flex: 0 0 auto;
}

.sample-box {
  width: fit-content;
  max-width: 100%;
  background: var(--primary-soft);
  border: 2px solid var(--sample-border);
  border-radius: var(--radius-md);
  padding: 1rem;
}

.sample-box.has-image,
.sample-box.has-demo {
  display: flex;
  align-items: center;
  gap: 1rem;
}

/* Let the animated demos drop below the word instead of overflowing on narrow screens. */
.sample-box.has-demo {
  flex-wrap: wrap;
}

.typing-demo {
  display: inline-flex;
  align-items: center;
  min-width: 150px;
  padding: 12px 18px;
  border: 2px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--card-bg);
  font-family: ui-monospace, Menlo, Consolas, "Courier New", monospace;
  font-size: 2.5rem;
  font-weight: 700;
}

.typing-demo.demo-muted #demoText {
  color: var(--demo-text);
}

.demo-cursor {
  margin-left: 2px;
  animation: demo-blink 1s step-end infinite;
}

/* Text-cursor task: a green arrow points down at the blinking cursor. */
.cursor-arrow {
  display: none;
  width: 0.85em;
  height: 0.85em;
  color: var(--cursor-arrow);
  transform: translate(-0.43em, -0.6em);
}

.typing-demo.has-arrow .cursor-arrow {
  display: inline-block;
}

.cursor-arrow svg {
  display: block;
  width: 100%;
  height: 100%;
}

@keyframes demo-blink {
  50% {
    opacity: 0;
  }
}

/* "type" task: a looping video of hands typing. */
.video-demo {
  flex-shrink: 0;
  height: 100px;
  width: auto;
  max-width: 100%;
  border-radius: var(--radius-md);
}

/* "click" task: the mouse photo dips like a press, and a ring pulses from the button. */
.click-demo {
  position: relative;
  display: inline-flex;
  flex-shrink: 0;
  line-height: 0;
}

.click-demo-img {
  height: 100px;
  width: auto;
  border-radius: var(--radius-md);
  animation: click-press 1.6s ease-in-out infinite;
}

.click-demo-ring {
  position: absolute;
  top: 22%;
  left: 36%;
  width: 13px;
  height: 13px;
  margin: -6.5px 0 0 -6.5px;
  border: 3px solid var(--card-bg);
  border-radius: 50%;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25);
  opacity: 0;
  animation: click-ring 1.6s ease-out infinite;
}

@keyframes click-press {
  0%, 100% {
    transform: translateY(0);
  }
  10% {
    transform: translateY(3px);
  }
  20% {
    transform: translateY(0);
  }
}

@keyframes click-ring {
  0% {
    transform: scale(0.4);
    opacity: 0;
  }
  10% {
    opacity: 0.9;
  }
  55% {
    transform: scale(2.6);
    opacity: 0;
  }
  100% {
    opacity: 0;
  }
}

/* Keyboard-key demos: the animated symbols.
   Pure CSS like the click demo. One #keyDemo holds all scenes; [data-mode] shows one.
   SPEED: --demo-dur below. SIZE: the clamp() font-size (keys + text scale with it).
   COLOURS: --border (keys), --primary (the ? and ! marks), --demo-ink (text/cursor). */
.key-demo {
  --demo-dur: 4.5s;
  display: inline-flex;
  flex-shrink: 0;
  font-family: ui-monospace, Menlo, Consolas, "Courier New", monospace;
  font-weight: 700;
  font-size: clamp(1.1rem, 3.4vw, 1.75rem);
  color: var(--demo-ink);
}

.key-demo .key-scene {
  display: none;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.6em;
}

.key-demo[data-mode="underscore"] .key-scene[data-scene="underscore"],
.key-demo[data-mode="dash"] .key-scene[data-scene="dash"],
.key-demo[data-mode="question"] .key-scene[data-scene="question"],
.key-demo[data-mode="exclaim"] .key-scene[data-scene="exclaim"],
.key-demo[data-mode="period"] .key-scene[data-scene="period"],
.key-demo[data-mode="press"] .key-scene[data-scene="press"],
.key-demo[data-mode="hold"] .key-scene[data-scene="hold"] {
  display: inline-flex;
}

/* press / hold. One keycap, two timings — that difference is the whole lesson,
   so nothing else about the two scenes changes. The 3D edge is a box-shadow
   rather than a bottom border because the key is animated: shadow and transform
   are paint and composite, while animating border-width would be layout. */
.ks-key {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 68px;
  height: 60px;
  border: 2px solid var(--field-border);
  border-radius: 10px;
  background: var(--keyboard-key);
  box-shadow: 0 5px 0 var(--field-border);
  color: var(--text-main);
  font-size: 1.5rem;
  font-weight: 700;
  line-height: 1;
}

/* The contact point: a fingertip resting on the lower part of the key. Blinks on
   for press, rests for hold — a key sitting down on its own reads as stuck, and
   the finger is what makes it read as held. Sits right of centre so it never
   covers the letter on the keycap. Outlined rather than filled with a skin
   tone, matching the pointer in the title demo: this is a diagram of a hand,
   not a picture of one, so it does not have to belong to anybody. */
.ks-key-touch {
  position: absolute;
  right: 6px;
  bottom: -2px;
  width: 22px;
  height: 27px;
  opacity: 0;
}

.ks-key-touch svg {
  display: block;
  width: 100%;
  height: 100%;
  fill: var(--card-bg);
  stroke: var(--text-main);
  stroke-width: 1.9;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.ks-key-press {
  animation: ks-key-press var(--demo-dur) ease-in-out infinite;
}

.ks-key-press .ks-key-touch {
  animation: ks-touch-press var(--demo-dur) ease-in-out infinite;
}

.ks-key-hold {
  animation: ks-key-hold var(--demo-dur) ease-in-out infinite;
}

.ks-key-hold .ks-key-touch {
  animation: ks-touch-hold var(--demo-dur) ease-in-out infinite;
}

/* Down and straight back up: contact is brief. */
@keyframes ks-key-press {
  0%, 22% { transform: none; box-shadow: 0 5px 0 var(--field-border); }
  32%, 42% { transform: translateY(4px); box-shadow: 0 1px 0 var(--field-border); }
  52%, 100% { transform: none; box-shadow: 0 5px 0 var(--field-border); }
}

@keyframes ks-touch-press {
  0%, 26% { opacity: 0; }
  32%, 42% { opacity: 1; }
  48%, 100% { opacity: 0; }
}

/* Down, and stays down for most of the loop: that dwell is what "hold" means. */
@keyframes ks-key-hold {
  0%, 14% { transform: none; box-shadow: 0 5px 0 var(--field-border); }
  24%, 78% { transform: translateY(4px); box-shadow: 0 1px 0 var(--field-border); }
  88%, 100% { transform: none; box-shadow: 0 5px 0 var(--field-border); }
}

@keyframes ks-touch-hold {
  0%, 18% { opacity: 0; }
  24%, 78% { opacity: 1; }
  84%, 100% { opacity: 0; }
}

/* Symbols appear with a calm scale. (Static shows them.) */
.ks-mark {
  display: inline-block;
  color: var(--primary);
  transform-origin: center bottom;
}

/* Symbol demos show the character alone, larger. */
.ks-mark-solo {
  font-size: 1.9em;
}

/* Symbol demos use the same white field as the special-characters demo. */
.ks-symbol-field {
  justify-content: center;
}

.ks-symbol-field .ks-mark-solo {
  font-size: 1em;
}

.key-demo[data-mode="underscore"] .ks-mark,
.key-demo[data-mode="dash"] .ks-mark,
.key-demo[data-mode="question"] .ks-mark,
.key-demo[data-mode="exclaim"] .ks-mark,
.key-demo[data-mode="period"] .ks-mark {
  animation: ks-mark var(--demo-dur) ease-in-out infinite;
}

@keyframes ks-mark {
  0%, 30% { opacity: 0; transform: scale(1); }
  40% { opacity: 1; transform: scale(1.25); }
  50% { transform: scale(1); }
  82% { opacity: 1; }
  90%, 100% { opacity: 0; transform: scale(1); }
}

.sample-text {
  margin-bottom: 0;
  font-weight: 700;
}

/* Capital Letters: the missing capital is a blank line the answer will fill.
   No height, so the border sits exactly on the baseline of the rest of the name. */
.sample-blank {
  display: inline-block;
  width: 0.7em;
  margin-right: 0.06em;
  border-bottom: 4px solid currentColor;
}

/* Answered: the capital drops into the blank, underline still under it. */
.sample-blank.is-filled {
  width: auto;
  text-align: center;
}

.sample-image {
  flex-shrink: 0;
  height: 84px;
  width: auto;
  max-width: 100%;
  object-fit: contain;
}

/* Cursor vocabulary: the pointer glides in, then rests long enough to study. */
.sample-image.is-cursor-demo {
  animation: cursor-icon-glide 4.5s cubic-bezier(0.16, 1, 0.3, 1) infinite;
}

.sample-image.is-spacebar-image {
  width: min(180px, 32vw);
  height: auto;
}

/* Name photos are pre-cropped to a circle on a white square background;
   mask the square away so only the circle shows. */
.sample-image.is-name-photo {
  width: 84px;
  border-radius: 50%;
  object-fit: cover;
}

@media (max-width: 640px) {
  .sample-image.is-name-photo {
    width: 56px;
  }

  /* No room beside the word on a phone, so the keys go back under it. Full
     basis forces the wrap at the breakpoint rather than leaving it to whatever
     the name happens to measure. */
  .sample-row:has(.sample-box:not(.hidden)) .key-combination-hint {
    flex-basis: 100%;
    margin-top: 0.75rem;
  }
}

@keyframes cursor-icon-glide {
  0%, 24% {
    opacity: 0;
    transform: translate3d(24px, 0, 0);
  }

  38%, 82% {
    opacity: 1;
    transform: translate3d(0, 0, 0);
  }

  92%, 100% {
    opacity: 0;
    transform: translate3d(0, 0, 0);
  }
}

@media (max-width: 640px) {
  .sample-image {
    height: 56px;
  }

  .sample-image.is-keyboard-image {
    width: min(120px, 34vw);
    height: auto;
  }

  .typing-demo {
    min-width: 120px;
    padding: 10px 14px;
    font-size: 1.75rem;
  }

  .click-demo-img,
  .video-demo {
    height: 76px;
  }
}

.feedback-message {
  min-height: 2rem;
  margin-top: 0.75rem;
  font-weight: 700;
}

.feedback-message.good:not(.hidden) {
  display: flex;
  align-items: center;
  gap: 1rem;
  background: var(--success-bg);
  color: var(--success-text);
  border: 3px solid var(--success-border);
  border-radius: var(--radius-md);
  padding: 1rem 1.25rem;
  font-size: 1.625rem;
  line-height: 1.3;
}

/* Pressing Check again without changing anything repeats the same message, so
   nothing on screen moves and the button reads as broken. This pulse is the
   only acknowledgement that the press landed. */
.feedback-message.is-repeat {
  animation: feedback-repeat 0.45s ease-out;
}

@keyframes feedback-repeat {
  0%, 100% { transform: none; box-shadow: 0 0 0 0 rgba(0, 0, 0, 0); }
  30% { transform: scale(1.02); box-shadow: 0 0 0 6px var(--warning-border); }
}

/* Same acknowledgement without the movement. */
@media (prefers-reduced-motion: reduce) {
  @keyframes feedback-repeat {
    0%, 100% { box-shadow: 0 0 0 0 rgba(0, 0, 0, 0); }
    30% { box-shadow: 0 0 0 6px var(--warning-border); }
  }
}

/* A listening run grades once, so the block that says the answer was wrong also
   says what the right answer was — spoken instructions mean the learner has
   never seen it. Second line, smaller and lighter than the verdict above it:
   information, not more alarm. */
.feedback-stack {
  display: flex;
  flex-direction: column;
  gap: 0.375rem;
}

.feedback-answer {
  font-size: 1.375rem;
  font-weight: 600;
  line-height: 1.5;
}

/* The named letter in a Text Cursor answer, marked the way the instruction
   itself marks it. */
.feedback-answer strong {
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-thickness: 2px;
}

.feedback-icon {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border-radius: 999px;
  color: var(--card-bg);
  font-size: 1.375rem;
  font-weight: 900;
  line-height: 1;
}

.feedback-message.good .feedback-icon {
  background: var(--success-border);
}

.feedback-message.check .feedback-icon {
  background: var(--warning-border);
}

.feedback-message.check:not(.hidden) {
  display: flex;
  align-items: center;
  gap: 1rem;
  background: var(--warning-bg);
  color: var(--warning-text);
  border: 3px solid var(--warning-border);
  border-radius: var(--radius-md);
  padding: 1rem 1.25rem;
  font-size: 1.625rem;
  line-height: 1.4;
}

.feedback-message.skip .feedback-icon {
  background: var(--primary);
}

.feedback-message.skip:not(.hidden) {
  display: flex;
  align-items: center;
  gap: 1rem;
  background: var(--primary-soft);
  color: var(--text-main);
  border: 3px solid var(--sample-border);
  border-radius: var(--radius-md);
  padding: 1rem 1.25rem;
  font-size: 1.625rem;
  line-height: 1.4;
}

.sample-space {
  display: inline-block;
  width: 0.33em;
  height: 0.8em;
  background: var(--space-cue);
  vertical-align: -0.08em;
}

.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 18px 40px;
}

.toolbar-group-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}

/* 1rem, not 0.9: the DESIGN.md Body floor. These name the controls a learner
   opens Settings to find, so they are not decoration. */
.toolbar-group-title {
  margin: 0 0 8px;
  color: var(--text-muted);
  font-size: 1rem;
}

.toolbar-group-controls {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(76px, 1fr));
  gap: 10px;
}

.space-toggle-button {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  min-height: 4rem;
  border: 2px solid var(--primary);
  border-radius: 0.875rem;
  padding: 0.5rem 0.875rem;
  cursor: var(--pointer-hand);
  font-weight: 700;
}

.space-toggle-button.active {
  background: var(--primary);
  color: var(--card-bg);
}

.space-toggle-icon {
  display: inline-flex;
  align-items: center;
}

.spacebar-icon {
  width: 28px;
  height: 12px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
}

.space-toggle-label {
  display: inline-block;
  /* Wide enough that the button does not resize when Show flips to Hide, and
     no wider — it was 100px back when the label carried the word "spaces". */
  min-width: 2.75rem;
  text-align: left;
}

.text-size-controls {
  display: flex;
  align-items: flex-start;
  gap: 1.25rem;
}

.size-control {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
}

.font-toolbar-group .toolbar-group-controls {
  display: flex;
  align-items: flex-start;
  gap: 14px;
}

.text-format-toolbar-group .toolbar-group-controls {
  display: flex;
  align-items: flex-start;
  gap: 14px;
}

.font-control {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  flex: 0 0 64px;
  width: 64px;
  min-width: 0;
  gap: 0.25rem;
}

.font-toolbar-group .font-control {
  position: relative;
}

.font-toolbar-group .tool-label {
  position: absolute;
  z-index: 2;
  top: 100%;
  left: 50%;
  width: max-content;
  max-width: 160px;
  margin: 0;
  border-radius: 6px;
  background: var(--text-main);
  color: var(--card-bg);
  padding: 5px 8px;
  font-size: 0.72rem;
  line-height: 1.2;
  opacity: 0;
  pointer-events: auto;
  transform: translate(-50%, -4px);
  visibility: hidden;
}

@media (hover: hover) {
  .font-control:not(.tooltip-suppressed):hover .tool-label {
    opacity: 1;
    transform: translate(-50%, 0);
    visibility: visible;
  }
}

/* :has(:focus-visible), not :focus-within — a Fonts task focuses its target font
   button on render, and :focus-within made that reveal the button's name with no
   pointer near it, naming the answer on the one task where the font itself is
   the question. :focus-visible only matches focus the browser attributes to the
   keyboard, so a learner tabbing here still hears and sees the name. */
.font-control:not(.tooltip-suppressed):has(:focus-visible) .tool-label {
  opacity: 1;
  transform: translate(-50%, 0);
  visibility: visible;
}

.tool-button {
  min-height: 4rem;
  min-width: 4rem;
  border: 2px solid var(--primary);
  border-radius: 0.875rem;
  background: var(--card-bg);
  color: var(--primary);
  cursor: var(--pointer-hand);
  padding: 0.5rem;
}

.tool-button.active {
  background: var(--primary);
  color: var(--card-bg);
}

button:disabled,
#guidedActions #checkButton[aria-disabled="true"] {
  border-color: var(--border);
  background: var(--keyboard-key);
  color: var(--text-muted);
  cursor: not-allowed;
}

.font-choice.unavailable {
  opacity: 0.55;
  cursor: not-allowed;
}

/* Cursor group. Sits straight after Spaces rather than pushed right, so the
   sizes and the swatches fit on one row and the drawer stays one row tall.
   The toolbar's own 40px column gap separates the two halves, matching the gap
   between every other pair of groups. */
.cursor-toolbar-group {
  display: grid;
  grid-template-columns: auto auto;
  align-items: center;
  column-gap: 40px;
}

.cursor-toolbar-group .toolbar-group-title {
  grid-column: 1 / -1;
}

.cursor-size-controls {
  display: flex;
  gap: 14px;
}

.cursor-size-choice {
  display: grid;
  place-items: center;
}

/* Both buttons stay 4rem so they remain equal touch targets — only the arrow
   inside changes size. A genuinely small button would be the hardest thing in
   the drawer to hit, a poor reward for asking for a smaller pointer. */
.cursor-size-glyph path {
  fill: var(--card-bg);
  stroke: var(--primary);
  stroke-width: 4;
  stroke-linejoin: round;
}

.cursor-size-choice.active .cursor-size-glyph path {
  stroke: var(--text-main);
  stroke-width: 3.5;
}

.cursor-size-glyph--normal {
  width: 24px;
  height: 24px;
}

.cursor-size-glyph--bigger {
  width: 42px;
  height: 42px;
}

.cursor-colour-controls {
  display: flex;
  gap: 8px;
}

/* Deliberately px, not rem, and the one place in this drawer that breaks the
   rem rule: the classroom board runs a 67% root, which would shrink a rem-sized
   44px swatch to about 29px — under the minimum touch target. */
.cursor-swatch {
  position: relative;
  width: 44px;
  height: 44px;
  border: 2px solid rgba(0, 0, 0, 0.18);
  border-radius: 10px;
  padding: 0;
  cursor: var(--pointer-hand);
}

/* Without its own dark edge the white swatch disappears into the drawer tint. */
.cursor-swatch--white {
  border-color: #6d7688;
  background: #ffffff;
}

.cursor-swatch--yellow {
  background: #f2c200;
}

.cursor-swatch.active {
  box-shadow: 0 0 0 3px var(--primary);
}

/* The swatch is the one control kept OFF the shared focus-visible list above.
   That rule adds a 2px box-shadow for separation, and .active draws the
   selected ring with box-shadow too — so sharing it would erase the selected
   ring the moment the selected swatch took focus. Outline only here. */
.cursor-swatch:focus-visible {
  outline: 4px solid var(--focus-blue);
  outline-offset: 3px;
}

/* Colour alone cannot carry the state — a learner who cannot separate white
   from yellow still has to see which one is on. */
.cursor-swatch-check {
  position: absolute;
  inset: 0;
  margin: auto;
  width: 22px;
  height: 22px;
  fill: none;
  stroke: var(--text-main);
  stroke-width: 3.4;
  stroke-linecap: round;
  stroke-linejoin: round;
  opacity: 0;
}

.cursor-swatch.active .cursor-swatch-check {
  opacity: 1;
}

/* Fonts mode dims the control groups the current task is not about, at the same
   strength as an unavailable font so the drawer has one "not now" look. The
   buttons stay in the tab order — see updateFontToolAvailability(). */
.tool-button.is-off-task,
.space-toggle-button.is-off-task,
.cursor-swatch.is-off-task,
.settings-close-button.is-off-task {
  opacity: 0.55;
  cursor: not-allowed;
}

.tool-icon,
.tool-label {
  display: block;
  text-align: center;
}

.tool-icon {
  font-size: 1.6rem;
  line-height: 1.1;
  font-weight: 700;
}

.bold-tool-icon {
  font-family: "Inter", "Inter Local", sans-serif;
  font-size: 2rem;
}

.underline-tool-icon {
  font-family: "Inter", "Inter Local", sans-serif;
  font-size: 2rem;
  text-decoration: underline;
  text-underline-offset: 3px;
}

/* Mirrors the settings drawer's font button so the task points at the thing the
   learner has to tap, without naming the font. */
.task-font-swatch {
  display: inline-block;
  min-width: 2.2em;
  padding: 0.05em 0.25em;
  border: 2px solid var(--primary);
  border-radius: 0.28em;
  color: var(--primary);
  background: var(--card-bg);
  font-weight: 700;
  line-height: 1.15;
  text-align: center;
  vertical-align: baseline;
}

/* 1rem, not 0.82: "Smaller" and "Bigger" label the controls for making text
   readable, so they were the smallest text in the app. The font names keep the
   smaller tooltip size below — they stay hidden by default on purpose. */
.tool-label {
  margin-top: 4px;
  color: var(--text-muted);
  font-size: 1rem;
  font-weight: 700;
}

.setting-status {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 4rem; /* matches .tool-button min-height so the number centers on the button, not the button+label stack */
  margin: 0;
  color: var(--text-main);
  font-size: 1.5rem;
  font-weight: 700;
}

.editor-heading-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 0.75rem;
  margin-bottom: 0.875rem;
}

.editor-heading-row h2 {
  margin-bottom: 0;
}

.typing-area-wrap {
  --typing-line-limit: 5;
  position: relative;
  border-radius: var(--radius-lg);
  background: var(--card-bg);
}

.typing-area-wrap.is-correct .typing-area {
  border-color: var(--success-border);
}

.typing-area-wrap.is-incorrect .typing-area {
  border-color: var(--warning-border);
}

.typing-space-mirror,
.typing-hint-mirror,
.typing-area {
  min-height: calc(1 * 1.6em + 2.25rem + 0.375rem);
  max-height: calc(var(--typing-line-limit) * 1.6em + 2.25rem + 0.375rem);
  border: 3px solid var(--field-border);
  border-radius: var(--radius-lg);
  padding: 1.125rem;
  font-family: "Atkinson Hyperlegible Local", sans-serif;
  font-size: 18px;
  line-height: 1.6;
  overflow-wrap: break-word;
  white-space: pre-wrap;
}

.typing-space-mirror,
.typing-hint-mirror {
  position: absolute;
  z-index: 0;
  inset: 0;
  border-color: transparent;
  color: transparent;
  overflow: hidden;
  pointer-events: none;
}

.typing-space-marker {
  color: transparent;
  background: linear-gradient(var(--space-cue), var(--space-cue)) center / 100% 0.8em no-repeat;
}

.typing-hint-marker {
  background: var(--hint-bg);
  border-radius: 2px;
  box-shadow: inset 0 -4px var(--hint-border);
}

/* Stays inline (not inline-block) so the bar's containing block is the text's own
   font box: top/bottom then track the line exactly, whichever font is chosen. */
.typing-hint-marker.is-insertion {
  position: relative;
  display: inline;
  background: transparent;
  border-radius: 0;
  box-shadow: none;
}

.typing-hint-marker.is-insertion::after {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 0.16em;
  min-width: 7px;
  /* Same colour as the marker above, not the activity accent: both mean
     "look here", so both look the same everywhere. Shape carries the
     difference — a block over letters, a bar between them. */
  background: var(--hint-bg);
  box-shadow: inset 0 -4px var(--hint-border);
  border-radius: 2px;
  transform: translateX(-50%);
}

/* text-decoration-skip-ink: by default a browser breaks the underline around
   every descender, so a word full of j/g/y turns the line into scattered
   dashes — worst in Poppins, whose descenders are long. Draw one straight line
   instead. No underline-offset: each font places its own, and pushing it below
   the tails reads as detached. Set on the box, not on `u`: the property
   inherits, so it still applies if an engine's execCommand builds the
   underline from a styled span instead of a <u>. */
.typing-area {
  position: relative;
  z-index: 1;
  background: transparent;
  outline: none;
  overflow: auto;
  text-decoration-skip-ink: none;
}

.typing-area:focus {
  border-color: var(--primary);
  box-shadow: 0 0 0 4px var(--focus-glow);
}

/* Clicking in gets the quiet glow above; arriving by keyboard gets the same
   4px ring every button uses. The glow alone is 14% alpha — too faint to be a
   focus indicator on its own for the learners being taught what focus is. */
.typing-area:focus-visible {
  outline: 4px solid var(--focus-blue);
  outline-offset: 3px;
  box-shadow: 0 0 0 2px var(--card-bg);
}

.cursor-practice-mode .typing-area {
  cursor: var(--pointer-text);
  font-weight: 700;
  letter-spacing: 0.04em;
}

.typing-limit-help,
.typing-input-status {
  margin: 8px 0 0;
  line-height: 1.4;
}

.typing-limit-help {
  color: var(--text-muted);
  font-size: 0.95rem;
}

.typing-input-status {
  color: var(--warning-text);
  font-weight: 700;
}

.typing-input-status:empty {
  display: none;
}

.help-dialog {
  width: min(520px, calc(100% - 32px));
  max-height: calc(100vh - 32px);
  max-height: calc(100dvh - 32px);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--card-bg);
  color: var(--text-main);
  box-shadow: var(--shadow);
  padding: 0;
  overflow: hidden;
}

.help-dialog::backdrop {
  background: var(--scrim);
}

.help-dialog-content {
  display: flex;
  flex-direction: column;
  box-sizing: border-box;
  max-height: calc(100vh - 32px);
  max-height: calc(100dvh - 32px);
  overflow: hidden;
  padding: 24px;
}

.info-dialog {
  max-height: calc(100vh - 32px);
  max-height: calc(100dvh - 32px);
  overflow: hidden;
}

.info-dialog-content {
  display: flex;
  flex-direction: column;
  box-sizing: border-box;
  max-height: calc(100vh - 32px);
  max-height: calc(100dvh - 32px);
  overflow: hidden;
}

.info-dialog-body {
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding-right: 4px;
}

[data-info-dialog-title]:focus-visible {
  border-radius: 4px;
  outline: 3px solid var(--focus-blue);
  outline-offset: 3px;
}

/* Never wraps: the button's label changes as it plays (Listen / Pause /
   Loading audio...), and with wrapping on, the widest label pushed it onto its
   own line and back again mid-playback. The heading shrinks instead. */
.help-dialog-topbar {
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  /* Room to read the title as its own line before the steps start. */
  margin-bottom: 1.75rem;
}

.help-dialog-topbar .audio-button {
  flex: 0 0 auto;
  white-space: nowrap;
  /* Fits every label this button shows — Listen, Pause, Loading... — so it is
     one fixed size for the whole playback, not three. rem, so it tracks the
     label at any root size (see the display-scale note at the top of this
     file). Reserving room for a longer loading word instead would squeeze the
     heading beside it to a sliver on a phone. */
  min-width: 10rem;
}

.help-dialog-topbar h2 {
  margin: 0;
  font-size: 1.5rem;
}

.help-dialog-heading {
  display: flex;
  align-items: center;
  gap: 12px;
  /* Lets the title wrap rather than forcing the button off the row. */
  min-width: 0;
}

.dialog-icon {
  display: flex;
  flex: 0 0 auto;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border-radius: 50%;
}

.dialog-icon svg {
  width: 20px;
  height: 20px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.dialog-icon--help {
  background: var(--primary-soft);
  color: var(--primary);
}

.dialog-icon--leave {
  background: var(--warning-bg);
  color: var(--warning-text);
}

.dialog-icon--info {
  background: var(--primary-soft);
  color: var(--primary);
}

.help-dialog-body {
  font-size: 20px;
  line-height: 1.5;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding-right: 4px;
}

.help-dialog-body p {
  margin: 0 0 12px;
}

.help-dialog-body p:last-child {
  margin-bottom: 0;
}

.info-dialog-list {
  margin: 0 0 12px;
  padding-left: 1.35em;
}

.info-dialog-list li {
  margin-bottom: 8px;
}

.info-dialog-list li:last-child {
  margin-bottom: 0;
}

.info-dialog-list li::marker {
  color: var(--primary);
}

.help-dialog-body .help-list {
  margin: 0;
  padding-left: 1.35em;
}

.help-dialog-body .help-list li {
  margin: 0 0 12px;
}

.help-dialog-body .help-list li:last-child {
  margin-bottom: 0;
}

.help-dialog-body .help-list li::marker {
  color: var(--primary);
}

.help-dialog-body .help-sublist {
  margin: 8px 0 0;
  padding-left: 1.35em;
}

.help-dialog-body .help-sublist li {
  margin-bottom: 4px;
}

.help-inline-icon-wrap {
  display: inline-flex;
  align-items: center;
  vertical-align: -0.15em;
  color: var(--primary);
}

.help-inline-icon {
  width: 1.25em;
  height: 1.25em;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.help-dialog-actions {
  display: flex;
  justify-content: flex-end;
  margin-top: 20px;
}

.help-dialog-actions .primary-button {
  min-width: 6rem;
}

.leave-practice-dialog .help-dialog-actions {
  flex-wrap: wrap;
  gap: 12px;
}

.leave-practice-dialog .help-dialog-actions .primary-button,
.leave-practice-dialog .help-dialog-actions .secondary-button {
  min-width: 9.375rem;
}

.info-dialog-body a {
  color: var(--primary);
  font-weight: 700;
}

.about-elevenlabs-logo {
  display: block;
  margin: 12px auto 0;
  max-width: 180px;
  width: 100%;
  height: auto;
}

.about-ircc-logo {
  display: block;
  margin: 20px auto 0;
  max-width: 420px;
  width: 100%;
  height: auto;
  transform: translateX(-10px);
}

/* Hints are gone (2026-08-28), so Check and Next no longer share the row with a
   third button. Stop them stretching to fill it and centre them instead, at one
   shared width so the row does not resize when Next replaces Check. */
#guidedActions {
  justify-content: center;
}

#guidedActions #checkButton:not(.hidden),
#guidedActions #nextTaskButton:not(.hidden) {
  flex: 0 0 auto;
  min-width: 18rem;
}

#activityCard .action-row {
  margin-top: 0.75rem;
  padding-top: 1rem;
  border-top: 1px solid var(--border);
}

#activityCard .action-row.has-success {
  margin-top: 1.25rem;
  padding-top: 0;
  border-top: 0;
}

#activityCard .action-row.has-check-feedback {
  margin-top: 1.25rem;
  padding-top: 0;
  border-top: 0;
}

#activityCard > .feedback-message:empty {
  min-height: 0;
  margin-top: 0;
}

.hidden {
  display: none;
}

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

@media (max-width: 640px) {
  /* Same full-width shell as desktop, keeping the narrower 10px mobile inset.
     The safe-area guard is what protects notched phones. */
  .app-shell {
    width: 100%;
    padding: 14px max(10px, env(safe-area-inset-right, 0px)) 2.25rem
      max(10px, env(safe-area-inset-left, 0px));
  }

  #taskTitle {
    font-size: 2.25rem;
  }

  .task-topline {
    align-items: flex-start;
  }

  /* Stacks label over count to stay narrow, but sits in the card's top-right
     corner beside Back rather than taking a full row under it. */
  .task-status {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 2px;
    /* Capped so the longest mode names wrap here instead of shoving the whole
       block onto its own row below Back. */
    max-width: 58%;
    margin-left: auto;
    line-height: 1.25;
    text-align: right;
  }

  .task-status .task-count::before {
    content: none;
    margin: 0;
  }

  /* .task-status is a column here, so the icon would sit above the name instead
     of beside it, in the tightest corner of the layout. The completion screen
     keeps its copy. */
  .task-status .title-brand-icon {
    display: none;
  }

  #guidedActions #checkButton:not(.hidden) {
    padding-inline: 8px;
    font-size: 1.5rem;
  }

  #guidedActions #hintButton:not(.hidden) {
    gap: 0.375rem;
    padding-inline: 8px;
    font-size: 1.5rem;
  }

  #guidedActions #hintButton .hint-icon {
    width: 1.25rem;
    height: 1.25rem;
  }

  .hint-button-label {
    line-height: 1.15;
  }

  .mode-card,
  .task-card {
    padding: 14px;
  }

  .intro-view .mode-card {
    padding: 1.75rem 1.125rem 1.5rem;
    gap: 1.875rem;
  }

  .linc-suite-logo-mark {
    width: min(108px, 40vw);
  }

  .footer-links {
    width: 100%;
    justify-content: center;
  }

  .footer-link-separator {
    display: none;
  }

  .intro-view .intro-header {
    min-height: auto;
    margin-bottom: 28px;
    padding: 0;
  }

  .intro-heading {
    gap: 6px;
  }

  /* Stacked below 640px: there is no room left of a two-line title for the mark
     to hang into, and the optical shift would push it off the screen edge. */
  .intro-title-row {
    flex-direction: column;
    gap: 12px;
    margin-right: 0;
  }

  .free-practice-tile-description {
    font-size: 0.9375rem;
  }

  .editor-toolbar {
    padding: 14px 0 12px;
    margin-top: 26px;
  }

  .toolbar {
    flex-direction: column;
    gap: 20px;
  }

  .toolbar-group {
    width: 100%;
  }

  .settings-close-button {
    position: static;
  }

  .font-toolbar-group .toolbar-group-controls {
    flex-wrap: wrap;
  }

  .completion-actions .primary-button,
  .completion-actions .secondary-button {
    width: 100%;
    flex: 1 1 100%;
  }

  /* At 375px "Practice something else" wraps to two lines at 1.5rem and the
     secondary ends up TALLER than the primary, inverting the hierarchy the
     stack exists to create. One step down keeps it on one line. */
  .completion-actions .secondary-button {
    font-size: 1.25rem;
  }

  /* Card padding drops to 14px here, so both gaps need 4px more to stay level
     with the 36px below Hear message. */
  .completion-screen {
    padding-top: 22px;
    padding-bottom: 22px;
  }

  .leave-practice-dialog .help-dialog-actions .primary-button,
  .leave-practice-dialog .help-dialog-actions .secondary-button {
    width: 100%;
  }

  .activity-view .app-header {
    align-items: center;
    padding-top: 0;
  }

  .activity-view .header-actions {
    width: auto;
    justify-content: flex-end;
    gap: 10px;
  }

  .home-button span,
  .settings-button span,
  .help-button span {
    display: none;
  }

  .home-button,
  .settings-button,
  .help-button {
    gap: 0;
    min-width: 3rem;
    padding: 0.5rem;
    justify-content: center;
  }

}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* A checkmark on a button that restores the sentence says the wrong thing, so
   the icon goes while the Fix Mistakes modes are offering Try again. */
#checkButton.is-retry .check-icon {
  display: none;
}

/* ---------------------------------------------------------------------------
   CLB 2 step sets
   ---------------------------------------------------------------------------
   The instruction list on the left, the boxes it names on the right, so a
   learner can read step 3 and look straight across at the box it points at.
   Below 640px the two stack, list first - the list IS the task, and putting it
   under the boxes would hide it behind the keyboard on a phone. */
.steps-panel {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 2rem;
  align-items: start;
}

/* .hidden is 0,1,0 and loses to any two-class display rule; .steps-panel sets
   display below and is declared later, so hiding it needs its own rule. Same
   trap the #guidedActions buttons carry :not(.hidden) for. */
.steps-panel.hidden {
  display: none;
}

.steps-heading {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin: 0 0 1rem;
  font-size: 1.75rem;
}

.steps-heading-icon {
  flex: 0 0 auto;
  width: auto;
  height: 34px;
}

/* The heading takes focus when a set opens, so it needs the same visible ring
   every other focus target has - it is not a silent -1 target. */
.steps-heading:focus-visible {
  outline: 3px solid var(--focus-blue);
  outline-offset: 4px;
  border-radius: 6px;
}

.steps-list {
  margin: 0;
  padding: 0;
  /* The numbers are drawn in .step-mark, which can swap to a tick. A native
     marker cannot, and two number columns would be worse than none. */
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

.step-row {
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
  padding: 0.75rem 1rem;
  border: 2px solid var(--border);
  border-radius: 12px;
  background: var(--card-bg);
  /* Deliberately NOT --steps-font-size. That variable is the learner's own text
     size from the Settings drawer, and it belongs to what they type; at its 50px
     desktop default a five-step list ran off the screen. The list is read, not
     typed into, so it takes a fixed size and the boxes keep the setting. */
  font-size: 1.75rem;
  line-height: 1.35;
}

.step-row.is-current {
  border-color: var(--primary);
  background: var(--primary-soft);
}

.step-row.is-done {
  border-color: var(--success-border);
  background: var(--success-bg);
  color: var(--success-text);
}

.step-mark {
  flex: 0 0 auto;
  min-width: 1.6em;
  font-weight: 700;
  text-align: center;
}

.step-text {
  flex: 1 1 auto;
}

.step-tick {
  flex: 0 0 auto;
  font-weight: 700;
  color: var(--success-text);
}

.steps-fields {
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
}

.step-field {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}

.step-field-label {
  font-size: 1.125rem;
  font-weight: 600;
  color: var(--text-muted);
}

.step-input {
  width: 100%;
  padding: 0.6rem 0.75rem;
  border: 2px solid var(--field-border);
  border-radius: 12px;
  background: var(--card-bg);
  color: var(--text-main);
  font-family: var(--steps-font-family, inherit);
  font-size: var(--steps-font-size, 1.5rem);
  line-height: 1.4;
  resize: none;
}

.step-input:focus-visible {
  outline: 3px solid var(--focus-blue);
  outline-offset: 2px;
}

/* A box the list has not reached yet. readOnly, not disabled: a disabled input
   cannot be clicked or focused, so it could not trigger the nudge that tells
   the learner which step to do first - which is the whole point of locking it.
   Deliberately NOT tinted: it stays the same white as an open box, because a
   darker box read as broken rather than as "not yet". The lock is still real -
   readOnly in script.js, and the nudge still fires. */
.step-input.is-locked {
  color: var(--text-muted);
  cursor: not-allowed;
}

/* The nudge, not an error. It borrows the warning palette rather than the red
   an error would use, and it holds its space so the boxes do not jump when it
   appears and disappears. */
.steps-nudge {
  margin: 0.75rem 0 0;
  min-height: 2.5rem;
  padding: 0.5rem 0.75rem;
  border-radius: 10px;
  font-size: 1.25rem;
  visibility: hidden;
}

.steps-nudge.is-visible {
  visibility: visible;
  border: 2px solid var(--warning-border);
  background: var(--warning-bg);
  color: var(--warning-text);
}

.steps-actions {
  margin-top: 1.25rem;
  justify-content: center;
}

/* Match the large guided-action target used by Check and Next in CLB 1. The
   button stays secondary because it can be pressed early to show the nudge. */
.steps-actions #stepsNextButton {
  flex: 0 1 18rem;
  width: 18rem;
  max-width: 100%;
  min-height: 5.25rem;
  padding: 1.375rem;
  border-radius: 0.75rem;
  font-size: 1.75rem;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: var(--shadow-action-secondary);
}

@media (max-width: 640px) {
  .steps-panel {
    grid-template-columns: minmax(0, 1fr);
    gap: 1.5rem;
  }

  .step-row {
    font-size: 1.5rem;
  }
}
