/* base.css — structure and components for Symmetry Scene.
   Adapted from the supplied template. Colour decisions live in the theme
   sheets (theme-classic / theme-paper / theme-contrast / theme-calm); this file
   defines the layout, the components, and fallback values for every theme token. */

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

/* `hidden` must win over component display rules. Without this, a `display: grid`
   or `flex` rule (e.g. .app-container) overrides the UA [hidden] rule and the
   element stays visible — the Exercises play view toggles on this. */
[hidden] { display: none !important; }

/* Nothing in this app should ever scroll sideways. The layout below is built so
   it can't, but Android font scaling can inflate rem-based padding past what a
   narrow phone has — so this is the backstop that guarantees it. */
html {
  overflow-x: hidden;
  max-width: 100%;
}

:root {
  /* Chrome */
  --bg-dark: #121318;
  --bg-panel: #1e2029;
  --bg-card: #282a36;
  --accent: #6272a4;
  --accent-active: #ff79c6;
  --accent-ink: #1a1226; /* text colour on the primary button */
  --text-main: #f8f8f2;
  --text-muted: #aab3d0; /* lightened from the template for AA contrast */
  --border-color: #343746;
  --header-height: 56px;

  /* Grid surface (theme-aware, per V1.1 decision 1) */
  --board-bg: #282a36;
  --cell-blank: #2f3240; /* colour of an unpainted cell */
  --cell-line: #5a6072; /* grid lines — must contrast with cells AND paint */

  /* Evaluation overlays */
  --axis: #ffd54a;
  --axis-halo: rgba(0, 0, 0, 0.55);
  --nearmiss: #ff3b30;
  --cursor: #ffffff;
  --cursor-halo: rgba(0, 0, 0, 0.6);

  /* Locked-cell padlock badge (V3). --lock is the badge, --lock-halo the
     contrast outline behind it. Default suits a dark board (Classic); the light
     and contrast themes override below. */
  --lock: #eae4f2;
  --lock-halo: rgba(0, 0, 0, 0.72);

  /* Tutorial transformation "ghost" — the moving copy. Distinct from the axis
     (yellow) and the cursor (white); each theme retunes it for its board. */
  --ghost: #38bdf8;                 /* outline of the moving copy */
  --ghost-fill: rgba(56, 189, 248, 0.22); /* faint fill so it's easy to track */

  --focus: #ffb300;
  --touch: 44px; /* minimum comfortable touch target */
}

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  background-color: var(--bg-dark);
  color: var(--text-main);
  /* Desktop: lock the page height so each panel scrolls on its own rather than
     the whole document. The narrow breakpoint below releases this. */
  height: 100vh;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  line-height: 1.4;
}

/* ---------- TOP BAR ---------- */
.top-bar {
  /* Anchors the narrow-screen dropdown panels. The z-index is required, not
     decorative: the board's backdrop-filter creates a stacking context, so
     without it the dropdowns render behind the grid. */
  position: relative;
  z-index: 50;
  min-height: var(--header-height);
  background-color: var(--bg-panel);
  border-bottom: 1px solid var(--border-color);
  /* Three regions: sections · title · controls. Equal 1fr side tracks with an
     auto centre keep the TITLE dead-centre on the page however wide the sections
     or controls grow — which a single centred nav could not do once the two
     sides differed in width (adding the "Remember theme" toggle exposed that). */
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: center;
  gap: 0.5rem 1rem;
  padding: 0.5rem 1.5rem;
  flex-shrink: 0;
}

/* On the app this is the page's <h1>; on a content page it's an <a> home link,
   because there the <h1> belongs to the article. Styled the same either way. */
.brand {
  font-weight: 700;
  font-size: 1.25rem;
  letter-spacing: 0.5px;
  min-width: 0; /* allow shrinking rather than forcing the bar wider */
  color: inherit;
  text-decoration: none;
}

a.brand:hover { text-decoration: underline; }

/* Assign the three regions explicitly (GOTCHA): the nav dropdown becomes
   position:absolute below the breakpoint. It now sits *inside* .top-bar__start
   rather than being a grid item itself, but pinning the columns still guards the
   layout — the title stays in the centre track and the controls hard-right. */
.top-bar__start { grid-column: 1; justify-self: start; }
.brand { grid-column: 2; justify-self: center; text-align: center; }
.top-bar__end { grid-column: 3; }

/* ---- Left region: menu button (narrow) + section nav ---- */
.top-bar__start {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  min-width: 0;
}

.nav { min-width: 0; }

.nav__list {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap; /* safety valve: grow the bar rather than overflow it */
  gap: 0.35rem;
  list-style: none;
  margin: 0;
  padding: 0;
}

.nav__link {
  display: inline-flex;
  align-items: center;
  min-height: var(--touch);
  padding: 0 0.75rem;
  border-radius: 8px;
  color: var(--text-muted);
  text-decoration: none;
  font-weight: 600;
  font-size: 0.95rem;
  white-space: nowrap;
}

.nav__link:hover { color: var(--text-main); background: rgba(127, 127, 127, 0.14); }

.nav__link[aria-current="page"] {
  color: var(--text-main);
  background: rgba(127, 127, 127, 0.18);
}

.top-bar__end {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  min-width: 0;
  justify-self: end;
}

.theme-field {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  min-width: 0;
}

.field-label {
  font-size: 0.85rem;
  color: var(--text-muted);
  white-space: nowrap;
}

/* "Remember theme" toggle. The whole label is the tap target (>=44px tall). */
.field-check {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  min-height: var(--touch);
  font-size: 0.85rem;
  color: var(--text-muted);
  white-space: nowrap;
  cursor: pointer;
}

.field-check input {
  width: 18px;
  height: 18px;
  cursor: pointer;
  accent-color: var(--accent-active);
}

/* The fuller explanation. On wide screens the footer/CTA carries it, so it shows
   only when the settings sit in the narrow dropdown (see the breakpoint below). */
.field-note { display: none; }

/* ---- Icon buttons (narrow screens only) ---- */
.icon-btn {
  display: none; /* the wide-screen default; the breakpoint reveals them */
  align-items: center;
  justify-content: center;
  width: var(--touch);
  height: var(--touch);
  padding: 0;
  border: 1px solid var(--border-color);
  border-radius: 10px;
  background: var(--bg-card);
  color: var(--text-main);
  cursor: pointer;
}

.icon-btn svg {
  width: 22px;
  height: 22px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
}

.icon-btn[aria-expanded="true"] {
  background: var(--accent-active);
  color: var(--accent-ink, #1a1226);
  border-color: var(--accent-active);
}

.select {
  background-color: var(--bg-card);
  color: var(--text-main);
  border: 1px solid var(--border-color);
  padding: 0.4rem 0.75rem;
  border-radius: 6px;
  font-size: 0.9rem;
  min-height: var(--touch); /* a child may tap this — keep it a full target */
  cursor: pointer;
  max-width: 100%;
  min-width: 0;
}

/* ---- Narrow header: nav and theme picker become dropdowns ----
   The panels are the *same* elements as above, repositioned — not copies — so
   there is only ever one theme <select> in the document.

   The breakpoint is in **em, not px**, deliberately. `em` in a media query is
   relative to the browser's default font size, which is exactly what Android
   and iOS font-size settings change. An em breakpoint collapses to the app-bar
   earlier when text is bigger, which is when the extra room is actually needed.

   Re-measured for the V1.4 layout (title centred, sections left, controls right).
   Centring the title makes the side tracks symmetric, so each 1fr reserves the
   *wider* side (the controls, ~337px with the "Remember theme" toggle); the
   inline header needs ≈ brand + 2·controls + gaps ≈ 920px at the 16px default.
   Below that it collapses cleanly to the app-bar — menu (left), title, settings
   (right) — so nothing squishes. 58.99em ≈ 944px gives a little slack.

   Keep in step with WIDE in js/nav.js, and re-measure when a section is added:
   more nav links widen the left side and push this up. Content-dependent. */
@media (max-width: 58.99em) {
  .icon-btn { display: inline-flex; }

  .nav,
  .theme-field {
    display: none;
    position: absolute;
    top: calc(100% + 6px);
    z-index: 40;
    flex-direction: column;
    align-items: stretch;
    gap: 0.35rem;
    min-width: 210px;
    max-width: calc(100vw - 1.5rem);
    padding: 0.5rem;
    background: var(--bg-panel);
    border: 1px solid var(--border-color);
    border-radius: 12px;
    box-shadow: 0 14px 34px rgba(0, 0, 0, 0.4);
  }

  /* Each dropdown opens under its own button: menu on the left, theme on the right. */
  .nav { left: 0.75rem; }
  .theme-field { right: 0.75rem; }

  .nav.is-open,
  .theme-field.is-open { display: flex; }

  .nav__list { flex-direction: column; align-items: stretch; gap: 0.15rem; }
  .nav__link { justify-content: flex-start; }

  .theme-field .select { width: 100%; }

  /* Room to explain the toggle now the picker is a stacked dropdown. */
  .theme-field .field-check { white-space: normal; }
  .theme-field .field-note {
    display: block;
    margin: 0.1rem 0 0;
    font-size: 0.8rem;
    line-height: 1.35;
    color: var(--text-muted);
    white-space: normal;
  }
}

/* ---------- APP FOOTER ----------
   Deliberately slim and quiet: it speaks to teachers and school IT, not to the
   child painting, so it should cost as little vertical space as possible. */
.app-footer {
  flex-shrink: 0;
  padding: 0.5rem 1.5rem;
  background-color: var(--bg-panel);
  border-top: 1px solid var(--border-color);
  font-size: 0.8rem;
  color: var(--text-muted);
  text-align: center;
}

.app-footer a { color: var(--accent-active); }

/* ---------- READING LAYOUT (content pages) ----------
   The app locks body height so its panels scroll independently. A content page
   is an ordinary document, so `body.page` releases that and lets the page
   scroll normally at every width. */
/* On a long article the header stays put, so the section nav and theme picker
   remain reachable without scrolling back. The app doesn't need this — its
   header never scrolls out of view. */
body.page .top-bar {
  position: sticky;
  top: 0;
}

/* Keep anchored targets clear of the sticky bar rather than tucked behind it.
   Nothing links to an anchor yet, but the V1.2 routing decision puts hash
   sub-pages inside sections, so this will matter. */
html { scroll-padding-top: 5rem; }

/* A landscape phone has little height to spare; give it back. */
@media (max-height: 26em) {
  body.page .top-bar { position: static; }
}

body.page {
  height: auto;
  min-height: 100vh;
  /* `visible`, never `auto`: any other value makes <body> its own scroll
     container, which stops its background propagating to the canvas. With the
     themes' background-attachment: fixed, scrolling then exposes bare canvas as
     a black band. Let the viewport do the scrolling. */
  overflow: visible;
}

.layout--reading {
  flex: 1;
  padding: clamp(1rem, 3vw, 2.5rem) 1rem clamp(2rem, 6vw, 4rem);
}

/* The prose sits on a panel surface, never directly on the page background:
   in Classic that background is the vivid gradient, where white text measured
   as low as 1.4:1. On --bg-panel the contrast is the value already verified
   per theme. */
.reading {
  max-width: 68ch; /* ~65–70 characters — the comfortable measure for prose */
  margin: 0 auto;
  padding: clamp(1.25rem, 4vw, 2.75rem);
  background: var(--bg-panel);
  border: 1px solid var(--border-color);
  border-radius: 14px;
}

.reading > * + * { margin-top: 1rem; }

.reading h1 {
  font-size: clamp(1.7rem, 4.5vw, 2.3rem);
  line-height: 1.2;
  letter-spacing: -0.01em;
}

.reading h2 {
  margin-top: 2.25rem;
  padding-top: 1rem;
  border-top: 1px solid var(--border-color);
  font-size: clamp(1.25rem, 3vw, 1.5rem);
  line-height: 1.25;
}

.reading h3 {
  margin-top: 1.75rem;
  font-size: 1.1rem;
}

.reading p,
.reading li { line-height: 1.65; }

.reading .lede {
  font-size: 1.1rem;
  color: var(--text-muted);
}

.reading ul,
.reading ol { padding-left: 1.4rem; }
.reading li + li { margin-top: 0.4rem; }

.reading a {
  color: var(--accent-active);
  text-underline-offset: 0.15em;
}
.reading a:hover { text-decoration-thickness: 2px; }

.reading code {
  padding: 0.1em 0.35em;
  border-radius: 5px;
  background: var(--bg-card);
  border: 1px solid var(--border-color);
  font-size: 0.9em;
}

.reading blockquote {
  padding: 0.6rem 0.9rem;
  border-left: 4px solid var(--accent-active);
  background: var(--bg-card);
  border-radius: 0 8px 8px 0;
  color: var(--text-muted);
}

/* Tables must be able to scroll on a phone without widening the page */
.reading .table-wrap { overflow-x: auto; }

.reading table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.95rem;
}

.reading th,
.reading td {
  padding: 0.5rem 0.6rem;
  border: 1px solid var(--border-color);
  text-align: left;
  vertical-align: top;
}

.reading thead th {
  background: var(--bg-card);
  font-size: 0.85rem;
  letter-spacing: 0.03em;
  text-transform: uppercase;
}

/* ---- Article diagrams ----
   Drawn in the app's own visual language and coloured from the theme tokens,
   so the figures re-theme along with everything else. */
.fig { display: block; width: 100%; height: auto; }
.fig__cell { fill: var(--cell-blank); stroke: var(--cell-line); stroke-width: 1; }
.fig__cell.is-on { fill: var(--accent-active); }
.fig__axis { stroke: var(--axis); stroke-width: 3; stroke-linecap: round; }

.fig-grid {
  display: grid;
  /* 120px, not 150: at 360px the reading column offers ~288px of content, so a
     150px minimum allows only one track — ten diagrams in a single stack. 120
     keeps two columns on a phone. */
  grid-template-columns: repeat(auto-fill, minmax(min(120px, 100%), 1fr));
  gap: 1.25rem 1rem;
  margin-top: 1.25rem;
}

.fig-card { text-align: center; }
.fig-card .fig { max-width: 132px; margin: 0 auto; }
.fig-card__name { margin-top: 0.5rem; font-weight: 700; }
.fig-card__blurb { font-size: 0.85rem; color: var(--text-muted); line-height: 1.4; }

.fig-solo { max-width: 190px; margin-inline: auto; }

.reading figure { margin-top: 1.5rem; text-align: center; }
.reading figcaption {
  margin-top: 0.5rem;
  font-size: 0.9rem;
  color: var(--text-muted);
}

/* Call to action closing an article */
.reading .cta { margin-top: 1.5rem; text-align: center; }

/* Must out-specify `.reading a` (0,0,1,1), which otherwise sets the text to
   --accent-active — the button's own background colour, making it invisible. */
.reading a.btn-cta {
  display: inline-block;
  min-height: var(--touch);
  padding: 0.7rem 1.6rem;
  border-radius: 10px;
  background: var(--accent-active);
  color: var(--accent-ink);
  font-weight: 700;
  font-size: 1.05rem;
  text-decoration: none;
}

.reading a.btn-cta:hover { filter: brightness(1.08); text-decoration: none; }

.reading .cta-note {
  margin-top: 0.6rem;
  text-align: center;
  font-size: 0.9rem;
  color: var(--text-muted);
}

.page-footer {
  max-width: 68ch;
  margin: 1.5rem auto 0;
  padding: 0 clamp(1.25rem, 4vw, 2.75rem);
  font-size: 0.9rem;
  color: var(--text-muted);
  text-align: center;
}
.page-footer a { color: var(--accent-active); }

/* ---------- THREE-COLUMN WORKSPACE ---------- */
.app-container {
  display: grid;
  /* Left panel is wide enough for 5 swatches at the 44px touch minimum plus
     gaps and padding (5×44 + 4×8 + 40 = 292). */
  grid-template-columns: 300px minmax(0, 1fr) 300px;
  flex: 1;
  min-height: 0; /* lets the panels scroll instead of stretching the page */
}

.panel {
  background-color: var(--bg-panel);
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
  padding: 1.25rem;
  overflow-y: auto;
  min-width: 0; /* a flex/grid child defaults to min-content — let it shrink */
  /* Always reserve the scrollbar gutter. Without this, the panel's content
     width changes the moment results make it scroll, which visibly resizes the
     Check symmetry button on platforms with space-taking scrollbars. */
  scrollbar-gutter: stable;
}

.panel-left { border-right: 1px solid var(--border-color); }
.panel-right { border-left: 1px solid var(--border-color); }

.section-title {
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 1px;
  color: var(--text-muted);
  margin-bottom: 0.75rem;
}

/* ---------- PALETTE ---------- */
.panel > section { min-width: 0; }

.palette-grid {
  display: grid;
  /* Five fixed columns that can shrink (minmax(0,…) not 1fr): 10 swatches read
     as a tidy 5×2, and the row can never be wider than its panel. */
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: 0.5rem;
}

.palette__swatch {
  aspect-ratio: 1;
  /* No min-width: the column sizing keeps these comfortably above 44px at every
     real width, and a hard minimum is what would force the panel to overflow. */
  min-height: var(--touch);
  padding: 0;
  border-radius: 8px;
  background: var(--swatch);
  border: 2px solid rgba(0, 0, 0, 0.25);
  cursor: pointer;
  transition: transform 0.1s, box-shadow 0.12s;
}

.palette__swatch:hover { transform: scale(1.06); }

.palette__swatch.is-selected {
  border-color: var(--text-main);
  box-shadow: 0 0 0 3px var(--accent-active);
}

/* The blank/eraser swatch shows the themed blank colour with a red slash */
.palette__swatch[data-color="0"] {
  background:
    linear-gradient(45deg, transparent 44%, #c0392b 44% 56%, transparent 56%),
    var(--cell-blank);
}

/* ---------- CONTROLS ---------- */
.control-group {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.control-label {
  display: flex;
  flex-wrap: wrap; /* wrap rather than push the panel wider */
  justify-content: space-between;
  gap: 0 0.5rem;
  font-size: 0.9rem;
}

.control-value { color: var(--text-muted); }

input[type="range"] {
  width: 100%;
  min-height: var(--touch);
  accent-color: var(--accent-active);
  cursor: pointer;
}

.button-stack {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.btn {
  min-height: var(--touch);
  padding: 0 0.9rem;
  /* Centre the label in both axes and drop the anchor underline, so button-styled
     links (the tutorial's Back/Next) match real <button>s. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  text-decoration: none;
  border: 1px solid var(--border-color);
  border-radius: 8px;
  background: var(--bg-card);
  color: var(--text-main);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
  transition: filter 0.12s;
}

.btn:hover:not(:disabled) { filter: brightness(1.25); }
.btn:disabled { opacity: 0.4; cursor: default; }

.btn-primary {
  width: 100%;
  min-height: var(--touch);
  /* A flex item by default shrinks when the panel overflows — pin the size so
     the button never changes shape as results appear. */
  flex-shrink: 0;
  background-color: var(--accent-active);
  color: var(--accent-ink);
  border: none;
  border-radius: 8px;
  padding: 0.6rem 1rem;
  font: inherit;
  font-weight: 700;
  font-size: 1.05rem;
  cursor: pointer;
  transition: filter 0.2s, transform 0.1s;
}

.btn-primary:hover { filter: brightness(1.08); }

/* ---------- WORKSPACE / GRID ---------- */
.workspace {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1.5rem;
  overflow: auto;
  min-width: 0;
}

.board {
  width: min(100%, 78vh);
  max-width: 700px;
  /* Fit whatever height is actually left, rather than trusting the 78vh guess.
     The header and footer both change height when their text wraps, so a fixed
     viewport fraction eventually overflows and the grid area starts scrolling.
     With aspect-ratio set, clamping the height shrinks the width to match. */
  max-height: 100%;
  aspect-ratio: 1 / 1;
  background: var(--board-bg);
  border: 1px solid var(--border-color);
  border-radius: 12px;
  padding: 0.5rem;
}

.grid { display: block; width: 100%; height: 100%; }

/* Cell fills come from the palette as a fill attribute; CSS wins for blanks
   so unpainted cells follow the theme (V1.1 decision 1). Stroke is set here
   rather than as a presentation attribute — var() in presentation attributes
   is not reliably supported outside Chromium. */
.grid__cell {
  stroke: var(--cell-line);
  stroke-width: 1;
}

.grid__cell--blank { fill: var(--cell-blank); }

.tagline {
  font-size: 1rem;
  color: var(--text-main);
  margin-bottom: 0.25rem;
  flex-shrink: 0; /* don't compress when results fill the panel */
}

/* ---------- RESULTS ---------- */
.results-card {
  background-color: var(--bg-card);
  border: 1px solid var(--border-color);
  border-radius: 8px;
  padding: 1rem;
}

.results__placeholder { margin: 0; color: var(--text-muted); }

.results__headline {
  margin: 0 0 0.75rem;
  font-size: 1.05rem;
  font-weight: 700;
}

.results__line { margin: 0.4rem 0; font-size: 0.95rem; }

.results__nearmiss {
  margin: 0.75rem 0 0.35rem;
  padding: 0.6rem 0.7rem;
  background: rgba(255, 179, 0, 0.16);
  border-left: 4px solid var(--focus);
  border-radius: 6px;
  font-size: 0.95rem;
}

.maths-corner {
  margin-top: 0.9rem;
  padding-top: 0.6rem;
  border-top: 1px dashed var(--border-color);
  font-size: 0.85rem;
}

.maths-corner summary {
  cursor: pointer;
  font-weight: 600;
  color: var(--text-muted);
  min-height: 24px;
}

.maths-corner p { margin: 0.4rem 0; color: var(--text-muted); }
.maths-corner__warn { color: #ff6b6b; font-weight: 600; }

/* ---------- GRID OVERLAYS ---------- */
.grid__highlights,
.grid__overlay,
.grid__cursor-group { pointer-events: none; } /* never block painting */

.grid__axis-halo {
  stroke: var(--axis-halo);
  stroke-width: 5;
  stroke-linecap: round;
}

.grid__axis {
  stroke: var(--axis);
  stroke-width: 2.5;
  stroke-linecap: round;
}

.grid__cell--nearmiss {
  fill: none;
  stroke: var(--nearmiss);
  stroke-width: 3;
  animation: nearmiss-pulse 1.1s ease-in-out infinite;
}

@keyframes nearmiss-pulse {
  0%, 100% { stroke-opacity: 1; stroke-width: 2; }
  50% { stroke-opacity: 0.35; stroke-width: 4.5; }
}

@media (prefers-reduced-motion: reduce) {
  .grid__cell--nearmiss { animation: none; stroke-width: 3.5; }
  .palette__swatch:hover { transform: none; }
  /* V3 exercise cards: no lift/slide when the viewer asks for less motion. */
  .ex-card__link { transition: none; }
  .ex-card__link:hover,
  .ex-card__link:focus-visible { transform: none; }
}

.grid__cursor-halo {
  fill: none;
  stroke: var(--cursor-halo);
  stroke-width: 5;
}

.grid__cursor {
  fill: none;
  stroke: var(--cursor);
  stroke-width: 2.5;
  stroke-dasharray: 5 3;
}

/* ---------- LOCKED CELLS (V3 exercises) ----------
   A padlock badge in the cell's top-left corner: the "you can't change this"
   metaphor a child reads at a glance. Shape-based, never colour-only, so it
   survives High contrast and colour-vision deficiency; each part is self-haloed
   (--lock over --lock-halo) so it reads on any cell colour, the way the axis and
   cursor overlays do. The badge sits in a corner so the cell's own colour — the
   thing the child has to mirror — stays visible. */
.grid__locks { pointer-events: none; } /* markers never block painting */

.grid__lock-halo {
  fill: var(--lock-halo);
  stroke: var(--lock-halo);
  stroke-width: 2.4;
  stroke-linejoin: round;
  stroke-linecap: round;
}
.grid__lock-shackle {
  fill: none;
  stroke: var(--lock);
  stroke-width: 1.3;
  stroke-linecap: round;
}
.grid__lock-body {
  fill: var(--lock);
  stroke: none;
}
.grid__lock-hole {
  fill: var(--lock-halo);
}

/* ---------- TUTORIAL TRANSFORMATION DEMO ---------- */
.grid__ghost {
  pointer-events: none; /* the ghost never intercepts anything */
}

.grid__ghost-cell {
  fill: var(--ghost-fill);
  stroke: var(--ghost);
  stroke-width: 2.5;
  paint-order: stroke fill; /* crisp outline over the faint fill */
}

/* Settle-to-endpoint on release. Motion only — disabled for reduced-motion and
   for Calm (sensory-sensitive), matching the near-miss animation policy. */
.grid__ghost.is-snapping {
  transition: transform 260ms ease;
}
@media (prefers-reduced-motion: reduce) {
  .grid__ghost.is-snapping { transition: none; }
}

/* The scrubber (pages 5, 7) lives in the right pane; the grid is in the centre. */
.transform-demo__controls {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}
.transform-demo__header {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 0.5rem;
}
.transform-demo__label {
  font-weight: 600;
}
.transform-demo__scrubber {
  width: 100%;
  min-height: var(--touch); /* keep the drag target comfortable */
}
.transform-demo__readout {
  color: var(--text-muted);
  font-variant-numeric: tabular-nums; /* the degree read-out doesn't jitter */
}

/* ---------- TUTORIAL PAGES (three-column shell, like the Canvas) ---------- */
/* The Canvas stacks grid-first on narrow screens; a tutorial reads better with
   the title and description first, then the grid, then the controls. */
/* On narrow screens the panes stack. Title/description live in the right pane
   now, so show that first, then the grid, then Back/Next + the page menu. */
@media (max-width: 1100px) {
  .app-container--tutorial .panel-right { order: 1; }
  .app-container--tutorial .workspace { order: 2; }
  /* align-items: stretch overrides the Canvas rule's flex-start, so the menu box
     spans the full pane width instead of shrinking to its content. */
  .app-container--tutorial .tutorial__info { order: 3; flex-direction: column; align-items: stretch; }
  /* Back on the left, Next on the right. */
  .app-container--tutorial .tutorial__topnav { justify-content: space-between; }
  .app-container--tutorial .tutorial__topnav .btn { flex: 0 0 auto; }
}

.tutorial__info { gap: 1rem; }

/* A touch tighter throughout the tutorial panes so the right pane's content
   stays compact (scoped — the Canvas keeps its roomier spacing). */
.app-container--tutorial .panel { padding: 1rem; }
.app-container--tutorial .panel-right { gap: 1rem; }

/* Title + description at the top of the right pane, above the page's controls. */
.tutorial__pageinfo {
  padding-bottom: 0.9rem;
  margin-bottom: 0.25rem;
  border-bottom: 1px solid var(--border-color);
}
.tutorial__title { margin: 0; font-size: 1.3rem; }
.tutorial__body p { margin: 0.4rem 0 0; }

/* Back / Next / Pages, pinned at the top of the left pane so the variable-length
   copy below never shifts them. */
.tutorial__topnav {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}
.tutorial__topnav .btn {
  flex: 1 1 auto;
  min-width: 0;
  text-align: center;
  padding-inline: 0.6rem;
  white-space: nowrap;
}
/* On the first/last page the unneeded button fades in place, rather than
   vanishing and leaving a gap. */
.tutorial__topnav .btn[aria-disabled="true"] {
  opacity: 0.35;
  pointer-events: none;
}

/* The page menu opens directly below the buttons (in flow — it pushes the title
   and description down rather than covering them). */
.tutorial__menu {
  border: 1px solid var(--border-color);
  border-radius: 8px;
  padding: 0.4rem;
  background: var(--bg-card);
}
.tutorial__menu-list {
  list-style: none;
  margin: 0.5rem 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
}
.tutorial__menu-link {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  padding: 0.35rem 0.5rem 0.35rem 1rem; /* every page shares this inset */
  border-radius: 6px;
  color: var(--text-muted);
  text-decoration: none;
  font-size: 0.9rem;
}
/* Session progress: a tick appears on pages seen this session. The span always
   holds its space, so the label doesn't shift when the tick shows. */
.tutorial__menu-tick { color: var(--accent-active); font-size: 0.85em; opacity: 0; }
.tutorial__menu-link.is-visited .tutorial__menu-tick { opacity: 1; }
.tutorial__menu-link:hover { background: var(--bg-card); color: var(--text-main); }
.tutorial__menu-link[aria-current="page"] {
  background: var(--bg-card);
  color: var(--text-main);
  font-weight: 600;
}
/* Symmetry-type sub-headings, each opening a section with a divider rule. */
.tutorial__menu-group {
  margin-top: 0.5rem;
  padding: 0.55rem 0.5rem 0.15rem;
  border-top: 1px solid var(--border-color);
  font-size: 0.7rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-muted);
}
/* A standalone page (the finale) that still begins a new block gets the rule too. */
.tutorial__menu-link.is-section-break {
  margin-top: 0.5rem;
  padding-top: 0.55rem;
  border-top: 1px solid var(--border-color);
}

/* Per-page controls stack in the right pane. */
.tutorial__controls { display: flex; flex-direction: column; gap: 0.85rem; }

/* Example gallery — buttons in a fixed two-column grid (2 side by side, 4 → 2×2),
   with a slightly smaller font + tighter padding so a two-word label
   ("Quarter turn") fits its half-pane cell on one line. */
.example-gallery { display: flex; flex-direction: column; gap: 0.85rem; }
.example-gallery__buttons {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.5rem;
}
.example-gallery__btn { font-size: 0.8rem; padding-inline: 0.35rem; }
.example-gallery__btn.is-selected {
  outline: 2px solid var(--accent-active);
  outline-offset: 1px;
}
.example-gallery__caption {
  color: var(--text-muted);
  margin: 0;
  min-height: 2.5em; /* reserve space so switching examples doesn't jump the pane */
}

/* Paint pages — guided / challenge controls in the right pane. */
/* Mirror-line dropdown (on the reflection paint pages). */
.paint-page__lines { display: flex; flex-direction: column; gap: 0.3rem; }
.paint-page__lines-label { font-weight: 600; }
.paint-page__lines .select { width: 100%; }
.paint-page__hint { margin: 0; color: var(--text-muted); }
.paint-page__hint p { margin: 0 0 0.6rem; }
.paint-page__hint p:last-child { margin-bottom: 0; }
/* Challenge heading + explanation, shown above the divider in challenge mode. */
.paint-page__challenge-heading { margin: 0 0 0.5rem; font-size: 1.05rem; font-weight: 600; }
.paint-page__challenge-intro { margin: 0 0 0.6rem; color: var(--text-muted); }
.paint-page__challenge-intro:last-child { margin-bottom: 0; }
.paint-page__palette { display: flex; gap: 0.75rem; }
.paint-page__swatch { width: var(--touch); height: var(--touch); }
.paint-page__result { margin: 0; min-height: 1.5em; }
.paint-page__result[data-state="pass"] { color: var(--accent-active); font-weight: 600; }
.paint-page__result[data-state="fail"],
.paint-page__result[data-state="hint"] { color: var(--text-muted); }

/* Staged builder (final page) — buttons stacked in the right pane. */
.staged-demo__controls { display: flex; flex-direction: column; gap: 0.6rem; }
/* "Start Again" reads as a quieter, outline reset — muted and set a little apart
   from the two filled build buttons above it. Token-based so it holds in every
   theme (not a .btn, so the themes' filled-button rules don't touch it). */
.staged-demo__reset {
  margin-top: 0.6rem;
  min-height: var(--touch);
  padding: 0 0.9rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: 1px solid var(--border-color);
  border-radius: 8px;
  color: var(--text-muted);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
  transition: color 0.12s, border-color 0.12s;
}
.staged-demo__reset:hover { color: var(--text-main); border-color: var(--text-muted); }
.staged-demo__reset:disabled { opacity: 0.4; cursor: default; }
/* The spinning/folding copy during a build step — decorative, never interactive.
   A translucent fill with a solid outline: see-through as it moves, but its
   square edges stay crisp. */
.staged-demo__ghost { pointer-events: none; }
.staged-demo__ghost rect {
  fill-opacity: 0.6;
  stroke: var(--cell-line);
  stroke-width: 1;
}
.btn--ghost { background: transparent; }

/* ---------- FOCUS & SCREEN READERS ---------- */
:where(button, select, input, summary, [tabindex]):focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
}

.grid:focus { outline: none; } /* the in-grid cursor ring is the indicator */
.grid:focus-visible { outline: 3px solid var(--focus); outline-offset: 3px; }

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

/* ---------- RESPONSIVE ----------
   The template is a fixed three-column desktop layout; tablets are the primary
   device for this age group (spec §6), so collapse to one scrolling column.
   Order on narrow screens: grid first, then controls, then results. */

/* Mid widths: two 300px sidebars would leave the grid cramped, so tighten them
   before the layout collapses. */
@media (min-width: 1101px) and (max-width: 1400px) {
  /* Left stays wide enough for 5 swatches (5×44 + 4×8 + 24 padding = 276);
     the right panel is only text, so it gives up more room to the grid. */
  .app-container { grid-template-columns: 280px minmax(0, 1fr) 250px; }
  .panel { padding: 0.75rem; gap: 1.25rem; }
  .workspace { padding: 1rem; }
}

@media (max-width: 1100px) {
  /* Release the desktop height lock — the whole page scrolls here instead.
     `visible`, not `auto`: see the note on body.page. `auto` would make <body>
     a scroll container and break background propagation, showing a black band
     under the fixed theme gradients while scrolling on a phone. */
  body { height: auto; min-height: 100vh; overflow: visible; }

  .app-container {
    /* minmax(0, 1fr), not 1fr: a plain `1fr` track has an automatic min-content
       minimum, so one stubborn child widens the whole page. */
    grid-template-columns: minmax(0, 1fr);
    height: auto;
  }

  .panel-left,
  .panel-right {
    border: none;
    border-top: 1px solid var(--border-color);
  }

  .workspace { order: 1; padding: 1rem; }
  .panel-left { order: 2; }
  .panel-right { order: 3; }

  .panel { overflow-y: visible; }

  /* Stacked layout: move the exercise instruction above the grid. Show the
     in-workspace copy, hide the right-panel one, and stack the workspace so the
     instruction sits over the board rather than beside it. (No effect on the
     Canvas/tutorial, whose workspace holds only the board.) */
  .workspace { flex-direction: column; }
  /* `.workspace` prefix, not a bare class: the base `.ex-play__instruction`
     { display: none } is later in the file, so this needs higher specificity to
     win when the query matches (and still lose on desktop, where it doesn't). */
  .workspace .ex-play__instruction {
    display: block;
    margin: 0 0 0.75rem;
    max-width: 42ch;
    /* Opaque panel surface, not the bare workspace: the workspace is transparent
       over the vivid background gradient, where white text drops to ~2:1 on the
       bright spots. --bg-panel is the app's audited text surface in every theme. */
    padding: 0.5rem 0.95rem;
    background: var(--bg-panel);
    border: 1px solid var(--border-color);
    border-radius: 10px;
    font-size: 1.1rem;
    font-weight: 700;
    color: var(--text-main);
    text-align: center;
  }
  #exTarget { display: none; }

  /* Keep the grid and the palette on screen together — a child shouldn't have
     to scroll between painting and picking a colour. */
  .board { width: min(100%, 48vh); }

  /* Controls read better side by side on a tablet's width */
  .panel-left { flex-direction: row; flex-wrap: wrap; align-items: flex-start; }
  .panel-left > section { flex: 1 1 240px; }
  .button-stack { flex-direction: row; flex-wrap: wrap; }
  .button-stack .btn { flex: 1 1 auto; }
}

@media (max-width: 560px) {
  .top-bar { padding: 0.5rem 0.75rem; }
  .brand { font-size: 1.1rem; }
  /* Give the squares as much of a phone screen as possible, and buy slack so
     Android font scaling can't inflate the padding past the screen width. */
  .workspace { padding: 0.5rem; }
  .board { padding: 0.25rem; }
  .panel { padding: 0.75rem; gap: 1rem; }
  .panel-left { flex-direction: column; }
  /* Back to natural heights — the tablet rule's 240px basis becomes a *height*
     basis once the panel is a column, which stretched the sections apart. */
  .panel-left > section { flex: 0 0 auto; width: 100%; }
  .palette-grid { grid-template-columns: repeat(5, 1fr); }
}

/* ================================================================
   EXERCISES (V3)
   The browser is a full-width card grid; the play screen reuses the
   Canvas's three-column .app-container, so it needs no new layout.
   ================================================================ */

.layout--full {
  max-width: 1100px;
  margin-inline: auto;
  padding: 1.5rem 1.25rem 2.5rem;
}

.ex-browser__intro { margin-bottom: 1.5rem; }
.ex-browser__title { margin: 0 0 0.4rem; font-size: 1.6rem; }
.ex-browser__lead { margin: 0 0 0.5rem; color: var(--text-muted); max-width: 60ch; }
.ex-browser__progress {
  margin: 0;
  font-weight: 700;
  color: var(--accent-active);
}
.ex-browser__error { color: var(--text-muted); }

/* Card grid — auto-fill so it reflows from one column on a phone to several on
   a desktop. minmax(0,…) per the layout GOTCHA so a wide child can't stretch it. */
.ex-cards {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(220px, 100%), 1fr));
  gap: 1.1rem;
}

.ex-card { display: flex; }

.ex-card__link {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  width: 100%;
  padding: 0.9rem;
  background: var(--bg-card);
  border: 1px solid var(--border-color);
  border-radius: 12px;
  text-decoration: none;
  color: var(--text-main);
  transition: transform 0.1s, box-shadow 0.12s, border-color 0.12s;
}
.ex-card__link:hover,
.ex-card__link:focus-visible {
  transform: translateY(-2px);
  border-color: var(--accent-active);
  box-shadow: 0 10px 24px rgba(0, 0, 0, 0.28);
}

.ex-card__thumb {
  aspect-ratio: 1;
  max-width: 150px;
  margin-inline: auto;
  width: 100%;
}

.ex-card__meta { display: flex; flex-direction: column; gap: 0.2rem; }
.ex-card__difficulty {
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.ex-card__name {
  margin: 0;
  font-size: 1.05rem;
  display: flex;
  align-items: center;
  gap: 0.4rem;
}
.ex-card__tick {
  color: var(--accent-active);
  font-weight: 700;
}
.ex-card__target { margin: 0; font-size: 0.85rem; color: var(--text-muted); }

.ex-card__link.is-done { border-color: var(--accent-active); }

/* ---- Play screen extras (the rest reuses .panel / .results-card) ---- */
.ex-play__back { align-self: flex-start; }

/* The above-the-grid instruction — hidden on desktop (the right panel carries it
   there); the stacked-layout media query below reveals it and hides the panel
   copy, so the task is the first thing a child reads on a phone or tablet. */
.ex-play__instruction { display: none; }

.ex-play__hint {
  font-size: 0.9rem;
}
.ex-play__hint summary {
  cursor: pointer;
  font-weight: 600;
  color: var(--text-muted);
  /* A child taps this — keep it a full 44px target. Padding grows the hit area
     without display:flex, which would drop the native disclosure triangle. */
  min-height: var(--touch);
  padding: 0.6rem 0;
}
.ex-play__hint p {
  margin: 0.5rem 0 0;
  color: var(--text-muted);
}

.ex-play__hint-do { margin-top: 0.6rem; }

.ex-play__hint-note {
  margin: 0.4rem 0 0;
  font-size: 0.85rem;
  color: var(--text-muted);
}
.ex-play__hint-note:empty { display: none; }

.ex-play__after {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
  margin-top: 1rem;
}
