/* =========================================================================
   Natteroo — Soft pastel

   The eight game colours are fixed, saturated and loud. Everything around
   them stays out of their way: a mint ground, white cards lifted by soft
   shadows rather than drawn with outlines, sky-blue edges where an edge is
   needed, and coral for the ONE primary action on a screen. Butter and
   peach are tints, never buttons. No black or dark outline on anything —
   elevation is a shadow, an edge is 1–2px of sky. (The rebrand to Natteroo,
   from the "Soft pastel" direction on the design canvas — see
   docs/STRATEGY.md.)

   Two typefaces: Quicksand 600/700 for headings, buttons and the wordmark;
   Lexend 300–700 for everything read — drawn to reduce visual stress for
   developing readers, which is most of who uses this. Both from Google
   Fonts by every page's <head> (a local() @font-face only ever worked for
   a visitor who had it installed).
   ========================================================================= */

:root {
  /* Surface — mint, plain, no pattern anywhere. Work happens on white. */
  --paper: #e9f6ef;
  --card: #ffffff;
  --card-raised: #ffffff;
  --card-sunk: #f4faf7;
  --line: #d5ecfb;
  --line-strong: #b9dcf3;

  /* Ink */
  --ink: #2a3a34;
  --ink-soft: #5f7069;
  --ink-faint: #8a9a93;

  /* Brand: coral is the one primary-action colour; the tint marks what is
     selected; the dark is hover. --accent is the same thing under the
     name the game screens have always used. */
  --brand: #f08a7e;
  --brand-dark: #d9736a;
  --brand-tint: #fdece9;
  --accent: var(--brand);
  --accent-tint: var(--brand-tint);

  /* Supporting tints */
  --sky: #d5ecfb;
  --sky-strong: #7fc8f8;
  --butter: #f2c94c;
  --peach: #ffe7b8;
  --warn: #9a6b12;
  --warn-tint: var(--peach);

  /* The turn prompt's two pill colours only — a vivid, unmistakable green
     and red nothing else in the interface uses, deliberately more
     saturated than the app's own calmer palette so the two read as an
     unambiguous "go" and "stop". Game mechanics, not brand: unchanged by
     the rebrand. */
  --go: #2e9e52;
  --stop: #d64545;
  --danger: #b3443a;
  --danger-tint: #fdece9;

  /* The pale plaque every picture sits on, so a dark drawing still reads
     on the black square. */
  --plaque: #ffffff;

  /* The back of a Pairs card. Playing-card red, deliberately not one of the
     game colours above — a board of sixteen of these is the most of any one
     colour a child sees in this app, and it should not be a colour that
     means something else somewhere. */
  --pairs-back: #a8232b;

  /* Game colours (mirrored from lib/game.js) — game mechanics, not brand */
  --sq-red: #e4443c;
  --sq-yellow: #f7c948;
  --sq-green: #2e9e52;
  --sq-blue: #2f6fd0;
  --sq-pink: #f075a8;
  --sq-purple: #7a4cc0;
  --sq-black: #20262f;
  --sq-white: #ffffff;

  --r-sm: 12px;
  --r-md: 16px;
  --r-lg: 22px;
  --r-xl: 28px;

  /* Soft and low, in the ink's own hue. Elevation is all the edge a card
     gets. */
  --shadow-sm: 0 4px 10px rgba(42, 58, 52, 0.06);
  --shadow-md: 0 8px 18px rgba(42, 58, 52, 0.08);
  --shadow-lg: 0 12px 28px rgba(42, 58, 52, 0.10);
  --shadow-brand: 0 8px 18px rgba(240, 138, 126, 0.35);

  --font: 'Lexend', ui-rounded, -apple-system, BlinkMacSystemFont, 'Segoe UI',
    'Trebuchet MS', system-ui, sans-serif;
  --font-display: 'Quicksand', var(--font);

  --tap: 56px;

  /* ---------------------------------------------------------------------
     The rows of the child's screen.

     Every one of these is fixed, and set from the VIEWPORT rather than from
     what happens to be inside it. That is the whole point: the board takes
     what is left over, so if any of its neighbours could grow — a message
     rewrapping around a longer name — every coloured square would shift
     under a child's finger mid-placement.
     --------------------------------------------------------------------- */
  --turnprompt-h: clamp(40px, 5.5vh, 56px);
  --foot-h: clamp(46px, 7vh, 62px);
  --tile-w: clamp(74px, 10.5vw, 108px);
  --tray-h: calc(var(--tile-w) * 1.12 + 24px);
  --game-gap: clamp(8px, 1.6vh, 16px);
  --game-pad-y: clamp(10px, 2vh, 18px);

  /* What is left for the board once every fixed row is taken out. Known in
     advance, from the viewport alone — never from anything on the screen.
     Four fixed rows now sit inside #screen-game itself (prompt, board,
     tray, foot), so three gaps come out rather than two. */
  --board-h: calc(100dvh - var(--turnprompt-h) - var(--tray-h)
    - var(--foot-h) - 3 * var(--game-gap) - 2 * var(--game-pad-y));
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  min-height: 100%;
}

body {
  font-family: var(--font);
  background: var(--paper);
  color: var(--ink);
  font-size: 17px;
  line-height: 1.45;
  -webkit-font-smoothing: antialiased;
  -webkit-text-size-adjust: 100%;
  overscroll-behavior: none;
}

/* Kids tap fast and repeatedly; kill the blue flash and the double-tap zoom. */
button, .tappable {
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
}

h1, h2, h3 { font-family: var(--font-display); line-height: 1.2; margin: 0 0 0.4em; letter-spacing: -0.01em; }
h1 { font-size: clamp(1.6rem, 3.2vw, 2.3rem); font-weight: 700; }
h2 { font-size: clamp(1.25rem, 2.2vw, 1.55rem); font-weight: 700; }
h3 { font-size: 1.05rem; font-weight: 600; }
p { margin: 0 0 0.8em; }
a { color: var(--brand); }

.muted { color: var(--ink-soft); }
.tiny { font-size: 0.83rem; }

/* Text sitting directly on the page ground, outside any card, on its own
   little plate — kept from the patterned days for the places that use it;
   on mint it is simply a white card. */
.on-paper {
  display: inline-block;
  background: var(--card);
  border-radius: var(--r-md);
  padding: 10px 18px;
  box-shadow: var(--shadow-sm);
}

/* ======================================================================
   The front door — mint, two soft blobs behind the content (see
   .page-blobs), the wordmark large, four real tiles, a footer.
   ====================================================================== */

.landing { width: min(1180px, 100%); margin: 0 auto; padding: clamp(28px, 6vh, 72px) clamp(14px, 3vw, 28px) 40px; }
/* The hero is one centred column: the wordmark, the eyebrow, the line,
   the two buttons, the how-are-you-playing pills. */
.landing-hero { display: grid; justify-items: center; text-align: center; gap: 18px; max-width: 760px; margin: 0 auto 40px; }
.eyebrow { margin: 0; font-size: 0.82rem; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--brand); }
.landing-hero h1 { font-size: clamp(2.6rem, 6vw, 4.5rem); font-weight: 700; line-height: 1; margin: 0; letter-spacing: -0.01em; text-wrap: balance; }
.landing-sub { font-size: clamp(1.05rem, 1.6vw, 1.2rem); line-height: 1.5; color: var(--ink-soft); margin: 0; max-width: 32em; text-wrap: pretty; }
.landing-actions { display: flex; gap: 12px; flex-wrap: wrap; justify-content: center; margin: 6px 0 0; }
.landing-how { display: grid; gap: 10px; justify-items: center; padding-top: 22px; margin-top: 8px; border-top: 1px solid var(--line); width: min(100%, 520px); }
.landing-toggles { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; }
/* The pills: white, lifted, no border; coral on hover. */
.pill-toggle {
  display: inline-flex; align-items: center; gap: 8px; padding: 10px 16px; border-radius: 999px;
  border: 0; background: var(--card); box-shadow: var(--shadow-sm); color: var(--ink); text-decoration: none; font-weight: 600; font-size: 0.92rem;
}
.pill-toggle:hover { color: var(--brand-dark); box-shadow: var(--shadow-md); }
.pill-toggle svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
/* Four real tiles in a row, the console's own tile. */
.landing-tiles { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 16px; }
.landing-tile { text-decoration: none; color: var(--ink); }
.landing-tile .game-card { cursor: pointer; }
.landing-header .btn { padding: 9px 18px; font-size: 0.95rem; min-height: 44px; }
.landing-header .btn.quiet { padding: 9px 10px; }
/* The join card under the tiles: a drawn code and QR on a sunk plate,
   one line beside them. */
.landing-join { display: grid; grid-template-columns: auto 1fr; gap: 18px; align-items: center; padding: 16px 20px; margin-top: 18px; }
.landing-join p { margin: 0; font-size: 0.95rem; line-height: 1.45; color: var(--ink-soft); }
.landing-join strong { color: var(--ink); }
.landing-join-code { display: grid; gap: 6px; justify-items: center; padding: 10px 14px; border-radius: var(--r-md); background: var(--card-sunk); }
.landing-join-code b { font-family: var(--font-display); font-size: 1.5rem; letter-spacing: 0.12em; font-variant-numeric: tabular-nums; color: var(--brand); }
.landing-join-qr {
  width: 44px; height: 44px; border-radius: 4px;
  background:
    linear-gradient(var(--ink), var(--ink)) 2px 2px / 12px 12px no-repeat,
    linear-gradient(var(--ink), var(--ink)) 30px 2px / 12px 12px no-repeat,
    linear-gradient(var(--ink), var(--ink)) 2px 30px / 12px 12px no-repeat,
    repeating-linear-gradient(90deg, var(--ink) 0 4px, transparent 4px 8px) 18px 18px / 24px 4px no-repeat,
    repeating-linear-gradient(0deg, var(--ink) 0 4px, transparent 4px 8px) 30px 20px / 4px 22px no-repeat,
    #fff;
}
.site-footer {
  position: relative; z-index: 1;
  display: flex; align-items: center; gap: 16px; flex-wrap: wrap; justify-content: space-between;
  width: min(1180px, 100%); margin: 0 auto; padding: 18px clamp(14px, 3vw, 28px) 28px;
  color: var(--ink-soft); font-size: 0.85rem; border-top: 1px solid var(--line);
}
.site-footer nav { display: flex; gap: 20px; }
.site-footer a { color: var(--ink-soft); font-weight: 500; text-decoration: none; }
.site-footer a:hover { color: var(--brand-dark); text-decoration: underline; text-underline-offset: 3px; }
/* The footer sits at the foot: a page with one is a column, its main
   part taking the height. */
body:has(> .site-footer) { min-height: 100dvh; display: flex; flex-direction: column; }
body:has(> .site-footer) > .wrap, body:has(> .site-footer) > .landing { flex: 1 0 auto; width: min(1180px, 100%); }
body:has(> .site-footer) > .site-footer { margin-top: auto; }
/* On the console the footer shows under the picker and the account pages
   only — a live game screen is built to fit the screen, and a footer would
   be the one thing pushing it into a scroll. */
body:has(> .wrap) > .site-footer { display: none; }
body:has(> .wrap > #screen-games:not(.hidden)) > .site-footer,
body:has(> .wrap > #screen-classes:not(.hidden)) > .site-footer,
body:has(> .wrap > #screen-picture-sets:not(.hidden)) > .site-footer,
body:has(> .wrap > #screen-guide:not(.hidden)) > .site-footer,
body:has(> .wrap > #screen-auth:not(.hidden)) > .site-footer,
body.printables-page > .site-footer, body.homework-page > .site-footer { display: flex; }
body.about-page > .site-footer { display: flex; }
/* /about: the story beside the cartoon, then privacy and contact. */
.about-wrap { max-width: 56rem; display: grid; gap: 18px; }
.about-card { display: grid; grid-template-columns: minmax(120px, 180px) minmax(0, 1fr); gap: 24px; align-items: start; padding: 28px; }
.about-card.is-text { grid-template-columns: 1fr; }
.about-adam { width: 100%; height: auto; filter: drop-shadow(0 6px 12px rgba(15, 40, 38, 0.18)); }
.about-card h1 { margin-bottom: 12px; }
@media (max-width: 600px) { .about-card { grid-template-columns: 1fr; justify-items: center; text-align: left; } .about-adam { width: 140px; } }
@media (max-width: 860px) {
  .landing-tiles { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 480px) {
  .landing-tiles { gap: 10px; }
  .landing-tile .game-logo-box { height: 84px; }
  .landing-tile .game-logo { max-height: 84px; }
  .landing-actions { width: 100%; }
  .landing-actions .btn.big { width: 100%; }
  .landing-join { grid-template-columns: 1fr; justify-items: center; text-align: center; }
}

/* A quiet way back, now that there is no header bar to click on. */
.link-back {
  display: inline-flex; align-items: center; gap: 6px;
  font-weight: 600; font-size: 0.92rem; color: var(--ink);
  text-decoration: none; padding: 6px 2px;
}
.link-back:hover { color: var(--brand-dark); }

/* A thin control strip — a set-up page's Back · title · Start bar, a live
   screen's logo · buttons · QR bar. White, a sky edge, a soft shadow; the
   sticky ones (`.is-sticky`) blur what scrolls under them. */
.topline {
  background: rgba(255, 255, 255, 0.85);
  -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  padding: 10px 16px;
  box-shadow: var(--shadow-sm);
}

.center { text-align: center; }
.hidden { display: none !important; }
.stack { display: grid; gap: 14px; }
.row { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
.spacer { flex: 1; }

.wrap {
  width: min(1180px, 100%);
  margin: 0 auto;
  padding: 20px clamp(14px, 3vw, 28px) 64px;
}

/* ----------------------------------------------------------- the wordmark */

/* The logo is the wordmark: "natteroo" in Quicksand 700, lowercase, in
   three colours — nat coral, te butter, roo sky. As live text wherever a
   font can be relied on (`<span class="wordmark"><span>nat</span><span>te
   </span><span>roo</span></span>`); as paths in public/images/brand/
   wordmark.svg everywhere else (favicons, the OG image, PDFs — built by
   scripts/build-brand.mjs). Size it with font-size. */
.wordmark {
  display: inline-block; font-family: var(--font-display); font-weight: 700;
  letter-spacing: -0.01em; line-height: 1; white-space: nowrap; text-decoration: none;
}
.wordmark > span:nth-child(1) { color: var(--brand); }
.wordmark > span:nth-child(2) { color: var(--butter); }
.wordmark > span:nth-child(3) { color: var(--sky-strong); }
.wordmark.is-hero { font-size: clamp(2.6rem, 6vw, 4rem); text-shadow: 0 10px 20px rgba(42, 58, 52, 0.12); }

/* The mark: a coral rounded square (radius 30% of the side) with a white
   lowercase n. The child's screens carry it as their only brand; the
   favicons are the same thing from public/images/brand/mark.svg. Size it
   with --side. */
.n-mark {
  --side: 72px;
  display: inline-grid; place-items: center; width: var(--side); height: var(--side);
  border-radius: 30%; background: var(--brand); color: #fff;
  font-family: var(--font-display); font-weight: 700; font-size: calc(var(--side) * 0.56); line-height: 1;
  box-shadow: 0 10px 22px rgba(240, 138, 126, 0.35); text-decoration: none; user-select: none;
}

/* Two large soft blobs behind the content — the front door and the
   console only, never a set-up page, a form or anything a child sees.
   Peach on the left, sky on the right, at 70%, clipped by the page. */
.page-blobs { position: fixed; inset: 0; z-index: 0; pointer-events: none; overflow: hidden; }
.page-blobs::before, .page-blobs::after { content: ''; position: absolute; opacity: 0.7; }
.page-blobs::before { left: -140px; bottom: -120px; width: 440px; height: 440px; border-radius: 42% 58% 55% 45%; background: var(--peach); }
.page-blobs::after { right: -160px; top: 100px; width: 480px; height: 480px; border-radius: 55% 45% 40% 60%; background: var(--sky); }
body:has(> .page-blobs) > .wrap, body:has(> .page-blobs) > .landing { position: relative; z-index: 1; }
/* On the console the blobs sit behind the picker only — never a set-up
   page, a form or a live screen. */
body:has(> .wrap) > .page-blobs { display: none; }
body:has(> .wrap > #screen-games:not(.hidden)) > .page-blobs { display: block; }

/* ---------------------------------------------------------------- buttons */

.btn {
  appearance: none;
  border: 0;
  border-radius: var(--r-md);
  background: var(--brand);
  color: #fff;
  text-decoration: none;
  font: inherit;
  font-family: var(--font-display);
  font-weight: 700;
  padding: 13px 24px;
  min-height: var(--tap);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 9px;
  transition: transform 0.12s ease, box-shadow 0.15s ease, background 0.15s ease;
  box-shadow: var(--shadow-brand);
}
.btn:hover { background: var(--brand-dark); }
.btn:active { transform: translateY(1px) scale(0.99); }
.btn:disabled { opacity: 0.42; cursor: not-allowed; transform: none; }
.btn:focus-visible { outline: 3px solid var(--sky-strong); outline-offset: 3px; }

/* Three buttons (Soft pastel): primary — coral, white text, the coral
   shadow, and the ONE on a screen (.btn and .btn.go are the same thing);
   secondary — white, ink text, a soft shadow, no border (.ghost and
   .secondary are the same thing); and .quiet, a text link that underlines
   on hover. Nothing here has an outline. */
.btn.ghost, .btn.secondary { background: var(--card); color: var(--ink); box-shadow: var(--shadow-sm); }
.btn.ghost:hover, .btn.secondary:hover { background: var(--card); color: var(--brand-dark); box-shadow: var(--shadow-md); }
.btn.quiet { background: transparent; color: var(--ink); box-shadow: none; padding: 10px 14px; min-height: 44px; }
.btn.quiet:hover { background: transparent; color: var(--ink); text-decoration: underline; text-underline-offset: 3px; }
.btn.go { background: var(--brand); }
.btn.go:hover { background: var(--brand-dark); }
.btn.danger { background: var(--danger); box-shadow: var(--shadow-sm); }
.btn.big { font-size: 1.25rem; padding: 18px 34px; min-height: 68px; border-radius: var(--r-lg); }
.btn.wide { width: 100%; }

/* (The front door's four coloured .btn.role doors went with the rebrand.) */

/* ------------------------------------------------------------------ cards */

.card {
  background: var(--card);
  border: 0;
  border-radius: var(--r-lg);
  padding: clamp(16px, 2.4vw, 26px);
  box-shadow: var(--shadow-lg);
}
.card + .card { margin-top: 16px; }

/* The sign-in card — the wordmark, the tagline, two fields. */
.auth-card { max-width: 26rem; margin: 0 auto; text-align: center; }
.auth-card .wordmark { font-size: 2.4rem; margin: 6px 0 4px; }
/* The sign-in card sits in the middle of the screen (the rebrand), not
   pinned to the top with empty ground below. */
.wrap:has(> #screen-auth:not(.hidden)) { display: grid; align-content: center; }
.auth-card .stack { text-align: left; margin-top: 22px; }
.auth-mark {
  width: 60px; height: 60px; margin: 0 auto 16px; display: grid;
  grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; gap: 6px;
  padding: 8px; background: var(--card-sunk); border-radius: 30%;
}
.auth-mark span { border-radius: 30%; display: block; }
.auth-title { margin: 0; font-size: clamp(1.4rem, 4vw, 1.7rem); }
.auth-links { text-align: center; margin: 14px 0 0; }
.card-head { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; margin-bottom: 14px; }
.card-head h2 { margin: 0; }

.pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 12px;
  border-radius: 999px;
  background: var(--brand-tint);
  color: var(--brand-dark);
  font-size: 0.8rem;
  font-weight: 700;
}
.pill.go { background: #e3f3e8; color: #1f6b3a; }
.pill.warn { background: var(--warn-tint); color: var(--warn); }
.pill.off { background: var(--card-sunk); color: var(--ink-faint); }

/* ----------------------------------------------------------------- inputs */

label.field { display: block; font-weight: 700; font-size: 0.9rem; margin-bottom: 6px; color: var(--ink-soft); }

input[type='text'], input[type='number'], input[type='password'], input[type='email'],
input[type='search'], textarea, select {
  width: 100%;
  font: inherit;
  color: var(--ink);
  background: var(--card);
  border: 2px solid var(--line);
  border-radius: var(--r-md);
  padding: 12px 14px;
  min-height: 52px;
}
textarea { min-height: 120px; resize: vertical; line-height: 1.6; }
/* A native select dressed like the other inputs (the rebrand): the browser's
   own arrow replaced by a chevron in the padding. */
select {
  appearance: none; -webkit-appearance: none; padding-right: 40px; cursor: pointer;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3E%3Cpath d='M4 6l4 4 4-4' fill='none' stroke='%235f7069' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 14px center;
}
.field-help { margin: -4px 0 12px; }
/* A small delete: coral-tinted, never outlined. */
.btn.danger.tiny { background: var(--danger-tint); color: var(--danger); box-shadow: none; }
.btn.danger.tiny:hover { background: var(--danger); color: #fff; }
input:focus, textarea:focus, select:focus { outline: none; border-color: var(--brand); box-shadow: 0 0 0 4px var(--brand-tint); }

.switch { display: flex; align-items: center; gap: 12px; padding: 12px 14px; border-radius: var(--r-md); background: var(--card-sunk); cursor: pointer; }
.switch input { width: 22px; height: 22px; accent-color: var(--brand); flex: none; }
.switch span { font-weight: 650; }
.switch small { display: block; font-weight: 400; color: var(--ink-soft); font-size: 0.82rem; }

.seg { display: inline-flex; background: var(--card-sunk); border-radius: 999px; padding: 4px; gap: 4px; }
.seg button {
  appearance: none; border: none; background: transparent; font: inherit; font-weight: 700;
  padding: 9px 18px; border-radius: 999px; cursor: pointer; color: var(--ink-soft); min-height: 44px;
}
.seg button[aria-pressed='true'] { background: var(--card); color: var(--ink); box-shadow: var(--shadow-sm); }

/* --------------------------------------------------------------- messages */

.notice {
  border-radius: var(--r-md);
  padding: 12px 16px;
  font-weight: 600;
  border: 0;
}
.notice.info { background: var(--sky); color: var(--ink); }
.notice.warn { background: var(--warn-tint); color: var(--warn); }
.notice.bad { background: var(--danger-tint); color: var(--danger); }
.notice.good { background: #e3f3e8; color: #1f6b3a; }

/* ================================================================ THE GAME */

/**
 * The child's screen is a grid of FIXED rows, not a stack of flexible ones.
 *
 * This is the fix for squares that jumped under a child's finger. The board
 * takes the space its neighbours leave, so while any neighbour could grow —
 * the sentence frame gaining a word, the turn bar rewrapping around a longer
 * name, the finish button appearing — the whole board resized mid-placement.
 * Every other row now has a height set from the viewport, so the board's
 * geometry cannot depend on what anybody says or does.
 */
.game-body {
  background: var(--paper);
}

/**
 * The live game only: pinned to the exact viewport, nothing scrolls, so the
 * felt board's geometry can never move under a child's finger — see the
 * comment above #screen-game. This used to be true of every screen, on the
 * theory that a fixed body gives iOS nothing to scroll when the keyboard
 * opens under a focused field. It over-reached: on real devices, a fixed,
 * non-scrolling page combined with iOS's own keyboard/viewport handling
 * produced exactly the clipped, half-missing joining screens it was meant
 * to prevent — and the CSS Grid + `:has()` rebuild that followed, despite
 * checking out in every simulation available while building it, made a
 * real iPad render one of those screens as almost nothing at all: no
 * card, no field, just a sliver of a rounded corner. Two comparatively
 * recent CSS features stacked on top of each other were involved either
 * way — `:has()` choosing the layout, `dvh` sizing it — and simulation
 * cannot be trusted to say whether a given device supports either. So
 * both are gone from this decision. Which layout applies now turns on a
 * plain class, ".is-live", that play.js sets and clears itself in
 * `show()` — an ordinary class selector, which has worked the same way
 * in every browser for over twenty years — rather than asking the CSS
 * engine to infer it from a sibling element's state.
 */
.game-body.is-live {
  position: fixed;
  inset: 0;
  height: 100vh;  /* the plain fallback, in case "dvh" itself is the */
  height: 100dvh; /* unsupported piece — the safer line goes second. */
  overflow: hidden;
  display: flex;
  flex-direction: column;
}
/* #screen-game is the only visible child here; it fills the space itself
   rather than the body needing a second grid row for it. */
.game-body.is-live > .game-main { flex: 1; min-height: 0; }

/**
 * The joining screens (code / name / lobby): the plainest possible page.
 * No CSS Grid, no fixed positioning, nothing keyed to a viewport unit
 * newer than "vh". Just ordinary block content that scrolls the ordinary
 * way if it ever needs to, the way every other page on the web does.
 */
.game-body:not(.is-live) {
  min-height: 100vh;
  min-height: 100dvh;
}

/**
 * There is no turn order any more, so this line no longer tries to referee
 * one — the children work out between themselves, out loud, whose go it
 * is. What's left is a single steady line above the board saying who's on
 * this team: "You are on a team with William". It doesn't change while the
 * game is being played, so it never needs the old go/stop colour coding —
 * one calm, friendly fill throughout.
 *
 * The text sits on its own solid pill rather than directly on the paper
 * background — plain coloured text on that pattern nearly vanished on a
 * real screen. A solid fill behind it is legible regardless of what's
 * behind it, without bringing back a full-width bar.
 */
.turn-prompt {
  height: var(--turnprompt-h);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 clamp(12px, 2.5vw, 22px);
}
.turn-prompt span {
  display: inline-flex;
  align-items: center;
  max-width: 100%;
  padding: 8px 22px;
  border-radius: 999px;
  font-weight: 800;
  font-size: clamp(1.05rem, 3.2vw, 1.5rem);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  background: var(--ink-faint);
  color: #fff;
  box-shadow: var(--shadow-sm);
}
.turn-prompt span.team { background: var(--accent); }

/* The connection pill still needs somewhere to live now the bar is gone —
   tucked in the corner rather than given a row of its own, since it only
   ever matters when it has bad news to deliver. */
.conn-corner {
  position: absolute;
  top: max(10px, env(safe-area-inset-top));
  right: max(12px, env(safe-area-inset-right));
}

/* The live game's own exit, opposite the connection pill — small and quiet
   so it never competes with the board for a child's attention, but always
   there, unlike the auto-resume this app used to trap a child behind. */
.leave-corner {
  position: absolute;
  top: max(10px, env(safe-area-inset-top));
  left: max(12px, env(safe-area-inset-left));
  padding: 6px 12px;
  min-height: 0;
  font-size: 0.78rem;
  background: rgba(255, 255, 255, 0.75);
  border-radius: 999px;
}

.game-main {
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: var(--game-gap);
  padding: var(--game-pad-y) clamp(12px, 2.5vw, 22px);
}

/**
 * The playing screen only. Four rows: the board takes what is left, and the
 * other three — the turn prompt above it, the tray and the foot below it —
 * are pinned, so no amount of text can move a coloured square. Positioned,
 * so the connection pill (.conn-corner) has something to sit relative to.
 */
#screen-game {
  position: relative;
  display: grid;
  grid-template-rows: var(--turnprompt-h) 1fr var(--tray-h) var(--foot-h);
  gap: var(--game-gap);
  min-height: 0;
}

/* --------------------------------------------------------------- the grid */

/**
 * Rows are declared explicitly, and every one is 1fr.
 *
 * Without this, the rows are sized by whatever is inside them — so the moment
 * a picture landed on a square, that row grew and the whole board jumped.
 * The squares now take their size from the board, never from their contents.
 */
.board {
  min-height: 0;
  min-width: 0;
  display: grid;
  gap: clamp(8px, 1.4vw, 14px);
  grid-template-columns: repeat(2, 1fr);
  grid-auto-rows: 1fr;
}
.board[data-squares='4'] { grid-template-rows: repeat(2, 1fr); }
.board[data-squares='6'] { grid-template-rows: repeat(3, 1fr); }
.board[data-squares='8'] { grid-template-rows: repeat(4, 1fr); }

@media (min-width: 700px) {
  .board { grid-template-columns: repeat(4, 1fr); }
  /* Four squares read better as a 2×2 block than as one wide strip. */
  .board[data-squares='4'] { grid-template-columns: repeat(2, 1fr); grid-template-rows: repeat(2, 1fr); }
  .board[data-squares='6'] { grid-template-columns: repeat(3, 1fr); grid-template-rows: repeat(2, 1fr); }
  .board[data-squares='8'] { grid-template-rows: repeat(2, 1fr); }
}

.square {
  position: relative;
  border-radius: var(--r-lg);
  border: none;
  display: flex;
  align-items: center;
  justify-content: center;
  /* Both zeros matter: they stop the contents setting the cell's size. */
  min-height: 0;
  min-width: 0;
  cursor: pointer;
  overflow: hidden;
  transition: box-shadow 0.15s ease, outline-color 0.15s;
  outline: 4px solid transparent;
  outline-offset: 3px;
  /* Pressed in, not sitting on top: a light lip along the top edge and a
     shadow along the bottom, as felt behaves. */
  box-shadow:
    inset 0 3px 0 rgba(255, 255, 255, 0.22),
    inset 0 -5px 0 rgba(0, 0, 0, 0.13);
  /* The board never scrolls, so a finger on a square is always a drag. */
  touch-action: none;
}
.square[data-colour='white'],
.square[data-colour='yellow'] {
  box-shadow:
    inset 0 3px 0 rgba(255, 255, 255, 0.55),
    inset 0 -5px 0 rgba(0, 0, 0, 0.08);
}
.square[data-colour='white'] { box-shadow: inset 0 0 0 1px var(--line-strong),
  inset 0 -5px 0 rgba(0, 0, 0, 0.07); }
.square.droppable { outline-color: rgba(18, 160, 138, 0.55); animation: nudge 1.8s ease-in-out infinite; }
.square.drop-target {
  outline-color: var(--accent);
  outline-width: 6px;
  animation: none;
  box-shadow: inset 0 0 0 3px rgba(255, 255, 255, 0.65), inset 0 -4px 0 rgba(0, 0, 0, 0.08);
}
.square.locked { cursor: default; }

/* Class-mode result marking, once every board has been checked against the
   leader's — the same visual language as the team-mode mini-board result
   (.mini.miss / .mini .tick), scaled up to the full-size interactive board. */
.square.correct { box-shadow: inset 0 0 0 4px var(--accent); }
.square.miss { box-shadow: inset 0 0 0 4px var(--danger); }
.square .square-tick {
  position: absolute; bottom: 8px; right: 8px; font-size: 1rem; line-height: 1;
  background: rgba(255, 255, 255, 0.92); border-radius: 50%; padding: 4px;
  display: grid; place-items: center;
}

@keyframes nudge {
  0%, 100% { outline-color: rgba(18, 160, 138, 0.6); }
  50% { outline-color: rgba(18, 160, 138, 0.18); }
}
@media (prefers-reduced-motion: reduce) { .square.droppable { animation: none; } }

.square .say {
  position: absolute;
  top: 6px;
  right: 6px;
  width: 40px; height: 40px;
  border: none; border-radius: 50%;
  background: rgba(255, 255, 255, 0.72);
  font-size: 1.05rem; line-height: 1; cursor: pointer;
  display: grid; place-items: center;
}
.square[data-colour='white'] .say, .square[data-colour='yellow'] .say { background: rgba(31, 39, 51, 0.1); }

.square .piece {
  display: block;
  width: 62%;
  max-height: 70%;
  aspect-ratio: 1;
  line-height: 0;
  animation: pop 0.28s cubic-bezier(0.2, 1.5, 0.4, 1);
}
.square .piece svg, .square .piece .pic { width: 100%; height: 100%; display: block; }
@keyframes pop { from { transform: scale(0.4); opacity: 0; } to { transform: scale(1); opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .square .piece { animation: none; } }

.square .piece-label {
  position: absolute; bottom: 4px; left: 4px; right: 4px;
  text-align: center; font-weight: 800;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  /* Off by default (see state.options.words in teacher.js) — a teacher who
     turns this on wants it to actually help a child read the word, so it
     is sized to be read across the room, not a quiet little caption. */
  font-size: clamp(1rem, 3.4vw, 1.35rem);
  opacity: 1;
}

/* ---------------------------------------------------------------- the tray */

/**
 * The tray keeps the same height whatever is in it.
 *
 * Its contents change constantly — tiles leave as they are placed, and a
 * locked child sees a note instead of tiles. If the tray were free to shrink,
 * the board above it would grow to fill the gap and every square would jump
 * under the child's finger. So the strip is given the height of one tile plus
 * its padding, once, and everything inside is centred in that space.
 */
:root { --tile-w: clamp(78px, 11vw, 108px); --tray-h: calc(var(--tile-w) * 1.12 + 16px); }

.tray-wrap { height: var(--tray-h); min-height: 0; }
.tray {
  display: flex;
  align-items: center;
  /* Centre the row of tiles when they all fit the width — a partly-used
     tray (a hand with only two or three left) reads oddly hugging the
     left edge. `safe` keeps the left-align fallback once the row is wider
     than the tray and needs to scroll, so an overflowing hand never has
     its first tile pushed out of scrolling reach. */
  justify-content: safe center;
  height: 100%;
  gap: clamp(8px, 1.4vw, 14px);
  overflow-x: auto;
  overflow-y: hidden;
  /* A picked tile lifts 6px, grows about 1px more from the scale, and
     carries a 4px glow ring on top of that — about 11px of clearance
     above its resting position. 6px of top padding was clipping the top
     of that ring off (the chopped-off box around a selected tile); 14px
     leaves it comfortable room. */
  padding: 14px 2px 10px;
  scrollbar-width: thin;
}
.tile {
  flex: none;
  position: relative;
  width: var(--tile-w);
  aspect-ratio: 1 / 1.12;
  border-radius: var(--r-md);
  border: 3px solid var(--line);
  background: var(--card);
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px;
  cursor: pointer;
  font: inherit;
  transition: transform 0.14s cubic-bezier(0.2, 1.4, 0.4, 1), box-shadow 0.15s, border-color 0.15s;
  box-shadow: var(--shadow-sm);
}
.tile .pic { line-height: 0; display: block; }
.tile .pic svg { display: block; width: clamp(42px, 6vw, 62px); height: auto; }
.pic { line-height: 0; }
.pic svg { display: block; }
/* Every picture is a real photo now (an <image> inside the SVG, not drawn
   vector art), and a browser's own click-and-drag on an image is to drag
   the image itself out — starting a native OS-level drag that pre-empts our
   own pointer-based one before it ever crosses DRAG_THRESHOLD. That native
   gesture leaves a plain tap still working (explaining "click works, drag
   doesn't") while dragging a picture onto a square silently does nothing.
   Switching it off here, on every picture and word tile wherever it
   appears, is what makes our own drag win instead. */
.pic, .pic *, .piece, .piece *, .tile .pic, .tile .pic * {
  -webkit-user-drag: none;
  user-drag: none;
  -webkit-touch-callout: none;
  user-select: none;
  -webkit-user-select: none;
}
/* The word card is the same box a picture draws in, so it needs no sizing
   rules of its own — only its text. */
.wc-text { font-family: var(--font); font-weight: 800; fill: var(--ink); }
/* On a tile the plaque is invisible against the tile itself, which is what
   we want — the plaque only has to do work on a coloured square. */
.tile { --plaque: transparent; }
.set-pics svg { display: block; }
.tile .name {
  /* Off by default, same setting as the board's own .piece-label — see
     state.options.words in teacher.js. */
  font-size: clamp(0.95rem, 2.6vw, 1.15rem);
  font-weight: 800;
  color: var(--ink-soft);
  max-width: 100%;
  padding: 0 4px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.tile[aria-pressed='true'] {
  border-color: var(--accent);
  box-shadow: 0 0 0 4px var(--accent-tint), var(--shadow-md);
  transform: translateY(-6px) scale(1.04);
}
.tile .say { position: absolute; top: 2px; right: 2px; width: 34px; height: 34px; border: none; background: transparent; font-size: 0.95rem; cursor: pointer; border-radius: 50%; }
/* A tile a child can't currently place stays exactly as visible as any
   other — the turn bar already says whose turn it is; a card fading out
   read as something being taken away, not just paused. */

/* Once every picture is placed, this fills the tray in place of the cards
   that were there — big and centred, rather than an empty strip below. A
   full board submits itself (see .autosubmit-bar below), so there is
   nothing to tap here any more — just a line saying what's about to
   happen. */
.finished-box { width: 100%; height: 100%; font-size: clamp(1.2rem, 2.8vw, 1.6rem); }

/**
 * The pop-up bar that counts a full board down to submitting. It sits
 * fixed to the bottom of the screen rather than taking a row of the game
 * grid — the four rows above (turn banner, board, tray, foot) are the ones
 * "squares never jump" depends on, and this bar needs to be able to
 * appear and disappear without disturbing any of them.
 */
.autosubmit-bar {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: 30;
  padding: 14px clamp(12px, 2.5vw, 22px) calc(14px + env(safe-area-inset-bottom));
  text-align: center;
  font-weight: 800;
  font-size: clamp(1rem, 2.6vw, 1.25rem);
  color: #fff;
  background: var(--accent);
  box-shadow: 0 -6px 18px rgba(0, 0, 0, 0.18);
  animation: autosubmit-in 0.2s ease;
}
@keyframes autosubmit-in {
  from { transform: translateY(100%); }
  to { transform: translateY(0); }
}

/* ------------------------------------------------------------------ footer */

.game-foot {
  height: var(--foot-h);
  display: flex;
  gap: 12px;
  align-items: center;
  min-height: 0;
  overflow: hidden;
}
.game-foot .status { font-weight: 700; color: var(--ink-soft); }

/* ----------------------------------------------------------------- overlay */

.overlay {
  position: fixed; inset: 0; z-index: 40;
  background: rgba(28, 34, 45, 0.55);
  backdrop-filter: blur(3px);
  display: grid; place-items: center;
  padding: 18px;
}
.sheet {
  background: var(--card);
  border-radius: var(--r-xl);
  padding: clamp(20px, 3vw, 34px);
  width: min(760px, 100%);
  max-height: 92dvh;
  overflow: auto;
  box-shadow: var(--shadow-lg);
}

/* ----------------------------------------------------------------- results */

.result-boards { display: grid; gap: 18px; grid-template-columns: 1fr; }
@media (min-width: 660px) { .result-boards { grid-template-columns: 1fr 1fr; } }
/* Rows never stretch: a card stretched to match a taller neighbour in the
   .dash grid would otherwise hand the extra height to the board's rows and
   pull every square out of shape (see .mini below). */
.mini-board { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; align-content: start; align-self: start; }
.mini-board[data-squares='6'] { grid-template-columns: repeat(3, 1fr); }
.mini-board[data-squares='4'] { grid-template-columns: repeat(4, 1fr); }
.mini {
  aspect-ratio: 1; border-radius: 10px; display: grid; place-items: center;
  font-size: clamp(1.1rem, 3vw, 1.7rem); position: relative;
  border: 2px solid rgba(31, 39, 51, 0.14);
  /* An aspect-ratio item in a 1fr track has a min-content width TRANSFERRED
     from whatever height the row happens to give it, and Chromium's grid
     resolves that in a second pass — so a freshly rebuilt class-mode board
     came out a frame or two wider than its card (Adam: "pop up bigger and
     then drop down smaller… outside of their bubble"). Zero minimums: the
     square is exactly a third (or a quarter) of the row, never its content. */
  min-width: 0; min-height: 0;
}
.mini.miss { box-shadow: inset 0 0 0 3px var(--danger); }
.mini .tick {
  position: absolute; bottom: 2px; right: 2px; font-size: 0.8rem; line-height: 1;
  background: rgba(255, 255, 255, 0.9); border-radius: 50%; padding: 1px;
}

.stars { font-size: 2.6rem; letter-spacing: 6px; }
.score-big { font-size: clamp(2.6rem, 9vw, 4.2rem); font-weight: 800; line-height: 1; color: var(--accent); }

/* ------------------------------------------------------- drawing dictation */

/**
 * Drawing Dictation's own live screen (#screen-draw). Same idea as
 * #screen-game above — a row grid keyed off the viewport, not off whatever
 * happens to be inside each row, so the canvas in the middle never resizes
 * out from under a child's pencil — just its own rows: a status line and
 * the canvas.
 */
:root {
  --draw-status-h: clamp(30px, 4.5vh, 40px);
}

/* The tool/colour toolbar used to be its own row, stacked above the old
   "I'm finished" button — on an iPad in landscape (the normal way this gets
   used) that ate a big slice of the limited vertical space. It now lives
   beside the canvas instead, in .draw-stage below, so the row grid only has
   to make room for the status line and the canvas — there is no finish
   button any more at all (see public/js/play.js's own comment on why, above
   ensureDrawSurface). */
#screen-draw {
  position: relative;
  display: grid;
  grid-template-rows: var(--draw-status-h) 1fr;
  gap: var(--game-gap);
  min-height: 0;
}

.draw-stage {
  display: flex;
  flex-direction: row;
  gap: clamp(8px, 1.4vw, 14px);
  min-height: 0;
  min-width: 0;
}

.draw-status {
  margin: 0;
  height: var(--draw-status-h);
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  font-weight: 700;
  color: var(--ink-soft);
}

/* Picture-led mode's own target-words strip (see public/js/drawing-sets.js)
   — a plain line of vocabulary, shown for either role. #screen-draw's fixed
   row grid has no spare row to give this its own line without resizing the
   board underneath it (the whole point of that grid — see its own doc
   comment above), so this sits over the top edge of the stage below
   instead, the same trick .conn-corner/.leave-corner already use on this
   same screen. Hidden outright in free-draw mode, which has no picture set
   to draw the words from — nothing then sits here to overlap anything. */
.draw-target-words {
  position: absolute;
  /* A few px clear of the status line above — .draw-status centres its own
     text within a short fixed row and, being bold, can sit close enough to
     that row's own bottom edge that this line touched it with no gap at
     all. */
  top: calc(var(--draw-status-h) + 10px);
  left: 0;
  right: 0;
  margin: 0;
  padding: 0 12px;
  text-align: center;
  font-size: 0.76rem;
  line-height: 1.3;
  color: var(--ink-soft);
  pointer-events: none;
}

/* The frame hands the canvas whatever space is left, with a small padding
   so the board reads as a card rather than butting straight into the page
   edge — but the canvas itself is never bigger than one fixed 4:3 size
   (see FIT_W/FIT_H in draw-ui.js, sized for a landscape iPad), and is
   never stretched to a different shape than that either way. Earlier
   versions tried a plain CSS aspect-ratio (which turned out not to
   actually hold on a frame shaped very differently, leaving real board
   space going unused) and then a full stretch-to-fill (which meant two
   children on differently-shaped screens ended up with differently
   distorted pictures of the same drawing). This version measures the
   frame in JS and sets the canvas's exact pixel size directly — always
   the same fixed shape, shrunk uniformly (never stretched on just one
   axis) only as far as a genuinely small screen actually needs, and
   centred here with blank space either side on any roomier screen, so
   every child's board looks the same as every other child's, and matches
   exactly what ends up on the teacher's dashboard and the reveal. */
.draw-frame {
  min-height: 0;
  min-width: 0;
  flex: 1 1 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--card-sunk);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-sm);
  padding: clamp(6px, 1.4vw, 12px);
}
.draw-canvas {
  flex: none;
  background: #fff;
  /* Colouring dictation (v4.89.0): the outline to colour is a background
     image on the canvas box — which fitToFrame keeps at exactly the
     canvas's own 4:3 shape, so "contain, centred" here lands the picture
     where renderPicture's <image> lands it on every thumbnail and reveal. */
  background-repeat: no-repeat;
  background-position: center;
  background-size: contain;
  border-radius: var(--r-md);
  box-shadow: var(--shadow-sm);
  touch-action: none;
  cursor: crosshair;
  -webkit-user-select: none;
  user-select: none;
}

/* Picture-led mode's describer screen (see #draw-stage-picture in
   play.html) — the reference photo itself, sized to fit the same frame the
   canvas uses without ever growing past it. The source images are not the
   canvas's own 4:3 (they're closer to 16:9 — see lib/drawing-sets.js's
   doc comment), so this scales by containment rather than the canvas's own
   exact-pixel-box approach, which assumes one fixed shape. */
.draw-reference-img {
  max-width: min(100%, 960px);
  max-height: min(100%, 720px);
  width: auto;
  height: auto;
  background: #fff;
  border-radius: var(--r-md);
  box-shadow: var(--shadow-sm);
  object-fit: contain;
  -webkit-user-select: none;
  user-select: none;
  -webkit-user-drag: none;
}

/* The tool/colour toolbar now runs down the left of the canvas rather than
   across a row above or below it — freeing up vertical space, which matters
   most on an iPad in landscape (the normal way this gets used), where every
   row taken by controls is a row not spent on the picture itself. A fixed
   column width keeps it out of the canvas's own space; it scrolls internally
   on the rare screen too short to show everything at once. */
.draw-toolbar {
  flex: none;
  width: clamp(76px, 10vw, 112px);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(8px, 1.6vh, 14px);
  padding: clamp(6px, 1vh, 10px) 6px;
  min-height: 0;
  overflow-y: auto;
  background: var(--card);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-sm);
}

.draw-tools {
  display: flex;
  flex-direction: column;
  gap: 6px;
  background: var(--card-sunk);
  border-radius: var(--r-md);
  padding: 4px;
}
/* The selected tool sits on a filled rounded square, so the picture has to
   fit INSIDE that square with room to spare. Adam, twice: "the icons for
   the pencil, crayon, and rubber are clearly not in the centre of the
   squares." They are centred — measured at every level, to 0.00 — but each
   one is drawn at 45 degrees and reaches the corners of its own box, so a
   picture that nearly fills the square has its tips poking out past the
   square's rounded corners. That reads as an icon falling out of its box,
   not as one sitting in the middle of it. The square is bigger and the
   picture is smaller than they were, which leaves a real, even margin the
   diagonal never crosses. */
.draw-tool {
  display: grid;
  place-items: center;
  width: 54px;
  height: 54px;
  border: none;
  border-radius: var(--r-sm);
  background: transparent;
  color: var(--ink-soft);
  cursor: pointer;
  font: inherit;
}
.draw-tool:hover { background: var(--card); color: var(--ink); }
.draw-tool[aria-pressed='true'] {
  background: var(--accent);
  color: #fff;
  box-shadow: var(--shadow-sm);
}
.draw-tool .ico { display: block; }

.draw-colours {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 6px;
}

.draw-toolbar #btn-draw-clear {
  width: 100%;
  white-space: normal;
  line-height: 1.2;
}
.draw-swatch {
  width: 30px;
  height: 30px;
  border-radius: 50%;
  border: 3px solid var(--card);
  box-shadow: 0 0 0 1.5px var(--line-strong);
  cursor: pointer;
  padding: 0;
}
.draw-swatch.picked { box-shadow: 0 0 0 2.5px var(--accent); }

/* Drawing Dictation's own reveal — everyone's picture, side by side, using
   the same layout rules as .result-boards above so a group of three still
   lands cleanly. No score sits alongside them; there's nothing to mark
   right or wrong. */
.result-pictures { display: grid; gap: 18px; grid-template-columns: 1fr; }
@media (min-width: 660px) { .result-pictures { grid-template-columns: 1fr 1fr; } }
.result-pictures[data-count='3'] { grid-template-columns: 1fr; }
@media (min-width: 760px) {
  .result-pictures[data-count='3'] { grid-template-columns: repeat(3, 1fr); }
}
/* Whole-class mode's own reveal (#result-draw-class) has only ever one
   picture — a child's own — so it never needs the two-up layout above,
   which would otherwise leave it stranded in a half-width column. */
.result-pictures[data-count='1'] { grid-template-columns: 1fr; max-width: 420px; margin: 0 auto; }

/* The shared "Finishing in 3, 2, 1…" countdown (#result-draw-countdown) —
   see public/js/play.js's startFinishingCountdown. */
.draw-countdown { font-size: clamp(2.2rem, 8vw, 3.4rem); font-weight: 800; text-align: center; color: var(--accent); }
.result-picture {
  display: flex;
  flex-direction: column;
  gap: 8px;
  align-items: center;
}
.result-picture .who { font-weight: 700; color: var(--ink-soft); }
/* Shared by a drawn picture (an inline <svg>, always exactly the canvas's
   own 4:3 — see draw-render.js) and, in picture-led mode, the describer's
   own reference photo (a plain <img>, closer to 16:9 — see
   lib/drawing-sets.js) in the same seat's slot. object-fit only affects
   the <img> case — inline SVG ignores it entirely — so a differently-shaped
   photo letterboxes cleanly inside the same fixed box a drawing would
   otherwise fill, rather than being stretched or cropped to fit it. */
.result-picture-svg {
  width: 100%;
  aspect-ratio: 4 / 3;
  background: #fff;
  border-radius: var(--r-md);
  box-shadow: var(--shadow-sm);
  border: 2px solid rgba(31, 39, 51, 0.14);
  object-fit: contain;
}

/* =============================================================== TEACHER UI */

.steps { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 20px; }
.step-chip {
  display: flex; align-items: center; gap: 8px; padding: 8px 16px; border-radius: 999px;
  background: var(--card); border: 2px solid var(--line); font-weight: 700; font-size: 0.9rem; color: var(--ink-faint);
}
.step-chip b { width: 22px; height: 22px; border-radius: 50%; background: var(--line); color: #fff; display: grid; place-items: center; font-size: 0.75rem; }
.step-chip.active { border-color: var(--brand); color: var(--brand-dark); background: var(--brand-tint); }
.step-chip.active b { background: var(--brand); }
.step-chip.done { border-color: var(--accent); color: #0b6d5d; }
.step-chip.done b { background: var(--accent); }

.grid-2 { display: grid; gap: 16px; grid-template-columns: 1fr; }
@media (min-width: 860px) { .grid-2 { grid-template-columns: 1.1fr 0.9fr; } }

.pair-list { display: grid; gap: 10px; }
.pair-row {
  display: grid; gap: 10px; align-items: center;
  grid-template-columns: 34px 1fr 1fr auto;
  background: var(--card-sunk); border-radius: var(--r-md); padding: 10px;
}
.pair-row .n { font-weight: 800; color: var(--ink-faint); text-align: center; }
.pair-row input { background: var(--card); min-height: 48px; }
@media (max-width: 620px) { .pair-row { grid-template-columns: 34px 1fr auto; } .pair-row input:nth-of-type(2) { grid-column: 2 / -1; } }

/* Student cards, designed to be printed and cut up. */
.cards { display: grid; gap: 14px; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); }
.student-card {
  border: 2px solid var(--line); border-radius: var(--r-lg); background: var(--card);
  padding: 16px; display: grid; gap: 10px; justify-items: center; text-align: center;
  break-inside: avoid;
}
.student-card .name { font-weight: 800; font-size: 1.15rem; }
.student-card .pairing { font-size: 0.8rem; color: var(--ink-soft); font-weight: 700; }
.student-card .code {
  font-size: 2.4rem; font-weight: 850; letter-spacing: 0.18em;
  background: var(--brand-tint); color: var(--brand-dark);
  border-radius: var(--r-md); padding: 6px 18px 6px 24px;
}
.student-card img { width: 132px; height: 132px; image-rendering: pixelated; }
.student-card .url { font-size: 0.75rem; color: var(--ink-faint); word-break: break-all; }

/* Not itself a ".card", so the "two cards in a row get a gap between them"
   rule above never applied to it — nothing separated the leaderboard from
   the group cards underneath it, and they visually ran together. */
.dash { display: grid; gap: 14px; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); margin-top: 18px; }
.dash-card { border: 2px solid var(--line); border-radius: var(--r-md); background: var(--card); padding: 14px; display: grid; gap: 10px; }
.dash-card.live { border-color: var(--accent); }
.dash-card.done { border-color: var(--brand); background: var(--brand-tint); }
/* The class-mode child currently leading — a glow rather than the "done"
   fill above, since nothing about this card means finished. */
.dash-card.leading { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-tint); }
.dash-seat { display: flex; align-items: center; gap: 8px; font-weight: 700; }
.dot { width: 10px; height: 10px; border-radius: 50%; background: var(--line-strong); flex: none; }
.dot.on { background: var(--accent); box-shadow: 0 0 0 3px var(--accent-tint); }
.bar { height: 10px; border-radius: 99px; background: var(--card-sunk); overflow: hidden; }
.bar i { display: block; height: 100%; background: var(--accent); border-radius: 99px; transition: width 0.3s; }

/**
 * The live leaderboard. One row per group: a rank, a name, a bar that
 * fills as their board fills, and a trailing number that is a running
 * percentage while they play and their score once they finish. Rows are
 * reused rather than rebuilt on every update (see renderLeaderboard in
 * teacher.js), which is what lets the bar glide to its new width instead
 * of jumping there.
 */
.leaderboard-card { margin-top: 16px; }
.leaderboard { display: grid; gap: 10px; }
.leaderboard-row {
  display: grid;
  /* The bubble gets the wider share on the console (v4.107.0) — names in
     it, and room to drop one more. */
  /* The score column used to be "auto" — sized to that one row's own text.
     "81 pts · +21 speed" is a lot wider than "13%", and since every row is
     its own independent grid, a wider score column there squeezed that
     row's bar-track column narrower than the row below it, so a finished
     bar at 100% still ended short of — or past — where the next bar's
     track ended: the "offset" look. A fixed width, the same on every row
     regardless of what the score says, is what actually lines the bars up. */
  grid-template-columns: 2ch minmax(0, 1fr) minmax(120px, 2fr) 7ch;
  align-items: center;
  gap: 12px;
}
/* The bubble is as wide as its names (v4.109.0, Adam: "a lot of dead
   space to the right of the children's names in each team and bubble.
   Remove this dead space") and the bar takes the rest; the right edges
   still line up because the score column is fixed. */
.wrap .leaderboard-row { grid-template-columns: 2ch auto minmax(120px, 1fr) 7ch; }
.leaderboard-rank { font-weight: 800; color: var(--ink-faint); text-align: right; }
.leaderboard-row.done .leaderboard-rank { color: var(--brand-dark); }
.leaderboard-name { font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.leaderboard-seat { display: inline-flex; align-items: center; gap: 2px; }
.leaderboard-seat .dash-remove { margin-left: 0; }
.leaderboard-name:has(.leaderboard-seat) { overflow: visible; white-space: normal; display: flex; flex-wrap: wrap; align-items: center; gap: 2px; }
/* The console's names sit in a bubble (v4.107.0, Adam: "each group is
   inside a bubble with the names inside the bubble and the progress bar to
   the right of it, outside the bubble") — the bubble is where a waiting
   child is dropped, or tapped in while one is picked up. Only the console
   draws it; the projected leaderboard keeps its plain names. */
.wrap .leaderboard-bubble {
  border: 2px solid var(--line);
  border-radius: 999px;
  padding: 6px 14px;
  background: var(--card-raised);
  min-height: 40px;
  overflow: visible;
  white-space: normal;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 2px;
  width: fit-content;
  max-width: 60%;
}
.wrap .leaderboard-row.done .leaderboard-bubble { border-color: var(--brand); }
.wrap .leaderboard.is-dragging .leaderboard-bubble.is-droppable,
.wrap .leaderboard.is-placing .leaderboard-bubble.is-droppable { border-style: dashed; border-color: var(--go); cursor: copy; }
.wrap .leaderboard-bubble.is-over,
.wrap .leaderboard.is-placing .leaderboard-bubble.is-droppable:hover { background: var(--accent-tint); box-shadow: 0 0 0 3px var(--accent-tint); border-style: solid; }
.leaderboard-bar { height: 16px; border-radius: 99px; background: var(--card-sunk); overflow: hidden; }
.leaderboard-bar i {
  display: block;
  height: 100%;
  border-radius: 99px;
  background: var(--accent);
  width: 0%;
  transition: width 0.6s ease;
}
.leaderboard-row.done .leaderboard-bar i { background: var(--brand); }
.leaderboard-score { font-weight: 800; font-size: 0.85rem; color: var(--ink-soft); white-space: nowrap; text-align: right; }
.leaderboard-row.done .leaderboard-score { color: var(--brand-dark); }

/**
 * The pop-out leaderboard page (leaderboard.html) — the same rows and the
 * same rules as the panel on the console, just blown up for a classroom
 * display the children read from across the room. Everything below scales
 * that one panel up; nothing here changes what a row means.
 */
/* THE POP-OUT NEVER SCROLLS. Adam: "the pop-out page displayed on the board
   should never scroll. It should remain static. At the moment, I can scroll
   up and down on this page, and it just makes it very awkward to navigate
   mid-game. Make sure this page is always self-contained and doesn't move,
   everything visible on a single browser tab, full screen."

   So the page is exactly the viewport, never more: a fixed height rather
   than a minimum one, and `overflow: hidden` so nothing can push it taller.
   Anything that genuinely might not fit — a long list of words at the end of
   a turn, a grid of thirty drawings — scrolls inside its own box instead,
   which is why every one of those has its own `overflow-y: auto` and a
   `min-height: 0` on its parents to let it shrink. */
.leaderboard-page {
  height: 100dvh;
  max-height: 100dvh;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  padding: clamp(20px, 3vw, 40px);
}
body:has(.leaderboard-page) {
  overflow: hidden;
}
/* Text sitting directly on the patterned page ground reads poorly — every
   other screen in the app puts its content on a plain .card for exactly
   that reason (see .on-paper above), and this page is no different just
   because it's projected large. */
.leaderboard-page .lp-card {
  flex: 1 1 auto;
  min-height: 0; /* lets the card shrink so its own children can scroll */
  /* Scrolls, because some of its faces genuinely can be taller than a
     projector screen — a whole class's finished drawings above all. The one
     that must NOT is Hot Seat's board, which is furniture rather than a page
     (see .is-board below). */
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: clamp(16px, 2.5vw, 28px);
}
.leaderboard-page .lp-card.is-board { overflow: hidden; }
/* The one list on this page that can genuinely be longer than the screen. */
.leaderboard-page .leaderboard {
  overflow-y: auto;
  min-height: 0;
}
.leaderboard-page .lp-head {
  display: flex;
  align-items: baseline;
  gap: 16px;
  flex-wrap: wrap;
}
/* Hot Seat's board is furniture in a classroom, not a page: Adam asked for
   the title and the game number off it entirely. What is left is the
   connection pill, and even that only appears when something is WRONG —
   `.live` is hidden, so a board silently showing stale data because the
   socket dropped is still visible as a problem, while a working one shows
   nothing at all. With every child hidden the row collapses to nothing. */
.leaderboard-page .lp-head.is-board { gap: 0; }
/* And the row itself goes when nothing in it is showing (v4.74.0) — its
   empty height plus the card's gap was a band of nothing above the logo.
   Adam: "there remains a lot of dead space above the hot seat logo and the
   teacher dashboard button… Please move everything up and fill this
   space." The page and the card pull their top padding in on the board too. */
.leaderboard-page .lp-head.is-board:not(:has(> #lp-conn.off)):not(:has(> #lp-conn.bad)) { display: none; }
.leaderboard-page:has(> .lp-card.is-board) { padding-top: 10px; }
.leaderboard-page .lp-card.is-board { padding-top: 10px; }
.lp-backs.is-classic .lp-backs-top .lp-backs-title { margin-top: 0; }
.leaderboard-page .lp-head.is-board #lp-title,
.leaderboard-page .lp-head.is-board #lp-game-logo,
.leaderboard-page .lp-head.is-board #lp-code,
/* `.go` is the class the pill wears while it reads "live" — the word is the
   text, not the class, which is why an earlier `#lp-conn.live` selector
   silently matched nothing and Adam still saw it. */
.leaderboard-page .lp-head.is-board #lp-conn.go,
/* A classroom screen does not need a status light saying the connection is
   fine — only one saying it is not, which .off and .bad still do. */
.leaderboard-page .lp-head.is-quiet #lp-conn.go { display: none; }
/* Story Blocks: the wordmark on its own. Every pixel this row keeps comes
   straight off the height three rows of blocks have to share between them,
   and the logo already says which game is on. */
.leaderboard-page .lp-head.is-wordmark { gap: 0; justify-content: center; }
.leaderboard-page .lp-head.is-wordmark #lp-title { display: none; }
/* The wordmark centred over the board, and much bigger than the line of type
   it replaced. Adam: "make the Story Blocks logo quite a bit bigger and put it
   in the centre of the page." It is the only thing in this row, so the spacer
   that would otherwise shove it back to the left has to go. */
.leaderboard-page .lp-head.is-wordmark .spacer { display: none; }
.leaderboard-page .lp-head.is-wordmark #lp-game-logo {
  height: clamp(64px, 11vh, 132px);
  max-width: 46%;
}
/* The wordmark doubles as the way back to the console — see leaderboard.js. */
.leaderboard-page #lp-game-logo.is-link { cursor: pointer; }
.leaderboard-page #lp-game-logo.is-link:hover { filter: brightness(1.06); transform: scale(1.03); transition: transform 120ms ease; }
.leaderboard-page .lp-head h1 { margin: 0; font-size: clamp(1.6rem, 3vw, 2.4rem); }
.leaderboard-page .lp-head .pill { font-size: 1rem; }
.leaderboard-page .leaderboard { gap: clamp(10px, 1.6vw, 18px); }
/* The name column gets the bigger share here, the reverse of the console's
   own ratio — blown up this large, "Mai Anh & Yuki · Attempt 2" needs more
   room than the bar does, and the name is the thing a room full of children
   is actually reading. */
/* A fixed score column, not "auto" — see the comment on the base
   .leaderboard-row rule for why "auto" made the bars themselves misalign
   row to row. */
.leaderboard-page .leaderboard-row { grid-template-columns: 2.4ch minmax(0, 1.6fr) minmax(110px, 1fr) 8ch; gap: clamp(12px, 2vw, 22px); }
.leaderboard-page .leaderboard-rank { font-size: clamp(1.3rem, 2.6vw, 2rem); }
.leaderboard-page .leaderboard-name { font-size: clamp(1.2rem, 2.4vw, 1.9rem); }
.leaderboard-page .leaderboard-bar { height: clamp(20px, 2.6vw, 34px); }
.leaderboard-page .leaderboard-score { font-size: clamp(1rem, 1.8vw, 1.4rem); }
.leaderboard-page .lp-empty { font-size: clamp(1.1rem, 2.2vw, 1.5rem); color: var(--ink-soft); text-align: center; margin-top: 10vh; }

/* Drawing Dictation's own face on the pop-out — no leaderboard, no ranking,
   just the point of the exercise in big letters (and any key words the
   teacher chose) — a reminder for the room, not a rundown of a score. */
.leaderboard-page .lp-drawing {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: clamp(20px, 4vh, 40px);
  text-align: center;
  padding: 4vh 2vw;
}
.leaderboard-page .lp-drawing-message {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(1.8rem, 5vw, 3.4rem);
  line-height: 1.25;
  color: var(--ink);
  max-width: 26ch;
}
.leaderboard-page .lp-keywords {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: clamp(8px, 1.4vw, 14px);
}
.leaderboard-page .lp-keyword-chip {
  padding: clamp(8px, 1.4vw, 14px) clamp(16px, 2.6vw, 26px);
  border-radius: 999px;
  background: var(--accent-tint);
  color: var(--brand-dark);
  font-weight: 700;
  font-size: clamp(1rem, 2vw, 1.5rem);
}

/* One group's own finished pictures, shown to the whole class in place of
 * the reminder message above — swapped in by showDrawing() whenever the
 * teacher highlights that group from the dashboard (see drawingGameCard in
 * teacher.js). Reuses .result-pictures/.result-picture/.result-picture-svg
 * from the child's own reveal screen (app.css, above) so the pictures
 * themselves look identical; only the name label is its own, larger class
 * — .result-picture's own tiny/muted label is sized for a child reading
 * their own device up close, not a whole room reading a projector. */
.leaderboard-page #lp-showcase { width: 100%; max-width: 1100px; margin: 0 auto; }
.lp-showcase-name {
  margin: 0;
  font-weight: 800;
  font-size: clamp(1.2rem, 2.6vw, 1.9rem);
  color: var(--ink);
  text-align: center;
}

/* Whole-class drawing mode's own face — every child's own finished picture,
 * all at once, populated the moment the teacher presses "Game Finished"
 * (see showDrawingClassResult in leaderboard.js). Deliberately not shown
 * while the class is still drawing — see showDrawingClassWaiting and its
 * own comment on why. */
.leaderboard-page .lp-drawing-class {
  display: grid;
  /* Big enough to actually look at from across a room. Adam, from a lesson:
     "I had to scroll down to show the pictures on that screen, and they were
     very small. I should be able to simply scroll down the whole page and
     have all the pictures shown." Two or three across a projector rather than
     six or seven, and the CARD scrolls (see .lp-card) rather than this grid
     having a scrollbar of its own inside a screen that had none. */
  grid-template-columns: repeat(auto-fill, minmax(min(18rem, 100%), 1fr));
  gap: clamp(14px, 2.4vw, 26px);
  padding: clamp(8px, 2vh, 20px) 0;
  align-content: start;
}
/* Each child's picture is a tile of its own — a bordered card, so a row of
   them reads as separate drawings rather than as one wall of lines. */
.leaderboard-page .lp-drawing-class-card {
  background: var(--card-sunk);
  border: 2px solid var(--line);
  padding: clamp(10px, 1.6vw, 18px);
}
.leaderboard-page .lp-drawing-class-card .result-picture-svg { width: 100%; }
/* A <button>, not a <div> — every picture is tappable to open it full-size
 * (see openLightbox) — so the usual button chrome is stripped back down to
 * plain layout, plus a hover/focus state to actually look pressable. */
.lp-drawing-class-card {
  display: grid;
  gap: 8px;
  justify-items: center;
  border: none;
  background: none;
  padding: 8px;
  border-radius: var(--r-md);
  cursor: pointer;
  font: inherit;
}
.lp-drawing-class-card:hover,
.lp-drawing-class-card:focus-visible { background: var(--card-sunk); }
.lp-drawing-class-name {
  margin: 0;
  font-weight: 700;
  font-size: clamp(1rem, 1.8vw, 1.3rem);
  color: var(--ink);
  text-align: center;
}

/* One picture, blown up full-size (see openLightbox in leaderboard.js) —
 * the close button sits in the picture's own top-right corner, the way
 * Adam asked for it, rather than in a header bar above it. */
.lp-lightbox-frame {
  position: relative;
  width: min(720px, 100%);
  background: var(--card);
  border-radius: var(--r-xl);
  padding: clamp(20px, 3vw, 34px);
  box-shadow: var(--shadow-lg);
  text-align: center;
}
.lp-lightbox-close {
  position: absolute;
  top: -14px;
  right: -14px;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  border: 2px solid var(--line);
  background: #fff;
  color: var(--ink);
  font-size: 1.1rem;
  line-height: 1;
  display: grid;
  place-items: center;
  cursor: pointer;
  box-shadow: var(--shadow-sm);
}
.lp-lightbox-close:hover { background: var(--card-sunk); }
#lp-lightbox .result-picture-svg { width: 100%; max-width: 640px; }
/* The console's own showcase (v4.109.0) is for a projected screen: as big
   as the window allows, the page 4:3 inside it. */
.drawing-showcase-frame { width: min(1100px, 94vw); }
#drawing-showcase .result-picture-svg { width: 100%; max-height: 72vh; }
#drawing-showcase .lp-showcase-name { font-size: clamp(1.2rem, 2.4vw, 1.8rem); font-weight: 800; margin: 0 0 10px; }

/* The join-code face this same page shows before a round starts (see
 * showJoin in leaderboard.js) — .project-url/.project-code are the same
 * classes the full-screen "Show on the board" overlay uses, just reused
 * here inside the card rather than filling the whole viewport, so the two
 * ways of projecting a join code never drift apart visually. */
/* Sized by the screen's HEIGHT as well as its width. Adam, from a lesson:
   "I was unable to scroll down to show the children the QR code. You need to
   make the QR code smaller so it fits on the screen." A width-only clamp asks
   for a 10rem code and a 260px QR on a 1280×720 projector, which together are
   taller than the screen — and this page does not scroll by default. Every
   part of this face is now bounded by dvh too, so it fits first and is only
   as big as the room allows. */
.leaderboard-page .lp-join {
  text-align: center;
  display: grid;
  /* Four rows that take what they need, then the QR takes what is LEFT.
     Adam, from a Where's Grandma? lesson: "when I pop out onto another
     screen, I have to scroll down to get to the QR code." The dvh clamps
     added in 4.29.1 were an arithmetic fix — they sized each part against
     the whole viewport, which knows nothing about the ~230px this page
     spends above and around this face on its own padding, its card and its
     heading. Every time something changed up there the sums went stale
     again. This version does no arithmetic: the QR is given the leftover
     row and shrinks to fit it, so the face cannot be taller than its box
     however small the screen or however tall the heading. */
  grid-template-rows: repeat(4, auto) minmax(0, 1fr);
  gap: 4px;
  justify-items: center;
  align-content: stretch;
  flex: 1 1 auto;
  min-height: 0;
  padding: clamp(8px, 2dvh, 28px) 0;
}
.leaderboard-page .lp-join .project-url { font-size: clamp(0.9rem, min(2.6vw, 3.4dvh), 1.6rem); }
.leaderboard-page .lp-join .project-code {
  /* Adam, looking at this on a classroom screen: "the QR code is too small,
     and the class code is too big. Shrink down the size of the numbers and
     increase the size of the QR code." Both come from this one number: the
     QR takes whatever row is left over, so every pixel taken off the code
     goes straight to it. */
  font-size: clamp(2.2rem, min(10vw, 16dvh), 6.5rem);
  line-height: 1.05;
}
.leaderboard-page .lp-join img {
  /* Square, so height is what decides it; width follows and is capped so a
     very wide, very short screen cannot stretch it sideways. */
  height: 100%;
  width: auto;
  min-height: 0;
  max-width: 100%;
  /* High enough that the cap is never what limits it on a projector — the
     leftover row is. */
  max-height: 460px;
  object-fit: contain;
  image-rendering: pixelated;
  border-radius: 12px;
  margin-top: clamp(4px, 1dvh, 10px);
}

/* The persistent join banner — see leaderboard.html's comment above
   #lp-join-mini — a full-width strip across the top of the projected
   screen, above the leaderboard or the drawing message, so the code, the
   web address and (especially) the QR code stay big enough to read and
   scan from across the room for the whole time a round is under way, not
   just at the very start. */
.lp-join-mini {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: clamp(18px, 3vw, 34px);
  padding: clamp(12px, 2vw, 20px) clamp(20px, 3vw, 40px);
  margin-bottom: clamp(16px, 2.5vw, 28px);
  border-radius: var(--r-lg, 20px);
  background: var(--card);
  box-shadow: var(--shadow-sm);
}
.lp-join-mini img {
  display: block;
  width: clamp(120px, 13vw, 200px);
  height: clamp(120px, 13vw, 200px);
  border-radius: 12px;
  image-rendering: pixelated;
  flex: none;
}
.lp-join-mini-text { display: flex; flex-direction: column; gap: 2px; line-height: 1.25; }
.lp-join-mini-text span { font-size: clamp(1.1rem, 2.2vw, 1.7rem); color: var(--ink-soft); font-weight: 650; }
.lp-join-mini-text b {
  font-size: clamp(2.4rem, 6vw, 4.4rem); letter-spacing: 0.08em; font-variant-numeric: tabular-nums; color: var(--brand-dark);
}

.picker-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(72px, 1fr)); gap: 8px; max-height: 46dvh; overflow: auto; padding: 4px; }
.picker-grid button {
  aspect-ratio: 1; border: 2px solid var(--line); border-radius: var(--r-sm); background: var(--card);
  font-size: 1.7rem; cursor: pointer; display: grid; place-items: center; padding: 0;
}
.picker-grid button:hover { border-color: var(--brand); background: var(--brand-tint); }

/* Drawing Dictation, picture-led mode — the roster's own "who holds the
   picture, and which one" choice (teacher.js's pictureAssignmentBlock) and
   the shared picker overlay's own grid of the twelve built-in pictures
   (teacher.js's renderPictureModal). The picker overlay reuses .picker-grid
   for its layout; .picture-pick overrides the plain icon-button look above
   since these buttons hold a photo and a caption, not a single glyph. */
.group-picture { margin-top: 8px; }
.group-picture-btn {
  display: flex; align-items: center; gap: 10px; width: 100%; text-align: left;
  border: 2px dashed var(--line-strong); border-radius: var(--r-sm); background: var(--card);
  padding: 8px 12px; cursor: pointer; font: inherit; color: var(--ink-soft);
}
.group-picture-btn:hover { border-color: var(--brand); background: var(--brand-tint); }
.group-picture-btn.chosen { border-style: solid; border-color: var(--line); color: inherit; }
.group-picture-btn img { width: 40px; height: 40px; object-fit: cover; border-radius: calc(var(--r-sm) - 2px); flex: none; }

.picker-grid .picture-pick {
  aspect-ratio: auto; height: auto; display: grid; gap: 4px; padding: 6px; place-items: stretch;
}
.picture-pick img { width: 100%; aspect-ratio: 1; object-fit: cover; border-radius: calc(var(--r-sm) - 2px); }
.picture-pick .tiny { text-align: center; color: var(--ink-soft); }

/* A teacher's own uploaded photos, in the set builder. */
.lib-tile { position: relative; }
.lib-tile .lib-photo { width: 100%; overflow: hidden; }
.lib-tile .lib-photo img { width: 100%; height: 100%; object-fit: cover; display: block; border-radius: calc(var(--r-sm) - 2px); }
.lib-photo-delete {
  position: absolute; top: -6px; right: -6px; width: 22px; height: 22px; border-radius: 999px;
  border: 2px solid var(--card); background: var(--danger); color: #fff; cursor: pointer;
  display: grid; place-items: center; padding: 0;
}

.photo-tools { display: grid; gap: 8px; }
.photo-paste {
  border: 2px dashed var(--line-strong); border-radius: var(--r-sm); padding: 14px;
  text-align: center; color: var(--ink-faint); font-size: 0.85rem; cursor: text;
  min-height: 44px; display: flex; align-items: center; justify-content: center;
}
.photo-paste:empty::before { content: attr(data-placeholder); }
.photo-paste:focus { outline: 2px solid var(--brand); border-style: solid; }

.chosen { display: flex; gap: 8px; flex-wrap: wrap; }
.chosen .chip {
  display: inline-flex; align-items: center; gap: 8px; padding: 6px 8px 6px 12px;
  border-radius: 999px; background: var(--card-sunk); border: 2px solid var(--line);
  font-weight: 650; font-size: 0.9rem;
}
.chosen .chip .x { border: none; background: transparent; cursor: pointer; font-size: 1rem; color: var(--ink-faint); padding: 4px; }
.chosen .chip [contenteditable]:empty::before { content: 'type a word'; color: var(--ink-faint); }

@media print {
  .no-print { display: none !important; }
  body { background: #fff; }
  .cards { grid-template-columns: repeat(3, 1fr); }
  .student-card { border-color: #999; box-shadow: none; }
}

/* =========================================================================
   Lobby, joining and grouping
   Added when the app moved from printed per-child cards to one code on the
   board, Blooket-style.
   ========================================================================= */

/* "safe center": if the card is too tall to fit (say, the iPad keyboard has
   covered half the screen), align it to the top instead of centring it
   off-screen. Plain "center" has no fallback — it happily centres a card
   whose top half is above the visible area. The plain "center" line stays
   first as a fallback for older Safari versions that don't know "safe" —
   an unsupported value is ignored, leaving the line before it in effect.
   This was the fix in 4.0.1, and it was real, but it was not the whole
   bug: it only bites once the keyboard is up. The actual "cut off at the
   top with no keyboard in sight" report turned out to be a different
   cause entirely — see the padding below. */
.centre-screen {
  /* .game-body no longer forces a height on these screens (see the "not
     .is-live" rule above) — it only sets a minimum, so a card taller than
     the screen can push the page taller and simply scroll. This element
     needs that same minimum for itself, or centring has no room to work
     in: a flex column exactly as tall as its own content can't centre
     anything within it, there's no spare space to place it in. */
  min-height: 100vh;
  min-height: 100dvh;
  justify-content: center; justify-content: safe center; align-items: center;
  /* The join/name/lobby screens ask for viewport-fit=cover, which opts a
     page out of iOS's automatic safe-area handling — it matters once the
     page is opened from a Home Screen icon: iOS then draws it full-bleed
     under its own status bar rather than leaving room for it, and a
     centred card is free to land with its top edge behind that status
     bar. These two lines give that strip back, only on these three
     screens; env() is 0 on a device with no such strip (an ordinary
     Safari tab), so nothing changes there. */
  padding-top: max(var(--game-pad-y), env(safe-area-inset-top));
  padding-bottom: max(var(--game-pad-y), env(safe-area-inset-bottom));
}

.join-card {
  width: min(30rem, 100%);
  text-align: center;
  display: grid;
  gap: 12px;
  justify-items: stretch;
}
.join-card.wide-card { width: min(42rem, 100%); }
.join-card h1 { margin: 0; }
.join-card .muted { margin: 0; }

/* The big code and name entries. Deliberately enormous: a seven-year-old is
   typing this on a wobbly iPad from a projector six metres away. */
/* Soft pastel: a mint field with a sky edge, 72px tall; the code in a
   monospace so 0 and O, 1 and I are told apart from a distance. */
input.big-entry {
  text-align: center;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: clamp(1.9rem, 9vw, 2.4rem);
  font-weight: 700;
  letter-spacing: 0.35em;
  text-indent: 0.35em;
  height: 72px;
  min-height: 72px;
  background: var(--paper);
  border-radius: 18px;
  font-variant-numeric: tabular-nums;
}
input.big-entry.name {
  font-family: var(--font);
  font-size: clamp(1.3rem, 5.5vw, 1.6rem);
  letter-spacing: 0;
  text-indent: 0;
  height: 64px;
  min-height: 64px;
}
.big-entry::placeholder { color: var(--ink-faint); }

.tick-big { font-size: 3rem; line-height: 1; }

.waiting-dots { display: flex; gap: 8px; justify-content: center; padding: 6px 0; }
.waiting-dots i {
  width: 10px; height: 10px; border-radius: 50%; background: var(--accent);
  animation: bob 1.2s ease-in-out infinite;
}
.waiting-dots i:nth-child(2) { animation-delay: 0.15s; }
.waiting-dots i:nth-child(3) { animation-delay: 0.3s; }
@keyframes bob { 0%, 100% { opacity: 0.25; transform: translateY(0); } 50% { opacity: 1; transform: translateY(-5px); } }
@media (prefers-reduced-motion: reduce) { .waiting-dots i { animation: none; opacity: 0.7; } }

/* "You are working with Will" — a plain, big sentence once the teacher has
   grouped the class, so a child does not have to guess which of the names
   on the generic "who's here" list below is actually their own partner. */
.lobby-partner {
  margin: 4px 0 0;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(1.4rem, 4vw, 2.1rem);
  color: var(--brand-dark);
  text-align: center;
}

.here-list { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; padding: 8px 0; }
.here-chip {
  padding: 6px 14px; border-radius: 999px;
  background: var(--card-sunk); font-weight: 650; font-size: 0.92rem;
}
.here-chip.away { opacity: 0.45; }

/* ---- the teacher's join panel ------------------------------------------ */

.code-hero {
  display: flex; align-items: center; gap: clamp(16px, 4vw, 40px); flex-wrap: wrap;
}
.join-url { font-family: var(--font); font-size: clamp(1.1rem, 2.6vw, 1.5rem); font-weight: 700; }
.code-big {
  font-size: clamp(3rem, 9vw, 4.6rem); font-weight: 850; line-height: 1;
  letter-spacing: 0.1em; color: var(--brand-dark);
  font-variant-numeric: tabular-nums;
}
.code-hero img { image-rendering: pixelated; border-radius: 10px; }
.hero-actions { margin-left: auto; display: grid; gap: 8px; }

/* Squarez's roster: the join details as a strip across the top rather than a
   hero card that owns half the screen. Adam: "the QR code, the sign up link,
   and the sign up code all arranged along a top toolbar that takes up no more
   than 25% of the height of the screen. Below that, as the children sign up,
   the teacher then allocates them into pairs." The names are the thing being
   worked with; the code is just there to be read off. */
.code-hero.is-strip {
  max-height: 25dvh;
  padding: 10px clamp(12px, 2vw, 20px);
  gap: clamp(14px, 2.5vw, 30px);
  flex-wrap: nowrap;
  overflow: hidden;
}
.code-hero.is-strip > div:first-child {
  display: flex;
  align-items: baseline;
  gap: clamp(8px, 1.6vw, 18px);
  flex-wrap: wrap;
  min-width: 0;
}
.code-hero.is-strip .eyebrow { margin: 0 !important; }
.code-hero.is-strip .join-url { font-size: clamp(0.95rem, 1.7vw, 1.3rem); }
.code-hero.is-strip .code-big { font-size: clamp(1.7rem, 4vw, 2.8rem); letter-spacing: 0.06em; }
.code-hero.is-strip img { height: min(17dvh, 118px); width: auto; }
.code-hero.is-strip .hero-actions { grid-auto-flow: row; gap: 6px; }

/* Which of the two windows this is. The console opened from the browser is
   the leaderboard; the one "Teacher dashboard" opens is the group-by-group
   view, and is optional — nothing about a lesson depends on it being open. */
/**
 * Pairs' three stages, on one screen. Adam's own flow: choose the cards, then
 * the code and who has arrived, then the leaderboard. Each stage shows its own
 * third and nothing else — which is what keeps the first screen free of a QR
 * code nobody needs yet.
 */
/* Regrouping: an iPad per card, a name per tile, and a tile can be picked up
   and dropped on another to swap the two children over. */
.pairs-regroup-card { display: none; }
#screen-pairs-setup[data-stage='regroup'] .pairs-regroup-card { display: block; }
#screen-pairs-setup[data-stage='regroup'] .pairs-build-row,
#screen-pairs-setup[data-stage='regroup'] #pairs-sets-card { display: none; }
.pairs-regroup-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 14px;
}
.pairs-regroup-pad {
  border: 2px solid var(--line);
  border-radius: var(--r-md);
  background: var(--card-raised);
  padding: 12px;
  display: grid;
  gap: 8px;
}
.pairs-regroup-pad h3 { margin: 0; font-size: 0.85rem; color: var(--ink-soft); font-weight: 700; }
.pairs-regroup-tile {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 14px;
  border-radius: 999px;
  border: 2px solid var(--line-strong);
  background: var(--card);
  font-weight: 750;
  cursor: grab;
  user-select: none;
  touch-action: none;
}
.pairs-regroup-tile.is-empty { border-style: dashed; color: var(--ink-faint); cursor: default; }
.pairs-regroup-tile.is-picked { border-color: var(--brand); background: var(--brand-tint); }
.pairs-regroup-tile.is-over { border-color: var(--go); background: var(--accent-tint); }

/* Nothing here says `display: revert` — the cards row has its own layout and
   putting it back to the UA default flattened it into a column, so the two
   set-up cards stopped matching heights. The stage a thing belongs to simply
   never hides it. */
#screen-pairs-setup .pairs-joined-card,
#screen-pairs-setup #pairs-live-card { display: none; }
#screen-pairs-setup[data-stage='join'] .pairs-build-row,
#screen-pairs-setup[data-stage='join'] #pairs-sets-card,
#screen-pairs-setup[data-stage='live'] .pairs-build-row,
#screen-pairs-setup[data-stage='live'] #pairs-sets-card { display: none; }
/* The code card is a strip, so the list of who has arrived — the thing the
   teacher is watching on this screen — gets the room. Adam: "this will allow
   you to shrink that top card and make a bit more space for everything below
   it." */
/* The join details live on the top bar now (v4.86.0); the old hero card is gone. */
#screen-pairs-setup[data-stage='join'] .pairs-joined-card { display: block; }
#screen-pairs-setup[data-stage='live'] #pairs-live-card { display: block; margin-top: 18px; }

/* The roster's bar, one shape for every game (v4.62.0). Adam: "Logo: left.
   Go back to the dashboard function: beneath the logo. Go button: in the
   middle at the top. QR code: completely on the right, with the website and
   the game number to its left." Three columns with the outer two equal, so
   the middle group is centred on the bar rather than between whatever the
   two sides happen to weigh. */
.roster-topline {
  display: grid;
  /* The outer two share one minimum, so they stay equal (and the middle
     stays centred) even when the middle group has to wrap: Squarez's runs
     to five controls, which is wider than the bar leaves it on a laptop. */
  grid-template-columns: minmax(min(250px, 26%), 1fr) minmax(0, auto) minmax(min(250px, 26%), 1fr);
  align-items: center;
  gap: 16px;
}
.roster-middle {
  display: grid;
  justify-items: center;
  gap: 8px;
  justify-self: center;
}
.roster-go-row,
.roster-extras {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 10px;
}
/* The row under Go is the quieter one. */
.roster-extras .btn { padding: 6px 12px; font-size: 0.85rem; }
.roster-extras:not(:has(:not(.hidden))) { display: none; }
.roster-topline .join-mini { justify-self: end; }
/* The join details can never be wider than their column (v4.79.0). Adam:
   "the QR code and game code are protruding outside the box." The outer
   columns share whatever the middle leaves, and a hero QR with a 3rem code
   beside it is wider than a quarter of a laptop's bar — so the right-hand
   cell is a size container and the QR and the code scale down to it
   rather than hanging out past the edge. `cqw` is a fraction of the cell,
   so on a wide screen the clamp wins and nothing changes. */
.roster-join { display: grid; justify-items: end; }
.roster-topline > .roster-join,
.roster-topline > .park-topline-right { container-type: inline-size; justify-self: stretch; min-width: 0; max-width: 100%; }
/* An auto margin on a grid item beats `justify-self: stretch`, and a size
   container with no width of its own is zero pixels wide — so the
   `.park-topline-right { margin-left: auto }` written for the flex bars is
   turned off in the grid one. */
.roster-topline > .park-topline-right { margin-left: 0; }
.roster-topline > .roster-join > .join-mini,
.roster-topline > .park-topline-right > .join-mini { max-width: 100%; min-width: 0; }
.roster-topline .join-mini.is-big.is-hero img { width: min(clamp(96px, 15vh, 168px), 42cqw); height: min(clamp(96px, 15vh, 168px), 42cqw); }
.roster-topline .join-mini.is-big.is-hero b { font-size: min(clamp(1.8rem, 4vw, 3rem), 10cqw); }
.roster-topline .join-mini .join-mini-text { min-width: 0; }
.roster-topline .join-mini .join-mini-text span { overflow-wrap: anywhere; }
/* The QR on the far right, the address and the code to its LEFT, both
   right-aligned so they sit against it. */
.join-mini.is-right { flex-direction: row-reverse; padding: 6px 6px 6px 18px; }
.join-mini.is-right .join-mini-text { align-items: flex-end; text-align: right; }
.topline .join-mini.is-big.is-hero.is-right { padding: 8px 8px 8px 26px; }

/* The game's own mark at the left of the roster's bar, with the way back
   directly under it. */
.roster-brand {
  display: grid;
  justify-items: start;
  gap: 2px;
  flex: none;
}
/* Adam, of Where's Grandma?'s first screen: "I need the QR code and join code
   to be quite a bit bigger. You can also make the logo quite a bit bigger to
   fit as well. The height of the QR code and logo should be the height of the
   existing Who's Here box, suitably expanded." So the whole bar grows to that
   height and everything on it grows with it. */
.roster-brand .dash-game-logo { max-width: none; height: clamp(72px, 13vh, 148px); }
/* Drawing Dictation's class bar (v4.79.0): the logo the same height as the
   QR code beside it — "considerably bigger so it is equal in size to the QR
   code, which I also want you to make bigger." */
.roster-brand .dash-game-logo.is-hero { height: clamp(96px, 15vh, 168px); }
/* Drawing Dictation's class sign-in screen (v4.79.1). Adam: "Make the QR
   code 50% bigger. Make the drawing dictation logo equal in size to the QR
   code. Make the website address and the game code bigger also." Half as
   big again as the hero size, logo and QR alike; still capped by the cell
   (cqw) so nothing leaves the bar on a narrow screen. */
/* The logo's cell is a size container too, so the logo is capped by the
   same fraction of the same-width column as the QR is — the two stay equal
   whatever the screen. */
.roster-topline > .roster-brand:has(.is-huge) { container-type: inline-size; min-width: 0; }
.roster-brand .dash-game-logo.is-hero.is-huge { height: min(clamp(144px, 22.5vh, 252px), 44cqw); max-width: 100%; }
.roster-topline .join-mini.is-big.is-hero.is-huge img { width: min(clamp(144px, 22.5vh, 252px), 44cqw); height: min(clamp(144px, 22.5vh, 252px), 44cqw); }
/* The code gets what the QR leaves of the cell: five digits at this
   letter-spacing are about 3.4 ems wide, and the QR, the gap and the
   padding take the rest. */
.roster-topline .join-mini.is-big.is-hero.is-huge b { font-size: min(clamp(2.4rem, 5.5vw, 4rem), calc(14cqw - 8px)); letter-spacing: 0.05em; }
.roster-topline .join-mini.is-big.is-hero.is-huge .join-mini-text span { font-size: min(1.3rem, calc(4cqw - 2px)); }
.roster-brand .link-back { font-size: 0.9rem; }

.topline .join-mini.is-big.is-hero { padding: 8px 26px 8px 8px; gap: 20px; }
.topline .join-mini.is-big.is-hero img { width: clamp(96px, 15vh, 168px); height: clamp(96px, 15vh, 168px); }
.topline .join-mini.is-big.is-hero b { font-size: clamp(1.8rem, 4vw, 3rem); letter-spacing: 0.08em; }
.topline .join-mini.is-big.is-hero .join-mini-text span { font-size: 1rem; }

body.is-dash-window .main-window-only { display: none !important; }
body:not(.is-dash-window) .dash-window-only { display: none !important; }

/* A small, always-visible copy of the same join details, on the live
   dashboard once a round is under way (see #playing-join-mini and its
   Drawing Dictation twin in teacher.html) — the big card above only ever
   shows on the roster, before Go, but a late child or one who got knocked
   offline needs the code just as much once everyone else is already
   playing. The pop-out screen carries the same small copy of its own
   (see .lp-join-mini in leaderboard.js) so it is visible on the projector
   too, not just the teacher's own device. */
.join-mini {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 4px 14px 4px 4px;
  border-radius: 999px;
  background: var(--card);
  box-shadow: var(--shadow-sm);
}
.join-mini img { display: block; border-radius: 6px; image-rendering: pixelated; }
/* The class-mode banner's copy is scanned by the children themselves rather
   than glanced at by the teacher, so it is bigger and its code is readable
   from a desk away. Adam: "on this screen, I want the children to be able to
   scan the QR code and log in." */
.join-mini.is-big { padding: 6px 18px 6px 6px; gap: 14px; }
.join-mini.is-big img { width: 76px; height: 76px; }
.join-mini.is-big b { font-size: 1.5rem; letter-spacing: 0.08em; }
/* Every live screen's bar ends with the join details, QR outermost (v4.70.0).
   Adam: "the QR code always positioned prominently on the right-hand side of
   that top bar, with the website and the sign-up code to the left." */
.join-mini.is-big.is-right { padding: 6px 6px 6px 18px; }
.topline > .join-mini.is-right { margin-left: auto; }

/* A group card the teacher can open up to see what is being drawn. Adam:
   "there should not be any drawings shown to begin with. The teacher should be
   able to click on a group, which expands the window and shows the drawings." */
.dash-card.is-openable { cursor: pointer; }
.dash-card.is-openable:hover { border-color: var(--brand); }
.dash-card.is-openable.is-open { cursor: default; }

/* Picture-led mode's pool of pictures — the ticked ones are what the game
   deals from, at random, one per group. */
.picture-card { display: grid; gap: 6px; justify-items: center; padding: 8px; }
.picture-card img { width: 100%; aspect-ratio: 4 / 3; object-fit: cover; border-radius: var(--r-sm); }
.picture-card.on { outline: 3px solid var(--brand); outline-offset: 2px; }

/* A picture being dragged over the upload button. */
.backs-chosen-word {
  flex: 1 1 auto;
  min-width: 0;
  font: inherit;
  font-weight: 700;
  padding: 4px 8px;
  border: 2px solid transparent;
  border-radius: var(--r-sm);
  background: transparent;
  color: inherit;
}
.backs-chosen-word:hover { border-color: var(--line); }
.backs-chosen-word:focus { border-color: var(--brand); background: var(--card-raised); outline: none; }

/* Finding a picture on the web, from inside the card builder. */
.pairs-web-search { margin-top: 14px; }
.pairs-web-results {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(84px, 1fr));
  gap: 8px;
  margin-top: 10px;
  max-height: 260px;
  overflow-y: auto;
}
.pairs-web-hit {
  padding: 0;
  border: 2px solid var(--line);
  border-radius: var(--r-sm);
  background: var(--card-raised);
  cursor: pointer;
  overflow: hidden;
  aspect-ratio: 1;
}
.pairs-web-hit img { width: 100%; height: 100%; object-fit: cover; display: block; }
.pairs-web-hit:hover { border-color: var(--brand); }

.pairs-upload-btn.is-dropping {
  outline: 3px dashed var(--brand);
  outline-offset: 3px;
  background: var(--brand-tint);
}

/* Shouting Dictation's board: one bar per child, all of them moving at once.
   Adam: "provide a status bar which shows what percentage of the total areas
   blanked out they have typed something into. This means that all groups will
   be updating at the same time." */
.shout-progress-list { display: grid; gap: 10px; margin-top: 10px; }
.shout-progress-row { display: grid; gap: 6px; }
.shout-progress {
  position: relative;
  height: 30px; /* thicker (v4.76.0) */
  border-radius: 999px;
  background: var(--card-sunk);
  border: 2px solid var(--line);
  overflow: hidden;
}
.shout-progress-fill {
  position: absolute; inset: 0 auto 0 0;
  background: var(--brand);
  transition: width 260ms ease;
  overflow: hidden; /* clips the white copy of the text to the fill */
}
.shout-progress-text {
  position: absolute; inset: 0;
  display: grid; place-items: center;
  font-size: 0.86rem; font-weight: 750; color: var(--ink);
  mix-blend-mode: normal;
  white-space: nowrap;
}
/* The white copy inside the fill is laid out against the WHOLE bar's width
   so it sits exactly over the dark one — see progressBar in teacher.js. */
.shout-progress-fill .shout-progress-text.is-inverse { width: calc(100% / (var(--fill-share, 1))); color: #fff; }
/* Marked against the document's own words (v4.70.0). Adam: "the leaderboard
   bar green when the pair's squares match perfectly, pink otherwise." */
.shout-progress.is-right .shout-progress-fill { background: var(--go); }
.shout-progress.is-wrong .shout-progress-fill { background: #e88aa4; }
.shout-progress.is-right { border-color: var(--go); }
.shout-progress.is-wrong { border-color: #e88aa4; }
/* And each answer on a thumbnail or the reveal: a green or pink edge. */
.shout-answer-text.is-right { box-shadow: inset 0 0 0 2px var(--go); }
.shout-answer-text.is-wrong { box-shadow: inset 0 0 0 2px #e88aa4; background: #fbe9ef; }

/**
 * The dashboard window's own live feed of the two copies.
 *
 * Adam, of the old version: "there is a severe error with how the pages are
 * displaying on the teacher's dashboard, the two pages are different sizes,
 * with Student B's page overlapping Student A's page. Student B's name and
 * Student A's name have been truncated." A Shouting group is always exactly
 * two, so it gets two equal columns rather than the three-across grid the
 * other games share — that third empty track is what left the two pages
 * different widths — and a name is allowed to wrap rather than being cut off
 * with an ellipsis in a column too narrow for it.
 */
body.is-dash-window #shouting-dash .dash-seats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
body.is-dash-window #shouting-dash .dash-seat { align-items: baseline; }
body.is-dash-window #shouting-dash .dash-seat .nm { overflow: visible; white-space: normal; }
body.is-dash-window #shouting-dash .dash-drawing-thumb { width: 100%; }

/* The builder's box handles: invisible until hovered, then an X in the
   corner of that box. The layer itself lets clicks through so drawing a new
   box still works everywhere the boxes are not. */
.doc-canvas-stage { position: relative; display: block; line-height: 0; }
.doc-box-layer { position: absolute; inset: 0; pointer-events: none; }
.doc-box {
  position: absolute;
  border-radius: 4px;
  pointer-events: auto;
  transition: box-shadow 120ms ease;
}
.doc-box:hover { box-shadow: inset 0 0 0 3px var(--danger); }
.doc-box-x {
  position: absolute;
  top: -10px; right: -10px;
  width: 24px; height: 24px;
  display: none; place-items: center;
  border-radius: 999px;
  border: 2px solid var(--danger);
  background: var(--card);
  color: var(--danger);
  cursor: pointer;
  padding: 0;
  line-height: 0;
}
.doc-box:hover .doc-box-x,
.doc-box:focus-within .doc-box-x { display: grid; }
.join-mini-text { display: flex; flex-direction: column; line-height: 1.25; }
.join-mini-text b {
  font-size: 1rem; letter-spacing: 0.08em; font-variant-numeric: tabular-nums; color: var(--brand-dark);
}
.join-mini-text span { font-size: 0.72rem; color: var(--ink-soft); }
/* The join address is a real link with its https:// (v4.118.2 — pasted
   into a Teams chat it has to come out clickable) and a small Copy beside
   it. Inherits the size of whatever card it sits on. */
.join-link { color: inherit; font: inherit; text-decoration: underline; text-decoration-color: color-mix(in srgb, currentColor 40%, transparent); text-underline-offset: 2px; overflow-wrap: anywhere; }
.join-link:hover { text-decoration-color: currentColor; }
.join-copy {
  margin-left: 6px; padding: 1px 8px; border-radius: 999px; border: 1.5px solid var(--line); background: var(--card);
  color: var(--brand-dark); font: inherit; font-size: 0.7em; font-weight: 700; line-height: 1.5; cursor: pointer; vertical-align: middle; white-space: nowrap;
}
.join-copy:hover { background: var(--brand-tint); }
.join-copy.is-done { background: var(--go); color: #fff; border-color: var(--go); }
.join-mini.is-right .join-mini-text span { display: flex; flex-direction: row-reverse; align-items: center; gap: 6px; flex-wrap: wrap; justify-content: flex-start; }
.join-mini.is-right .join-mini-text span .join-copy { margin: 0; }

/* ---- assigning a late arrival mid-game ---------------------------------- */

/* A child who joins after Go, or who was left out when the class was first
   grouped, waits here instead of just vanishing off the teacher's screen —
   each chip picks which already-playing group to drop them into (see
   waitingChip in teacher.js and Lobby.assignToGame / DrawingLobby.assignToGame). */
.dash-waiting {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  align-items: center;
  margin-bottom: 16px;
  padding: 10px 14px;
  border-radius: var(--r-lg);
  background: var(--card);
  box-shadow: var(--shadow-sm);
}
.dash-waiting-label { font-weight: 700; font-size: 0.85rem; color: var(--ink-soft); }
.waiting-chip {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 6px 10px;
  border-radius: 999px;
  background: var(--card-sunk);
}
.waiting-chip .nm { font-weight: 650; }
.waiting-chip select {
  font: inherit;
  font-size: 0.85rem;
  border-radius: 999px;
  border: 2px solid var(--line);
  padding: 4px 8px;
  background: #fff;
}
.waiting-chip .tiny.muted { font-style: italic; }

/* ---- grouping ---------------------------------------------------------- */

.tiny-head {
  font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--ink-faint); margin: 0 0 8px;
}
.quiet-block { opacity: 0.55; }

.chip-tray { display: flex; flex-wrap: wrap; gap: 8px; min-height: 44px; align-items: flex-start; }

.name-chip {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 8px 8px 8px 12px; border-radius: 999px;
  border: 2px solid var(--line); background: var(--card);
  font: inherit; font-weight: 650; font-size: 0.95rem; cursor: pointer;
  min-height: 44px;
}
.name-chip:hover { border-color: var(--line-strong); }
.name-chip.picked {
  border-color: var(--accent); background: var(--accent-tint);
  box-shadow: 0 0 0 4px var(--accent-tint);
}
.name-chip.away { opacity: 0.5; }
.name-chip .x {
  border: none; background: transparent; cursor: pointer; padding: 2px 4px;
  color: var(--ink-faint); font-size: 0.9rem; border-radius: 50%;
}
.name-chip .x:hover { color: var(--danger); }

.groups { display: grid; gap: 12px; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); }
.group-card {
  border: 2px solid var(--line); border-radius: var(--r-md);
  background: var(--card-sunk); padding: 12px; display: grid; gap: 10px;
}
.group-card.trio { border-color: var(--brand); }
.group-head { display: flex; align-items: center; gap: 8px; font-size: 0.9rem; }

.slot-drop {
  border: 2px dashed var(--line-strong); border-radius: var(--r-sm);
  background: transparent; color: var(--ink-faint);
  font: inherit; font-size: 0.85rem; padding: 10px; cursor: pointer; min-height: 44px;
}
.slot-drop.ready { border-color: var(--accent); color: var(--accent); background: var(--accent-tint); }

/**
 * This was "position: sticky", then "position: fixed" — two different
 * attempts at pinning it to the screen edge, and both read as "floating"
 * on a real device: sticky only locks to the bottom once scrolling has
 * carried its normal spot that far down, sitting wherever it falls in the
 * document the rest of the time; fixed avoids that, but this app's other
 * brush with fixed positioning on the join screens (below) turned into a
 * much worse rendering bug on an actual iPad than the one it replaced.
 * The button now simply sits in the ordinary page, right after the last
 * card — nothing pinned, nothing that can slide around, no exotic
 * positioning for a real device's browser to get wrong.
 */
.go-bar {
  display: flex; align-items: center; gap: 14px;
  margin-top: 20px; padding-top: 16px;
  /* No border-top here on purpose — Adam found the divider line above the
     Go button distracting rather than useful, so the bar now just sits in
     the ordinary page flow with nothing drawn above it. */
}
.go-bar .status { font-weight: 650; color: var(--ink-soft); }

/* ---- projecting the code ----------------------------------------------- */

/* This and the pop-out second screen (leaderboard.html's own join face) are
   the same thing shown two ways — one over the console, one on its own
   screen — and Adam asked for them to look and behave the same: "check both
   of these screens and ensure that they are consistent." So this one is
   built exactly like that one now: the patterned ground with a single cream
   card on it, and the QR taking whatever room the rest of the card leaves
   rather than a size of its own. See .leaderboard-page .lp-join for the
   reasoning behind the leftover-row trick — the short version is that no
   amount of vw/dvh arithmetic survives a change to anything above it, and
   this screen had gone back to scrolling on a 1280x720 projector. */
.overlay.project {
  background: var(--paper);
  backdrop-filter: none;
}
.project-inner {
  text-align: center;
  display: grid;
  /* Go to / url / Type this code / code, then the QR takes the rest, then
     the names and the Close button take what they need back. */
  grid-template-rows: repeat(4, auto) minmax(0, 1fr) auto auto;
  gap: 6px;
  justify-items: center;
  align-content: stretch;
  background: var(--card);
  border-radius: var(--r-xl);
  box-shadow: var(--shadow-lg);
  width: min(1180px, 100%);
  height: 100%;
  min-height: 0;
  padding: clamp(14px, 2.4vw, 30px);
}
.project-url { font-size: clamp(1.4rem, 4vw, 2.4rem); font-weight: 700; }
.project-code {
  /* Deliberately the same balance as the pop-out's own code above — these
     two screens are one thing shown two ways. Bigger here only because this
     one has a whole screen rather than a card inside a card. */
  font-size: clamp(2.4rem, min(14vw, 20dvh), 8rem);
  font-weight: 850;
  line-height: 1;
  letter-spacing: 0.08em;
  color: var(--brand-dark);
  font-variant-numeric: tabular-nums;
}
.project-inner img {
  height: 100%;
  width: auto;
  min-height: 0;
  max-width: 100%;
  max-height: 520px;
  object-fit: contain;
  image-rendering: pixelated;
  border-radius: 12px;
  margin-top: 8px;
}
/* A class's worth of names can be long; it gets its own scroll rather than
   pushing the code and the QR off a projector. */
.project-names {
  display: flex; flex-wrap: wrap; gap: 8px; justify-content: center;
  max-width: 60rem; margin-top: 10px;
  max-height: 18dvh; overflow-y: auto; min-height: 0;
}

/* Three boards side by side on the results screen. */
.result-boards[data-count='3'] { grid-template-columns: 1fr; }
@media (min-width: 760px) {
  .result-boards[data-count='3'] { grid-template-columns: repeat(3, 1fr); }
}

/* =========================================================================
   Dragging
   Tap-to-place still works exactly as before; dragging is added on top.
   Pointer events rather than HTML5 drag-and-drop, because iPad Safari does
   not fire drag events for arbitrary elements.
   ========================================================================= */

/* A transform never affects layout, so pressing a square cannot move the
   board. Kept separate from the sizing rules above for that reason. */
.square:active { transform: scale(0.985); }

/* `touch-action: pan-x` used to sit here so a horizontal gesture could
   scroll the tray natively. On iOS Safari that raced our own pointer-event
   drag: the browser sometimes claimed a mostly-sideways touch for native
   panning before our JS ever saw ten pixels of movement, cancelling the
   drag with a pointercancel — which is why dragging a picture out of the
   tray felt clunky and worked "sometimes". `touch-action: none` hands the
   whole gesture to our own code every time; onPointerMove in play.js now
   does the sideways-scroll itself (see the DRAG_THRESHOLD block there) so
   scrolling the tray still works, just without a browser race to lose. */
.tile { touch-action: none; }

.tile.dragging,
.square.dragging-from .piece,
.square.dragging-from .piece-label { opacity: 0.25; }

/* The picture that follows the finger. */
.drag-ghost {
  position: fixed;
  z-index: 60;
  pointer-events: none;
  transform: translate(-50%, -50%) scale(1.15);
  filter: drop-shadow(0 8px 16px rgba(0, 0, 0, 0.3));
  display: grid;
  place-items: center;
  font-size: clamp(2.6rem, 8vw, 4.6rem);
  line-height: 1;
  will-change: left, top;
}
@media (prefers-reduced-motion: reduce) {
  .drag-ghost { transform: translate(-50%, -50%); }
}

/* While a drag is in flight nothing else should react to the pointer. */
body.is-dragging { cursor: grabbing; user-select: none; -webkit-user-select: none; }
body.is-dragging .tray { overflow-x: hidden; }

/* =========================================================================
   The teacher's live view
   Boards sit side by side inside a group so a whole group fits on screen,
   and each one is capped rather than growing with the window.
   ========================================================================= */

.dash { grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); }

/* Always exactly three tracks, never "however many seats this particular
   card happens to have" (that used to be `repeat(auto-fit, minmax(96px,
   1fr))`, which divided a card's width by 2 for a pair but by 3 for a
   group of three — same card width, so a pair's thumbnails rendered
   visibly bigger than a three's, sitting right next to each other on the
   same dashboard). Three is the largest a group ever gets (see
   lib/drawing.js's ALL_SEATS), so a two-seat pair simply leaves the third
   track empty — every thumbnail on the dashboard is now the same size
   regardless of group size. */
.dash-seats {
  display: grid;
  /* minmax(0, …) rather than minmax(96px, …) (v4.105.0): three boards at
     96px plus the gaps were wider than the card, so a group of three
     protruded out of it — Adam: "a group of three currently protrudes
     outside of the cards". The boards shrink to share the card instead,
     and the dashboard window's cards are wider so they need not shrink far. */
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 10px;
  align-items: start;
}
.dash-seat-col .mini-board, .dash-seat-col .mini { min-width: 0; }
/* (The dashboard window's own columns are set beside its cards' rules —
   see "The grid is four tracks" further down.) */
.dash-seat-col { display: grid; gap: 6px; min-width: 0; }
.dash-seat-col .dash-seat { font-size: 0.86rem; gap: 6px; }
.dash-seat-col .dash-seat .nm {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
}
.dash .mini-board { gap: 3px; }
.dash .mini { border-width: 1px; border-radius: 6px; font-size: clamp(0.8rem, 1.6vw, 1.1rem); }

/* The teacher's small boards must mirror the child's exactly, or comparing
   what she sees with what they see needs a mental rotation. */
.mini-board[data-squares='4'] { grid-template-columns: repeat(2, 1fr); }
.mini-board[data-squares='6'] { grid-template-columns: repeat(3, 1fr); }
.mini-board[data-squares='8'] { grid-template-columns: repeat(4, 1fr); }

/* A team-mode game card narrows its mini-boards by nesting them inside
   .dash-seats/.dash-seat-col, one narrow column per seat. The class-roster
   card has no seats to nest inside — one child, one board, straight inside
   .dash-card — so without its own cap the board stretched to the full card
   width and every square rendered oversized. */
/* No cap since v4.109.0 (Adam: "a little bit of dead space to the
   right-hand side of each board. I want you to expand the boards, make
   them bigger, and fill this space") — the board is the card's width. */
#class-roster .mini-board { max-width: none; gap: 4px; }
#class-roster .mini { border-width: 1px; border-radius: 6px; font-size: clamp(0.8rem, 1.6vw, 1.1rem); }

/* A little air between the children in a group, so three boards do not read
   as one long strip. */
.dash-seats { gap: 14px; }
.dash-seat-col + .dash-seat-col { border-left: 1px solid var(--line); padding-left: 12px; }

/* Picture-led mode's own "has the picture" tag on the teacher's dashboard
   (see drawingGameCard in teacher.js) — a block of its own underneath the
   name row rather than another chip crammed into it: that row is a narrow,
   non-wrapping flex line with no spare width in a three-wide card, and a
   phrase this long overflowed into the next child's own column when it
   was tried there. As a block it simply wraps within its own column
   instead, the same as any other text would. */
.dash-seat-tag {
  display: block;
  width: fit-content;
  max-width: 100%;
  font-size: 0.72rem;
  font-weight: 700;
  color: var(--brand-dark);
  background: var(--brand-tint);
  border-radius: 999px;
  padding: 2px 9px;
  white-space: normal;
}

/* Drawing Dictation's own dashboard card — the same .dash/.dash-card/
   .dash-seats/.dash-seat-col frame as a Squarez group card above, with a
   live picture in place of a mini-board. Always live: unlike Squarez's
   .mini-board (behind the peek switch, because it could give away the
   answer), there is nothing to spoil in a drawing, so every child's page
   just shows as it is drawn. */
.dash-drawing-thumb {
  background: #fff;
  border-radius: var(--r-sm);
  border: 1px solid rgba(31, 39, 51, 0.14);
  overflow: hidden;
}
/* Same letterboxing note as .result-picture-svg above — this class is also
   shared between a live/finished drawing (<svg>, 4:3) and, for a
   picture-led describer's seat, their reference photo (<img>, ~16:9). */
.dash-drawing-svg { display: block; width: 100%; aspect-ratio: 4 / 3; object-fit: contain; }
/* Class mode's showcase (v4.106.0): the picture is a button — a tap puts
   that child's page up on the projected screen; the one that is up wears
   the brand ring. */
button.dash-drawing-showcase { display: block; width: 100%; padding: 0; cursor: pointer; font: inherit; }
button.dash-drawing-showcase:hover { border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-tint); }
button.dash-drawing-showcase[aria-pressed='true'] { border: 3px solid var(--brand); box-shadow: 0 0 0 4px var(--brand-tint); }

/* The last finished result for a group, retained on the card even once
   "Draw again" has cleared the live seats above back to blank — see
   DrawingRound.lastResult in lib/drawing.js. Its own small heading and a
   divider keep it visually apart from the live picture, since the two can
   be showing genuinely different pages once a fresh attempt is underway. */
.dash-last-result {
  border-top: 1px solid var(--line);
  padding-top: 10px;
  display: grid;
  gap: 8px;
}

/* ---------------------------------------------------- the vocabulary screen */

/* Every set in one flat, big-picture grid — no theme headings above them.
   A teacher scanning for "farm animals" does not need to know it lives
   under "Core EAL starter" first. */
.vocab-grid {
  display: grid;
  gap: 18px;
  grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
}
/* A choice card (Soft pastel): white with a 2px sky edge; chosen = the
   coral tint with a coral edge. The same shape as .mode-card below. */
.set-card {
  text-align: left; font: inherit; cursor: pointer;
  border: 2px solid var(--line); border-radius: var(--r-lg); background: var(--card);
  padding: 18px; display: grid; gap: 10px;
}
.set-card:hover { border-color: var(--line-strong); }
.set-card[aria-pressed='true'] { border-color: var(--brand); background: var(--brand-tint); }
.set-card .title { font-family: var(--font-display); font-weight: 700; font-size: 1.2rem; }
.set-pics { display: flex; gap: 6px; flex-wrap: wrap; }

/* --------------------------------------------------------- the mode picker */

/* Team mode or class mode — the very first choice, before vocabulary is
   even picked, so the rest of the console can build on knowing which one
   it's in rather than asking again later. */
.mode-grid {
  display: grid;
  gap: 16px;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}
.mode-card {
  text-align: left; font: inherit; cursor: pointer;
  border: 2px solid var(--line); border-radius: var(--r-lg); background: var(--card);
  padding: 20px; display: grid; gap: 8px;
}
.mode-card:hover { border-color: var(--line-strong); }
.mode-card h3 { font-family: var(--font-display); font-size: 1.15rem; }

/* ------------------------------------------------------- the game picker */

/* The very first thing a signed-in teacher sees: which Barrier game to
   play — each one its own tile, with its own little "How to Play" button
   sitting underneath rather than inside it, so tapping the game and
   tapping the instructions are never the same target.
   Widened from the old 160-200px icon-and-label columns in v4.33.0 to fit
   Adam's own Gemini-made logos (see public/images/logos/) — a wordmark with
   its own small illustrated scene, illegible shrunk to icon size — but
   240-360px turned out wider than any logo actually needs: at the tile's
   own max height every logo renders 200-240px wide (they're all roughly the
   same 1.3-1.6:1 landscape shape), so a 360px column left "too much dead
   space either side of the logos" — Adam's own words — and only ever fit
   two to a row. Narrowed to fit the logos themselves rather than an
   arbitrary column width, which is what actually lets three or four sit
   side by side. */
/* The picker's tiles (the rebrand, v4.120.0). The sixteen logos are as
   they are — deliberately distinct — and the FRAME is what is uniform: one
   tile size, one logo box every logo is fitted into, the name in Lexend
   under it, and a foot with the play-mode tag and "How to play". */
.picker { max-width: 72rem; margin: 0 auto; display: grid; gap: 16px; }
/* The filter row: two rows of chips, single-select in each. */
/* The filter row (Soft pastel): white chips on the mint, lifted by a soft
   shadow, the chosen one in ink; then a divider and the Barrier games
   toggle. No box round the row. */
.picker-filters { display: flex; gap: 10px 14px; flex-wrap: wrap; align-items: center; padding: 2px 0; }
.chip-row { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
.chip-row + .chip-row { padding-left: 14px; border-left: 1px solid var(--line-strong); }
.chip {
  display: inline-flex; align-items: center; gap: 8px; padding: 0 14px; min-height: 36px; border-radius: 999px;
  border: 0; background: var(--card); color: var(--ink); box-shadow: var(--shadow-sm);
  font: inherit; font-weight: 600; font-size: 0.86rem; cursor: pointer;
}
.chip:hover { box-shadow: var(--shadow-md); }
.chip[aria-pressed="true"] { background: var(--ink); color: #fff; }
.chip svg { width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
/* The toggle inside a chip: a sky track, a white knob that slides right
   and the track goes coral when it is on. */
.chip .toggle { width: 28px; height: 16px; border-radius: 999px; background: var(--sky); position: relative; display: inline-block; flex: none; transition: background 120ms ease; }
.chip .toggle::after { content: ''; position: absolute; left: 2px; top: 2px; width: 12px; height: 12px; border-radius: 50%; background: #fff; transition: transform 120ms ease; }
.chip[aria-pressed="true"] .toggle { background: var(--brand); }
.chip[aria-pressed="true"] .toggle::after { transform: translateX(12px); }
.chip.is-toggle[aria-pressed="true"] { background: var(--card); color: var(--ink); }
.game-tile.is-filtered { display: none; }
.picker-empty { color: var(--ink-soft); }
@media (max-width: 720px) { .chip-row + .chip-row { padding-left: 0; border-left: 0; } }
.game-grid {
  display: grid;
  gap: 18px;
  /* Four across on a laptop (the picker is 72rem at most), so a one-line
     subtitle like "Countable and uncountable nouns" has the room it needs. */
  grid-template-columns: repeat(auto-fill, minmax(236px, 1fr));
}
/* A tile (Soft pastel): a white card, no border, lifted by the big soft
   shadow; the logo in a fixed box (it carries the game's name — the name
   is the logo's alt and the button's aria-label, never repeated as text),
   one line of subtitle, and a foot with the hardware tag and How to play. */
.game-tile {
  position: relative;
  display: grid; grid-template-rows: 1fr auto;
  border: 0; border-radius: var(--r-lg); background: var(--card); box-shadow: var(--shadow-lg);
  overflow: hidden; transition: transform 120ms ease, box-shadow 120ms ease;
}
.game-tile:hover, .game-tile:focus-within { transform: translateY(-2px); box-shadow: 0 16px 32px rgba(42, 58, 52, 0.14); }
.game-tile:focus-within { outline: 3px solid var(--sky-strong); outline-offset: 2px; }
.game-card {
  width: 100%; text-align: center; font: inherit; cursor: pointer;
  border: 0; background: transparent; padding: 16px 16px 8px;
  display: grid; justify-items: center; gap: 8px; color: var(--ink);
}
.game-logo-box { display: grid; place-items: center; width: 100%; height: 110px; }
/* A fixed max-height, not 100%: a grid item's percentage height is
   indefinite here, so 100% resolved to nothing and every logo drew at full
   width over its own name. */
.game-logo { max-width: min(100%, 170px); max-height: 110px; width: auto; height: auto; object-fit: contain; }
.game-name { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
.game-sub { font-size: 0.82rem; line-height: 1.3; color: var(--ink-soft); max-width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tile-foot {
  display: flex; align-items: center; justify-content: space-between; gap: 6px; flex-wrap: nowrap;
  margin: 0 16px; padding: 8px 0 12px; border-top: 1px solid var(--line); background: var(--card);
}
/* The hardware tag is plain text — "one screen" or "devices" — in the
   soft ink. */
.tile-tag {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 0.72rem; font-weight: 600; line-height: 1.4; white-space: nowrap;
  color: var(--ink-soft);
}
.tile-tag svg { width: 13px; height: 13px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
/* Beta sits on the tile's corner as a butter pill, so the foot is the same
   one line on every tile. */
.tile-tag.is-beta { position: absolute; top: 10px; right: 10px; z-index: 1; padding: 4px 9px; border-radius: 999px; background: var(--butter); color: var(--ink); font-size: 0.66rem; letter-spacing: 0.06em; text-transform: uppercase; font-weight: 700; }
.tile-how {
  margin-left: auto; padding: 4px 0; border: 0; background: transparent; font: inherit;
  font-size: 0.78rem; font-weight: 700; color: var(--ink); cursor: pointer; white-space: nowrap; text-decoration: underline; text-underline-offset: 3px;
}
.tile-how:hover { color: var(--brand-dark); }
.tile-how:focus-visible { outline: 3px solid var(--brand); outline-offset: 2px; border-radius: 6px; }
@media (max-width: 560px) {
  .game-grid { grid-template-columns: 1fr 1fr; gap: 10px; }
  .game-logo-box { height: 84px; }
  .game-logo { max-height: 84px; }
  .game-card { padding: 12px 8px 8px; gap: 6px; }
  .game-name { font-size: 0.88rem; }
  /* A phone's half-width tile cannot hold "Projector" and "How to play"
     on one line, so the foot may wrap there (measured at 390px: the link
     was clipped to "How to pla"). */
  .tile-foot { padding: 6px 8px 8px; flex-wrap: wrap; row-gap: 2px; }
  .tile-how { font-size: 0.78rem; padding: 4px 2px; }
  .tile-tag { font-size: 0.7rem; padding: 2px 7px; }
  .tile-tag.is-beta { top: 6px; right: 6px; }
}
/* Printables re-uses the tile frame with no foot: the logo box alone. */
.printables-grid .game-tile { grid-template-rows: 1fr; }
.printables-grid .game-card { padding: 16px 14px; }
.printables-grid .game-logo { max-height: 120px; }

/* --------------------------------------------------------- the how-to guide */

/* One guide screen per game, reached from that game's "How to Play"
   button. A simple numbered list — a short instruction, then the real app
   caught mid-action with an arrow pointing at exactly what to tap. */
.guide-card { max-width: 46rem; margin: 0 auto; }
.guide-intro { color: var(--ink-soft); font-size: 1.05rem; }
/* The two bubbles at the top of every guide (v4.116.0): what the game is,
   and the language it targets. */
.guide-bubble { border-radius: var(--r-lg); padding: 16px 20px; margin-top: 14px; }
.guide-bubble h3 { margin: 0 0 6px; font-family: var(--font-display); font-size: 1.05rem; }
.guide-bubble p { margin: 0; color: var(--ink); }
.guide-bubble.is-what { background: #e6f4ea; border: 2px solid #bfe3c9; }
.guide-bubble.is-language { background: #fff3c4; border: 2px solid #f1dd8a; }
.guide-how-head { margin: 26px 0 6px; font-family: var(--font-display); font-size: 1.3rem; }

.guide-steps { display: grid; gap: 30px; margin-top: 20px; }
.guide-step { display: grid; gap: 10px; }
.guide-step-head { display: flex; align-items: center; gap: 12px; }
.guide-num {
  flex: none; width: 34px; height: 34px; border-radius: 50%;
  background: var(--brand); color: #fff;
  display: grid; place-items: center;
  font-family: var(--font-display); font-weight: 800; font-size: 1.05rem;
}
.guide-step h3 { margin: 0; font-family: var(--font-display); font-size: 1.15rem; }
.guide-step p { margin: 0; color: var(--ink-soft); }
.guide-shot {
  display: block; width: 100%; height: auto;
  border-radius: var(--r-lg);
  border: 1px solid var(--line-strong);
  box-shadow: var(--shadow-md);
}

.guide-extension {
  background: var(--brand-tint); border-radius: var(--r-lg);
  padding: 18px 20px; margin-top: 26px;
}
.guide-extension h3 { margin: 0 0 6px; font-family: var(--font-display); }
.guide-extension p { margin: 0; color: var(--ink-soft); }

.guide-close {
  text-align: center; margin: 24px 0 4px;
  font-family: var(--font-display); font-size: 1.1rem;
}

/* The options a teacher sets once a term, laid out in plain sight rather
 * than folded behind a summary line — down to three now that the code,
 * names, grouping, timer and colour words all live elsewhere (or not at
 * all). Picture count gets its own full-width row; the two switches then
 * sit side by side underneath, rather than one trailing alone below an
 * uneven number of items sharing the same two columns. */
.options-panel {
  display: grid;
  gap: 16px;
  grid-template-columns: 1fr;
}
@media (min-width: 720px) {
  .options-panel { grid-template-columns: 1fr 1fr; }
  .options-panel > div { grid-column: 1 / -1; }
}
.switch { padding: 16px 18px; }
.switch span { font-size: 1.05rem; }

/* The picture picker in the set builder shows drawings now, not glyphs. */
#picker button {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  padding: 6px;
  cursor: pointer;
  display: grid;
  place-items: center;
  min-height: 48px;
}
#picker button:hover { border-color: var(--brand); background: var(--brand-tint); }

/* ------------------------------------------------------------- the icons */

/**
 * The interface's own marks. Emoji used to do this job; they rendered
 * differently on every device and carried a cartoon tone the rest of the
 * design does not.
 */
.ico { display: block; flex: none; }

.square .say, .tile .say {
  display: grid;
  place-items: center;
  color: var(--ink-soft);
}
.square .say { background: rgba(255, 255, 255, 0.8); color: var(--ink); }

.tick { display: inline-grid; place-items: center; }
.tick.yes { color: var(--accent); }
.tick.no { color: var(--danger); }

.tick-big { width: 64px; height: 64px; margin: 0 auto 8px; color: var(--accent); display: block; }

/**
 * The board keeps its cells close to square.
 *
 * Its outer box is still the fixed grid row — only the board's own width
 * narrows within it — so this changes how the squares look without letting
 * anything move them.
 */
.board { margin: 0 auto; width: 100%; }
@media (min-width: 700px) {
  .board[data-squares='4'] { max-width: calc(var(--board-h) * 1.35); }
  .board[data-squares='6'] { max-width: calc(var(--board-h) * 1.9); }
  .board[data-squares='8'] { max-width: calc(var(--board-h) * 2.45); }
}

/* ============================================================ SHOUTING === */
/* Shouting Dictation: the document library, the box-drawing builder, and
   the child's fill-in-the-blank overlay. */

.doc-grid {
  display: grid;
  gap: 16px;
  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
}
.doc-card {
  text-align: left; font: inherit; cursor: pointer;
  border: 3px solid var(--line); border-radius: var(--r-lg); background: var(--card);
  padding: 14px; display: grid; gap: 8px;
}
.doc-card:hover { border-color: var(--line-strong); }
.doc-card[aria-pressed='true'] { border-color: var(--brand); background: var(--brand-tint); box-shadow: 0 0 0 4px var(--brand-tint); }
.doc-card .doc-thumb {
  width: 100%; aspect-ratio: 4 / 3; object-fit: cover;
  border-radius: calc(var(--r-sm) - 2px); background: var(--card-sunk);
  border: 1px solid var(--line);
}
.doc-card .title { font-family: var(--font-display); font-weight: 800; font-size: 1.05rem; }
.doc-card .doc-meta { display: flex; gap: 6px; flex-wrap: wrap; }
.doc-card-actions { display: flex; gap: 10px; }
.doc-card-actions span { font-size: 0.8rem; font-weight: 700; cursor: pointer; }
.doc-card-actions .doc-edit { color: var(--brand); }
.doc-card-actions .doc-delete { color: var(--danger); }
.doc-card-actions .doc-print { color: var(--ink-soft); }

/* Student A / B — red, blue, matching the child's own copy of whichever
   colour their role is drawn in on the printed original. Unlike .seg's
   plain brand-coloured "pressed" state, each button here takes on its own
   role's colour once pressed, so the active colour is obvious at a glance
   while drawing boxes. */
.role-seg button[data-role='A'][aria-pressed='true'] { background: var(--sq-red); color: #fff; }
.role-seg button[data-role='B'][aria-pressed='true'] { background: var(--sq-blue); color: #fff; }

/* The banner that follows the page down. Adam, after trying to blank out an
   A4 sheet: "I cannot zoom in on the PDF… change it so that the whole of the
   A4 sheet shows on the screen that I can scroll up and down. Have student A
   and student B as a floating banner that scrolls with me." Sticky rather
   than fixed, so it belongs to this screen and cannot be left hanging over
   any other one. */
.doc-banner {
  position: sticky;
  top: 0;
  z-index: 6;
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
  margin: 16px 0 12px;
  padding: 10px clamp(10px, 1.6vw, 16px);
  background: var(--card);
  border: 2px solid var(--line-strong);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-md);
}

.doc-canvas-wrap {
  border: 2px solid var(--line-strong); border-radius: var(--r-md);
  background: var(--card-sunk); overflow: hidden;
  display: grid; place-items: center; padding: 8px;
  touch-action: none; /* the canvas itself handles the drag */
}
/* max-width/max-height with width/height both auto — not width:100% with
   height:auto — is what actually lets a replaced element (the canvas
   already carries its true intrinsic width/height as HTML attributes, set
   in showShoutingPage) shrink to fit BOTH bounds while keeping its real
   shape. The width:100% version looks right until a document's real shape
   makes the width-derived height exceed max-height: at that point the
   spec only clamps height, width stays at 100%, and the canvas visibly
   distorts into whatever box results — a portrait page rendering into a
   wide, squashed frame. Confirmed by measuring the actual rendered box in
   a real browser, not just reasoning about it: with width:100% in place,
   an 826×1169 canvas (ratio 0.707) rendered at 589×449 (ratio 1.31) —
   height correctly hit the 60dvh cap, width did not budge. With this
   max-width/max-height/auto version, both dimensions are solved together
   for every document shape, same as the classic `max-width:100%;
   height:auto` responsive-image pattern. */
/* NO max-height. It used to be 60dvh, which is what made an A4 page render
   about a third of the size it needs to be — Adam: "an A4 PDF is appearing
   very small on my screen, and I cannot reliably block out different parts of
   it… make sure that the A4 is full size on the screen so I can work through
   it and block bits out." The page is taller than the screen now, and the
   screen scrolls, which is what the sticky banner above is for.

   `width: auto` with `max-width: 100%` keeps the two rules that mattered in
   the first place: the page is never blown up past the resolution it was
   rendered at, and it is never squashed out of shape — height and width are
   still solved together rather than one being clamped while the other stays
   put, which is the bug this rule was written for in v4.19.0. */
.doc-canvas-wrap canvas {
  display: block; width: auto; height: auto;
  max-width: 100%; cursor: crosshair;
  border-radius: calc(var(--r-sm) - 2px);
}
.doc-canvas-empty {
  padding: 60px 20px; text-align: center; color: var(--ink-soft);
}
/* Several pages (v4.78.0), stacked down the screen with a break between. */
.doc-pages { display: grid; gap: 0; width: 100%; justify-items: center; }
.doc-page { width: 100%; display: grid; justify-items: center; }
.doc-page-break {
  width: 100%;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 14px 0;
  color: var(--ink-soft);
  font-size: 0.8rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
.doc-page-break::before, .doc-page-break::after { content: ''; flex: 1; border-top: 2px dashed var(--line-strong); }
/* The page buttons: green when the page is in. */
.doc-page-buttons { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 12px 0 0; }
.doc-page-btn {
  min-width: 40px;
  height: 40px;
  padding: 0 12px;
  border-radius: 999px;
  border: 2px solid var(--line-strong);
  background: var(--card);
  color: var(--ink-soft);
  font: inherit;
  font-weight: 800;
  cursor: pointer;
}
.doc-page-btn.is-on { background: var(--accent); border-color: var(--accent); color: #fff; }
.doc-page-btn:hover { filter: brightness(1.05); }

.doc-blank-list { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
.doc-blank-chip {
  display: inline-flex; align-items: center; gap: 6px;
  background: var(--card); border: 2px solid var(--line-strong);
  border-radius: 999px; padding: 4px 6px 4px 12px;
  font-weight: 650; font-size: 0.86rem;
}

/* ---- the child's own overlay, while filling in blanks ------------------ */

/* The frame's aspect-ratio is set inline, per document, to the page's own
   width/height (see play.js) — so it always matches the image pixel for
   pixel with no letterboxing, and every blank's <input> can be positioned
   with plain percentage left/top/width/height computed straight from its
   stored fractional coordinates, no pixel math at render time. */
.shout-frame {
  position: relative;
  /* width:100% + height:auto + max-height:100%, together with the
     aspect-ratio set inline per document, is what makes this "contain"
     itself inside .draw-frame's flex row without distortion or overflow —
     the browser computes height from the full available width first, then,
     if that overflows the row's own height, re-derives width from the
     capped height through the same ratio (the CSS Sizing spec's own
     "transferred size" behaviour for aspect-ratio boxes). */
  width: 100%; height: auto; max-height: 100%; max-width: 100%;
  border-radius: var(--r-md); overflow: hidden;
  border: 2px solid var(--line-strong); background: var(--card-sunk);
}
.shout-frame img { display: block; width: 100%; height: 100%; }
/**
 * The child's own page SCROLLS rather than being squashed into the screen.
 *
 * Adam, from a real test: "there is now a critical problem when the teacher
 * presses Go. I've just tested this, and the whole PDF document has been
 * squashed onto a single browser page. The student should be able to scroll
 * up and down the A4 page without any problem." An A4 sheet shrunk to fit an
 * iPad in landscape leaves text a few pixels high with a box to type into
 * that nobody can aim at.
 */
main#screen-shout .draw-frame {
  overflow: auto;
  align-items: flex-start;
  justify-content: center;
}
main#screen-shout .shout-frame {
  max-height: none;
  flex: none;
}
/* Several pages, stacked with a break (v4.78.0). */
.shout-pages { display: grid; width: 100%; justify-items: stretch; align-content: start; }
main#screen-shout .shout-pages { flex: none; max-width: 100%; }
.shout-page-break {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 0;
  color: var(--ink-soft);
  font-size: 0.8rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
.shout-page-break::before, .shout-page-break::after { content: ''; flex: 1; border-top: 2px dashed var(--line-strong); }

/* The reveal, side by side and much bigger. Adam: "when the students are
   finished, their documents should be side by side, much bigger than at
   present, so they can see what's been written in each box. At the moment,
   they're so small they can't see anything." Each copy takes half the width
   and as much height as it needs; the sheet behind them scrolls. */
.shout-side-by-side { grid-template-columns: repeat(auto-fit, minmax(360px, 1fr)); }
#result-shout .sheet { width: min(1800px, 96vw); }
#result-shout .shout-frame { max-height: none; }
/* THE BOX IS EXACTLY THE BOX THE TEACHER DREW (v4.70.0). Adam, from a real
   lesson: the typing boxes came out "much larger than drawn, covering two
   lines instead of one" — the game was unplayable. The cause was the site's
   own `input[type='text']` rule (min-height: var(--tap), 12px padding, a 2px
   border), which outranks a plain class and turned every 40px box on an A4
   page into a 56px one. Hence the selector's weight here, and the explicit
   min-height, padding and border. The text is left-aligned — "left-align text
   in the typing blocks" — and sized to the box by fitShoutBlanks in play.js
   (--blank-font), because a box is whatever height the teacher drew. */
input.shout-blank[type='text'] {
  position: absolute;
  min-height: 0;
  border: none; border-radius: 4px;
  background: #fff; box-shadow: inset 0 0 0 2px var(--brand);
  font: inherit; font-size: var(--blank-font, 1rem); line-height: 1;
  padding: 0 0.4em; text-align: left;
}
input.shout-blank[type='text']:focus { box-shadow: inset 0 0 0 3px var(--accent); outline: none; }

.shout-side-by-side {
  display: grid; gap: 18px; grid-template-columns: 1fr;
}
@media (min-width: 660px) { .shout-side-by-side { grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); } }
/* An answer drawn back onto the page (the reveal, and the dashboard's
   thumbnails). Ordinary weight and sized to the BOX it fills, wrapping
   inside it — Adam (v4.75.0): "the text appears far too big and far too
   bold. Please make sure that all of the text that the students have entered
   appears on the pages." fitShoutAnswers in play.js / teacher.js sets
   --blank-font from the box's own height and shrinks it until the words fit. */
.shout-answer-text {
  position: absolute; display: flex; align-items: center; justify-content: flex-start;
  font-weight: 500; text-align: left; overflow: hidden; white-space: normal; overflow-wrap: anywhere;
  background: rgba(255, 255, 255, 0.92); border-radius: 4px;
  font-size: var(--blank-font, clamp(0.55rem, 1.6vw, 0.9rem)); line-height: 1.1; padding: 0 0.3em;
  box-sizing: border-box;
}

/* ====================================================================== */
/* Backs to the Board                                                      */
/* ====================================================================== */
/* The fourth game. Two things drive nearly every choice here: the card has
   to read from the back of a classroom (it is often on a television with
   children several metres away), and the three buttons have to be hittable
   without looking, by a child holding an iPad while talking at speed. So
   the card is as large as the space allows and the buttons are big, far
   apart, and colour-coded the way Adam described them — a green "Correct —
   next word" and a "big red button" to pass. */

.backs-field {
  display: block;
  font-weight: 700;
  font-size: 0.95rem;
}
.backs-field input,
.backs-field textarea {
  width: 100%;
  margin-top: 6px;
}
.backs-rule {
  border: 0;
  border-top: 1px solid var(--line);
  margin: 18px 0;
}

/* ---- the set-up screen ---- */

.backs-setup-grid {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 20px;
  align-items: start;
}
/* On the set-up bar's right, the QR outermost and the code to its left
   (v4.70.0) — Adam: "Reposition the QR code and the game code… so they are
   on the top toolbar on the right-hand side." */
.backs-join {
  display: flex;
  flex-direction: row-reverse;
  gap: 14px;
  align-items: center;
  text-align: right;
}
.backs-join > div { display: flex; flex-direction: column; align-items: flex-end; }
.backs-join img {
  border-radius: var(--r-sm);
  background: #fff;
  width: clamp(96px, 14vh, 132px);
  height: clamp(96px, 14vh, 132px);
}
.backs-join .code-big {
  display: block;
  line-height: 1.1;
  font-size: clamp(2rem, 4.5vw, 3.2rem);
}
.backs-setup-topline-right .backs-join { margin-left: 8px; }
.backs-team-row {
  display: flex;
  gap: 14px;
  align-items: flex-end;
  flex-wrap: wrap;
}
.backs-team {
  display: grid;
  gap: 4px;
  flex: 1 1 12rem;
}
.backs-team input {
  font-weight: 700;
}
.backs-score {
  font-size: 1.6rem;
  line-height: 1;
}
.backs-setting-row {
  display: flex;
  gap: 18px;
  align-items: flex-end;
  flex-wrap: wrap;
}
.backs-setting-row label {
  display: grid;
  gap: 4px;
}
.backs-setting-row input {
  width: 7rem;
}
/* Teams, timer and words side by side, and the screen itself wider than the
   app's others to fit them. Adam: "it's all very narrow on my screen, and I
   have to scroll still to reach some of the features… you could have the
   teams, the timer, and the choose words boxes next to each other, one by
   one." Everything needed to start a game is now above the fold on a laptop;
   the picture library below is the one thing that unfolds. */
.backs-setup-screen .topline,
.backs-setup-screen > .card,
.backs-setup-screen .backs-go-bar {
  max-width: min(96vw, 86rem);
  margin-left: auto;
  margin-right: auto;
}
.backs-setup-screen .topline { margin-bottom: 6px; }
/* Everything a lesson needs is on this one screen, so it is drawn a little
   tighter than the app's other cards. */
.backs-setup-screen > .card { padding: 16px; }
/* The game's own logo, centred in the bar regardless of how wide the
   back-link and the Go/Pop-out group either side of it are — a plain flex
   row can only centre something between its immediate neighbours, not in
   the bar as a whole, which is why this is a grid instead. */
/* The logo gives this bar its height now, so the bar's own padding is only
   there to keep the back-link off the edge — every pixel of it comes
   straight off the set-up screen, which has to fit on one screen without
   scrolling (Adam, from a lesson: "I need to scroll up and down to fully
   access the game"). */
.backs-setup-topline {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  padding-top: 4px;
  padding-bottom: 4px;
}
/* .wrap ends every page with a 64px gutter so a scrolling screen does not
   finish flush against the window. This one never scrolls — it is measured
   in the browser run — and that gutter was most of what a bigger logo had
   to find room for. */
.backs-setup-screen {
  margin-bottom: -56px;
}
.backs-setup-logo {
  justify-self: center;
  /* Adam has asked for this twice — "the logo on the teacher's dashboard is
     too small. Make it bigger", and then again once it was 68px. It is the
     name of the game on the screen a teacher runs the whole lesson from, so
     it can afford to be the biggest thing in that bar; the back-link and
     the Go group either side are short and set their own height. */
  height: clamp(64px, 10vw, 116px);
}
/* The builder's own "type a word" box, at the top of the middle column and
   above the cards — Adam's own order: "a box at the top of the central
   column, to add individual typed words. Before the card below it gives me
   the option to add words from the pictures." */
.backs-type-word {
  padding: 10px 12px;
  margin-bottom: 10px;
}
.backs-type-word .field { margin-bottom: 6px; }
.backs-type-word input { flex: 1; min-width: 0; }

/* A typed word in the "In this set" column has no picture to show, so it
   gets a plain marker in the space one would have taken — otherwise its row
   sits out of line with every card's. */
.backs-chosen-typed {
  width: 26px;
  height: 26px;
  flex: none;
  display: grid;
  place-items: center;
  border-radius: 4px;
  background: var(--card-sunk);
  color: var(--ink-faint);
  font-size: 0.72rem;
  font-weight: 800;
}

/* The one way into the builder for a set that already exists, said quietly
   beside the heading rather than as a button on every card. */
.backs-sets-hint {
  font-style: italic;
  font-size: 0.82rem;
  color: var(--ink-faint);
}
.backs-setup-topline-right {
  display: flex;
  align-items: center;
  gap: 8px;
  justify-self: end;
  /* Pinned to the third column, and the logo to the second, so a bar whose
     back link happens to be hidden (Pairs' regroup stage has two of them
     hidden at once) does not slide everything left into the empty column. */
  grid-column: 3;
}
.backs-setup-topline > .backs-setup-logo { grid-column: 2; }
.backs-go-top { display: flex; gap: 8px; }
.backs-setup-screen .backs-go-bar { margin-top: 10px; }
/* The top row: the teams, the clock, and what a card shows. */
.backs-setup-cols {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 16px;
  align-items: stretch;
}
/* The row underneath: the two ways to fill a game, side by side. */
.backs-source-cols {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.35fr);
  gap: 16px;
  align-items: stretch;
  margin-top: 12px;
}
/* Hot Seat's Go is in the topline, so this bar has nothing in it — and an
   empty bar should take no room. */
.backs-go-bar:empty { display: none; }
@media (max-width: 900px) {
  .backs-source-cols { grid-template-columns: 1fr; }
}
/* The box a teacher types their own words into — the main way words get into
   this game, so it is given real room rather than a single line. */
.backs-typed-words {
  flex: 1;
  min-height: clamp(96px, 11dvh, 190px);
  resize: vertical;
  font: inherit;
  line-height: 1.45;
  padding: 10px 12px;
  border: 2px solid var(--line-strong);
  border-radius: var(--r-sm);
  background: var(--card);
  color: var(--ink);
}
.backs-typed-words::placeholder { opacity: 0.5; }
.backs-panel {
  display: grid;
  gap: 10px;
  align-content: start;
  background: var(--card-sunk);
  border-radius: var(--r-md);
  padding: 14px;
}
@media (max-width: 1100px) {
  .backs-setup-cols { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
}
@media (max-width: 700px) {
  .backs-setup-cols { grid-template-columns: 1fr; }
}

/* The three headings — The teams, Timer, Your words — line up across the row,
   which they did not while one of them shared its line with a button. Every
   panel opens with a header row of the same height whether it has a button in
   it or not. */
.backs-panel > .row:first-child,
.backs-panel > h3:first-child {
  min-height: 40px;
  display: flex;
  align-items: center;
  margin: 0;
}

/* Two ways to fill a game, one in play at a time — the other says so rather
   than looking broken. Adam: "the 'Your words' box should only be for the
   words the teacher wants to manually input. It should not be the total of
   all words selected." */
/* "Let's have a thicker blue outline highlight the whole card when that card
   is the active one for the game." The card IS the control: pressing it makes
   it the one the game is played from, and the outline is the only thing that
   says so. */
.backs-source {
  border: 3px solid transparent;
  cursor: pointer;
  transition: border-color 0.12s ease, background 0.12s ease;
}
.backs-source:hover { border-color: color-mix(in srgb, var(--sq-blue) 35%, transparent); }
.backs-source.is-live {
  border-color: var(--sq-blue);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--sq-blue) 18%, transparent);
}
/* Anything inside that does its own thing keeps a normal pointer. */
.backs-source button,
.backs-source input,
.backs-source textarea,
.backs-source label { cursor: auto; }
.backs-source button,
.backs-source .backs-set-open,
.backs-source .backs-all-toggle,
.backs-source .backs-new-set { cursor: pointer; }

/* Two options, one dot. */
.backs-radio {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  border-radius: var(--r-sm);
  font-weight: 700;
  cursor: pointer;
}
.backs-radio:hover { background: var(--card); }
.backs-radio input {
  appearance: none;
  width: 22px;
  height: 22px;
  margin: 0;
  border: 2px solid var(--line-strong);
  border-radius: 50%;
  background: var(--card);
  display: grid;
  place-items: center;
  cursor: pointer;
}
.backs-radio input::after {
  content: '';
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--go);
  transform: scale(0);
  transition: transform 0.1s ease;
}
.backs-radio input:checked { border-color: var(--go); }
.backs-radio input:checked::after { transform: scale(1); }
.backs-radio.is-off { opacity: 0.42; cursor: not-allowed; }
.backs-radio.is-off input { cursor: not-allowed; }
.backs-panel-rule {
  border: none;
  border-top: 1px solid var(--line);
  margin: 6px 0 2px;
  width: 100%;
}

/* The naming sheet — small, and about one thing. */
/* Adam: "this dialogue box does not seem consistent with the design
   language... a nice rectangular box with curved edges." `.sheet` on its own
   is just a shadow and a fill — every other "box" in this app (game cards,
   dash cards, the word-set tiles above) traces its rounded corners with a
   visible border, which is what actually reads as "curved" rather than
   "cut out of a rectangle." Narrow width plus that missing border made this
   one and its delete-confirm twin the odd ones out. */
.backs-name-sheet {
  width: min(28rem, 100%);
  display: grid;
  gap: 4px;
  border: 3px solid var(--line);
}
.backs-name-sheet .backs-field { margin-top: 10px; }
/* Adam: "make this something a little bit bigger that I can type into." It
   was actually the plain unstyled browser default the whole time — missing
   `type="text"` in the markup meant it never matched this app's own
   `input[type='text']` rule at all — fixed there too, and sized up further
   on top of that fix since a dialog with one single field can afford it. */
.backs-name-sheet input {
  width: 100%;
  padding: 14px 16px;
  font-size: 1.1rem;
}

/* ---- choosing the words, on the set-up screen itself -------------------- */
/* Adam: "alongside where it says 'Choose Words,' I want to have all verbs,
   all nouns, and all adjectives available to highlight or dehighlight. I want
   a collapsible box below it that allows me to select individual words from
   different word groups." So: three big lit-or-not toggles at the top, and
   everything else folded away behind one summary line, because the common
   case in a lesson is "all nouns" and nothing more. */
.backs-words-panel {
  display: grid;
  gap: 8px;
  align-content: start;
}
/* The whole lists and a teacher's own sets are one wrapping row of small
   cards: from here they are the same thing — something to put into play in
   one press — and the set-up screen has to stay one screenful. */
.backs-set-strip {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: stretch;
  /* Belt and braces alongside `align-items: stretch` above: Safari does not
     stretch a flex item through an ancestor with `display: contents` (the
     two-groups wrapper below), so every tile in this row also gets this
     exact height set directly — see the three rules that reference it. */
  --backs-tile-h: 62px;
}
/* "Let's move that green box to the left of the word sets and simply have
   'New set' with a plus button next to it." */
.backs-new-set {
  display: grid;
  justify-items: center;
  gap: 0;
  height: var(--backs-tile-h);
  padding: 8px 16px;
  border: none;
  border-radius: var(--r-md);
  background: var(--brand-dark, #2b5b4d);
  color: #fff;
  font: inherit;
  cursor: pointer;
}
.backs-new-set:hover { filter: brightness(1.08); }
.backs-new-set-label { display: flex; align-items: center; gap: 10px; }
/* Centred in the box (v4.77.0) — Adam: "the text for 'New Set' and the plus
   button should be in the middle of the box. At the moment, it's aligned
   towards the top." The invisible second line that matched the other
   tiles' two-line height was holding the label up; the tile has its own
   fixed height, so the line is simply not drawn. */
.backs-new-set { align-content: center; }
.backs-new-set .tiny { display: none; }
.backs-new-set-plus {
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.22);
  font-weight: 800;
  line-height: 1;
}
/* The two groups take part in the strip's own wrapping rather than each
   being a row of its own. */
.backs-all-toggles,
.backs-set-row { display: contents; }
@media (max-width: 620px) {
  .backs-all-toggles { grid-template-columns: 1fr; }
}
/* Lit means chosen. There is no third state and nothing remembers having
   been pressed — a toggle is on precisely when every word on its list is in
   the selection, which is what lets "all nouns plus three verbs" show
   honestly as one lit toggle and a "+ 3 more" on the summary line. */
.backs-all-toggle {
  display: grid;
  gap: 0;
  justify-items: center;
  height: var(--backs-tile-h);
  padding: 8px 16px;
  border: 2px solid var(--line);
  border-radius: var(--r-md);
  background: var(--card);
  color: inherit;
  font: inherit;
  cursor: pointer;
  transition: transform 0.06s ease, border-color 0.12s ease, background 0.12s ease;
}
.backs-all-toggle b { font-size: 0.95rem; }
.backs-all-toggle .tiny { opacity: 0.6; }
.backs-all-toggle:hover { border-color: var(--accent); }
.backs-all-toggle:active { transform: scale(0.98); }
.backs-all-toggle.on {
  border-color: var(--go);
  background: color-mix(in srgb, var(--go) 16%, var(--card));
}
.backs-all-toggle.on .tiny { opacity: 0.8; }

#backs-set-name { font-size: 1.05rem; }

/* A teacher's own sets, ready to deploy in one press, alongside the three
   whole lists above them. */
.backs-set-row {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}
/* Compact enough to sit in the same row as the whole lists, since a teacher
   picks one of these at a glance rather than browsing them. */
.backs-set-row .backs-set-card {
  flex: 0 0 auto;
  margin: 0;
  padding: 6px 10px;
}
/* "I want to simply have the name of the set of words and how many words
   there are below." One box per set, with Delete inside it rather than
   hanging off its edge. */
.backs-set-row .backs-set-card.is-mine {
  border: 2px solid var(--line-strong);
  background: var(--card);
}
.backs-set-row .backs-set-open {
  display: grid;
  justify-items: center;
  gap: 0;
  padding: 0;
  border: none;
  background: none;
  border-radius: 0;
}
/* The cross in the corner — inside it, so a row of sets does not have little
   circles hanging between the cards. The padding stays even on both sides so
   the name and the count sit in the middle of the card: Adam, on a set of his
   own, "the customised set appears to be left-aligned, and it is not
   appearing in the centre of the box." */
.backs-set-row .backs-set-card.is-mine { position: relative; padding: 8px 22px; height: var(--backs-tile-h); }
.backs-set-row .backs-set-card.is-mine .backs-set-open { width: 100%; text-align: center; }
.backs-set-remove {
  position: absolute;
  top: 3px;
  right: 3px;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  border: none;
  background: none;
  color: var(--ink-soft);
  font-size: 1rem;
  line-height: 1;
  display: grid;
  place-items: center;
  cursor: pointer;
  padding: 0;
}
.backs-set-remove:hover { background: var(--danger-tint); color: var(--sq-red); border-radius: 50%; }
/* "When I highlight a set, I should have the option to edit that set."
   Only on the lit one, and small — the set's own name is what a teacher is
   reading along this row, not a row of buttons. */
.backs-set-row .backs-set-edit {
  flex: none;
  align-self: center;
  padding: 3px 12px;
  min-height: 0;
  font-size: 0.75rem;
}
.backs-set-row .title { font-size: 0.95rem; }
.backs-set-row .backs-set-card.on {
  border-color: var(--go);
  background: color-mix(in srgb, var(--go) 14%, var(--card));
}
/* Delete sits inside the card rather than under it, so a row of sets is one
   row tall. */
.backs-set-row .backs-set-card { display: flex; align-items: center; gap: 8px; padding: 8px 14px; }
.backs-set-row .doc-card-actions { padding: 0; margin: 0; }
.backs-set-row .doc-edit { font-size: 0.72rem; opacity: 0.7; }

/* ---- the card builder, on its own screen ------------------------------- */
/* "The categories of the words are arranged in the column on the left, and
   then there is a card on the right. When I click on a category of words,
   then the pictures available and the words available appear in a card on
   the right." */
.backs-picker-screen .topline,
.backs-picker-screen > .card {
  max-width: min(96vw, 86rem);
  margin-left: auto;
  margin-right: auto;
}
.backs-picker-screen .topline { margin-bottom: 14px; }
/* Three columns, Adam's own layout: "the left-hand column is the different
   categories... you can shrink this down and make it a bit smaller. At the
   moment, this takes up a lot of space, and it doesn't need to... The
   central column, which takes up most of the screen, will then be all of
   the words. On the right, I want there to be a current list of words from
   top to bottom that are in that set." */
.backs-picker-cols {
  display: grid;
  grid-template-columns: minmax(0, 11rem) minmax(0, 1fr) minmax(0, 15rem);
  gap: 14px;
  align-items: start;
}
@media (max-width: 1080px) {
  .backs-picker-cols { grid-template-columns: minmax(0, 10rem) minmax(0, 1fr); }
  .backs-picker-chosen { grid-column: 1 / -1; }
}
@media (max-width: 820px) {
  .backs-picker-cols { grid-template-columns: 1fr; }
}
.backs-picker-side {
  display: grid;
  gap: 8px;
  align-content: start;
}
/* "Add a card below those three boxes that counts how many total words
   have been selected so far." */
.backs-picker-total {
  border: 2px solid var(--line);
  border-radius: var(--r-md);
  background: var(--card-sunk);
  padding: 7px 10px;
  font-size: 0.85rem;
  font-weight: 700;
  text-align: center;
}

/* The set as it stands, on the right. It scrolls inside itself rather than
   making the screen taller — the same rule the category column follows. */
.backs-picker-chosen {
  display: grid;
  gap: 8px;
  align-content: start;
  background: var(--card-sunk);
  border: 2px solid var(--line);
  border-radius: var(--r-md);
  padding: 10px;
}
.backs-chosen-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
}
.backs-chosen-list {
  display: grid;
  gap: 5px;
  max-height: 54dvh;
  overflow-y: auto;
  padding-right: 2px;
}
.backs-chosen-row {
  display: flex;
  align-items: center;
  gap: 8px;
  background: var(--card);
  border: 2px solid var(--line);
  border-radius: var(--r-sm);
  padding: 4px 6px;
}
.backs-chosen-img {
  width: 26px;
  height: 26px;
  object-fit: contain;
  background: #fff;
  border-radius: 4px;
  flex: none;
}
.backs-chosen-word {
  flex: 1 1 auto;
  min-width: 0;
  font-size: 0.9rem;
  font-weight: 700;
  overflow-wrap: anywhere;
}
.backs-chosen-remove {
  flex: none;
  appearance: none;
  border: none;
  background: none;
  color: var(--ink-soft);
  font-size: 1.1rem;
  line-height: 1;
  padding: 2px 4px;
  border-radius: 50%;
  cursor: pointer;
}
.backs-chosen-remove:hover { background: var(--danger-tint); color: var(--sq-red); }
.backs-chosen-empty { margin: 0; }
/* The categories are a column here, not the wrapped strip they were when
   they sat above the words. */
.backs-picker-side .backs-cats {
  display: grid;
  gap: 6px;
  margin: 0;
  /* Eighteen categories are taller than a laptop screen; the column scrolls
     inside itself rather than making the whole page do it. */
  max-height: 54dvh;
  overflow-y: auto;
  padding-right: 4px;
}
/* Adam: "each of the buttons is very big. You can make it a little bit
   smaller." The category column is a list to run an eye down, not a row of
   primary controls, so it is drawn smaller than a .backs-cat is anywhere
   else. */
.backs-picker-side .backs-cat {
  width: 100%;
  justify-content: flex-start;
  text-align: left;
  padding: 4px 10px;
  font-size: 0.82rem;
  border-width: 1px;
  border-radius: var(--r-sm);
}
.backs-picker-side .backs-cat .backs-cat-count {
  font-size: 0.72rem;
  padding: 0 5px;
}
/* Three parts of speech, as three plain boxes rather than tabs in a tray.
   Adam, on the tray: "as I select these, the oblong of the option I've
   selected bleeds into the edge of the oblong that groups the three options
   together. I want you to rearrange this." A control that has to sit inside
   another control's padding will always look like that at some width; these
   are simply three buttons in a row of their own. */
.backs-pos-seg {
  display: grid;
  /* Wraps rather than forcing three across. The column they sit in got
     narrower when it became a third of a three-column screen, and
     "Adjectives" is simply a longer word than the space three-across
     leaves — it wraps to a line of its own instead of being clipped. */
  grid-template-columns: repeat(auto-fit, minmax(4.4rem, 1fr));
  gap: 6px;
  background: none;
  padding: 0;
}
.backs-pos-seg button {
  appearance: none;
  display: grid;
  justify-items: center;
  align-content: center;
  gap: 0;
  font: inherit;
  /* "Adjectives almost protrudes outside its box" — small enough that the
     longest of the three has room to breathe, with the wrapping above
     doing the rest of the work. */
  font-size: 0.78rem;
  font-weight: 700;
  line-height: 1.15;
  color: var(--ink);
  border: 2px solid var(--line-strong);
  border-radius: var(--r-sm);
  background: var(--card);
  padding: 10px 6px;
  min-height: 40px;
  cursor: pointer;
}
.backs-pos-seg button[aria-pressed='true'] {
  border-color: var(--accent);
  background: var(--accent-tint);
}
.backs-picker-main {
  display: grid;
  gap: 12px;
  align-content: start;
  background: var(--card-sunk);
  border-radius: var(--r-md);
  padding: 16px;
  min-width: 0;
}
.backs-picker-main .backs-search { margin-top: 0; }
.backs-picker-main .backs-word-tools { margin-bottom: 0; }
.backs-add-typed { display: flex; }
.backs-show-seg button { padding: 8px 14px; }

/* The set-up screen's team list — any number of them, each a name box, a
   score and (once there are more than the minimum) a way to take it out.
   Stacked one under another rather than wrapped across a grid, so adding a
   team simply extends the column: Adam's own call, along with bigger name
   boxes than the cramped ones they replaced. */
.backs-team-list {
  display: grid;
  gap: 10px;
}
.backs-team-list .backs-team {
  display: flex;
  align-items: center;
  gap: 10px;
}
.backs-team-list .backs-team input {
  flex: 1;
  min-width: 0;
  font-size: 1.05rem;
  font-weight: 700;
  padding: 12px 14px;
}
.backs-team-list .backs-score { font-size: 1.4rem; min-width: 1.6ch; text-align: right; }

/* The timer, as presets rather than a number box. */
.backs-timer-choice { display: grid; gap: 6px; }
/* "In the time box, we have the different options for how long the game will
   be displayed in oblongs I can highlight as I select." Squared-off boxes in
   a grid rather than pills in a pill-shaped tray, so the chosen one reads as
   filled in rather than as a tab. */
.backs-secs-seg {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 8px;
  background: none;
  padding: 0;
}
.backs-secs-seg button {
  border: 2px solid var(--line-strong);
  border-radius: var(--r-sm);
  background: var(--card);
  padding: 10px 8px;
  min-height: 46px;
}
.backs-secs-seg button[aria-pressed='true'] {
  border-color: var(--go);
  background: color-mix(in srgb, var(--go) 16%, var(--card));
  box-shadow: none;
}
/* The other half of that card: what a card puts on the board. */
.backs-show-seg { display: grid; gap: 2px; }


/* The console's own "now" and "next". Next is deliberately smaller — it is
   the teacher's own look ahead, not the thing the class is describing. */
.backs-nownext {
  display: grid;
  grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
  gap: 14px;
  flex: 1 1 auto;
  min-height: 0;
}
.backs-nownext .backs-now,
.backs-nownext .backs-next {
  display: flex;
  flex-direction: column;
  gap: 6px;
  min-height: 0;
}
/* "Make the words 'now' and 'next' in bold and in red type on the teacher's
   control screen." They label the two cards a teacher's eye jumps between
   mid-turn, and were the same grey as everything else. */
.backs-nownext .eyebrow {
  font-weight: 800;
  font-size: 1rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--sq-red);
}
.backs-card-small { min-height: 0; }
.backs-card-small .backs-card-word { font-size: clamp(1.1rem, 2.4vw, 1.8rem); }

/* The next five, in the order they will come, draggable into another one. */
.backs-next-stack {
  display: grid;
  gap: 8px;
  align-content: start;
  min-height: 0;
  overflow-y: auto;
}
.backs-next-card {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  border: 2px solid var(--line);
  border-radius: var(--r-sm);
  background: var(--card);
  cursor: grab;
}
.backs-next-card:hover { border-color: var(--brand); }
.backs-next-card.is-dragging { opacity: 0.45; cursor: grabbing; }
.backs-next-grip { color: var(--ink-soft); letter-spacing: -2px; }
.backs-next-thumb {
  width: 34px;
  height: 34px;
  object-fit: contain;
  background: #fff;
  border-radius: 6px;
}
.backs-next-card b { overflow-wrap: anywhere; }
.backs-choose-more { justify-self: start; }

.backs-own-word {
  display: flex;
  gap: 8px;
  align-items: center;
  margin-bottom: 10px;
}
.backs-own-word input { flex: 1; min-width: 0; }
.backs-upcoming-group { margin-bottom: 12px; }
.backs-upcoming-group h4 {
  margin: 0 0 6px;
  font-size: 0.82rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--ink-soft);
}
.backs-upcoming {
  padding-top: 10px;
  max-height: 30dvh;
  overflow-y: auto;
}

.backs-words-chosen {
  display: grid;
  gap: 4px;
  margin-left: auto;
  justify-items: start;
}
.backs-go-bar {
  display: flex;
  gap: 12px;
}
.backs-go-bar .btn {
  flex: 1;
}

/* ---- choosing words ---- */

/* ---- choosing a set to play ---- */
.backs-sets-heading { margin: 22px 0 8px; font-size: 1rem; }
.backs-set-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
  gap: 12px;
}
/* A ready-made set is one button; one of the teacher's own is a button with
   Edit/Delete beside it, so that half is its own element rather than a
   button inside a button. */
.backs-set-card,
.backs-set-open {
  display: grid;
  gap: 8px;
  justify-items: center;
  text-align: center;
  font: inherit;
  border: 2px solid var(--line-strong);
  border-radius: var(--r-md);
  background: var(--card);
  padding: 14px 12px;
  cursor: pointer;
}
.backs-set-card.is-mine { padding: 0; border: none; background: none; gap: 0; }
.backs-set-card.is-mine .backs-set-open { width: 100%; border-radius: var(--r-md) var(--r-md) 0 0; }
.backs-set-card.is-mine .doc-card-actions { justify-content: center; gap: 14px; padding: 8px 0; }
.backs-set-card:hover,
.backs-set-open:hover { border-color: var(--brand); }
.backs-set-card .title,
.backs-set-open .title { font-weight: 800; }
.backs-set-thumbs {
  display: flex;
  gap: 4px;
  align-items: center;
  justify-content: center;
  min-height: 42px;
}
.backs-set-thumbs img {
  width: 38px; height: 38px; object-fit: contain;
  background: #fff; border-radius: 6px;
}
.backs-set-typed {
  font-family: var(--font-display);
  font-size: 1.6rem;
  font-weight: 800;
  opacity: 0.5;
}

/* Searching the whole library. Six hundred and sixty-nine words is more than
   anyone should have to page through by category to find "hospital". */
.backs-search {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 12px;
}
.backs-search input {
  flex: 1;
  min-width: 0;
}
/* A search hit says which list it came from, because the same word can be on
   two of them with two different pictures — "fish" the animal and "fish" the
   thing you do — and the chip alone cannot tell them apart. */
.backs-search-hit {
  display: flex;
  flex-direction: column;
  gap: 2px;
  align-items: flex-start;
}
.backs-hit-tag {
  font-size: 0.68rem;
  opacity: 0.65;
  padding-left: 4px;
}
.backs-cats {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 12px 0 14px;
}
.backs-cat {
  border: 2px solid var(--line-strong);
  background: var(--card);
  color: var(--ink);
  border-radius: 999px;
  padding: 7px 14px;
  font: inherit;
  font-weight: 700;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 7px;
}
.backs-cat.on {
  background: var(--sq-blue);
  border-color: var(--sq-blue);
  color: #fff;
}
.backs-cat-count {
  background: rgba(0, 0, 0, 0.18);
  border-radius: 999px;
  padding: 1px 7px;
  font-size: 0.8rem;
}
.backs-cat.on .backs-cat-count {
  background: rgba(255, 255, 255, 0.3);
}
.backs-word-tools {
  margin-bottom: 10px;
}
.backs-word-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  max-height: 42dvh;
  overflow-y: auto;
}
.backs-chip {
  border: 2px solid var(--line-strong);
  background: var(--card);
  color: var(--ink);
  border-radius: var(--r-sm);
  padding: 6px 11px;
  font: inherit;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 7px;
}
.backs-chip.on {
  background: var(--sq-green);
  border-color: var(--sq-green);
  color: #fff;
}
.backs-chip-img {
  width: 26px;
  height: 26px;
  object-fit: contain;
  background: #fff;
  border-radius: 4px;
}

/* ---- the live turn, shared by all three screens that show it ---- */

.backs-stage,
.backs-play-stage {
  display: flex;
  flex-direction: column;
  gap: 12px;
  min-height: 0;
}
.backs-stage {
  max-width: 60rem;
  margin: 0 auto;
  width: 100%;
}
/* The live turn is one card, not a stack of things on the page background. */
.card.backs-stage { gap: 14px; }

/* Adam: "on the teacher's dashboard mid-game, I need to scroll up and down
   to fully access the game. Change this so that it all fits on one screen
   without the need to scroll." `.backs-nownext`'s card and
   `.backs-next-stack` were both already built to shrink and to scroll
   internally (flex: 1 1 auto, min-height: 0, overflow-y: auto) — the piece
   that was missing was ever giving them a bounded ancestor to shrink
   WITHIN. Without one, flexbox has no pressure to distribute and every
   child simply rendered at its natural size, adding up to more than a
   screen. `.wrap >` scopes this to the teacher's own console: `#screen-
   backs` is also the id of the CHILD's live screen in play.html, which
   already has its own exact-viewport layout and sits outside `.wrap`
   entirely, so this must not reach it. */
.wrap > #screen-backs {
  display: flex;
  flex-direction: column;
  /* .wrap's own 20px-top/64px-bottom padding, reclaimed — there is no
     corner-pinned pill to reserve room for here, unlike the child's own
     screen this id is shared with. */
  padding-top: 0;
  height: calc(100dvh - 84px);
  min-height: 0;
}
.wrap > #screen-backs > .topline { flex: none; }
.wrap > #screen-backs > .card.backs-stage {
  flex: 1 1 auto;
  min-height: 0;
  overflow: hidden;
}
/* The played-words card (v4.108.0) sits under the stage and takes at most
   a quarter of the screen; the stage shrinks to make room, as it was built
   to. Its words scroll inside it. */
.wrap > #screen-backs > .backs-played {
  flex: none;
  max-width: 60rem;
  width: 100%;
  margin: 12px auto 0;
  max-height: 26dvh;
  display: flex;
  flex-direction: column;
  min-height: 0;
}
/* Out of the way while "Choose more…" is open — that list is the whole deck,
   played words included, and it needs the stage's room. */
.wrap > #screen-backs:has(#backs-upcoming:not(.hidden)) > .backs-played { display: none; }
.backs-played-words { display: flex; flex-wrap: wrap; gap: 8px; overflow-y: auto; min-height: 0; }
.backs-played-word {
  display: inline-flex; align-items: center; gap: 8px;
  font: inherit; cursor: pointer;
  border: 2px solid var(--line); border-radius: 999px; background: var(--card-raised);
  padding: 6px 14px 6px 8px; min-height: 40px;
}
.backs-played-word:hover { border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-tint); }
.backs-played-word .backs-next-thumb { width: 28px; height: 28px; }

/* The clock. A plain width transition rather than a CSS animation, because
   the bar is re-anchored on every broadcast (see renderBacksPlaying) and an
   animation would fight that every time it happened. */
.backs-timer {
  position: relative;
  height: 26px;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.14);
  overflow: hidden;
  flex: none;
}
.backs-timer-fill {
  height: 100%;
  width: 100%;
  background: var(--sq-green);
  transition: width 120ms linear;
}
.backs-timer-fill.low {
  background: var(--sq-red);
}
.backs-timer-text {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  font-weight: 800;
  font-size: 0.9rem;
  color: var(--ink);
}

/* Wraps rather than overflowing. Adam, once he tried six teams: "on the
   teacher's dashboard, the six teams go off the end of the screen. You're
   going to need to stack them on top of one another." Each card keeps a
   sensible minimum width and simply drops to a second row once there isn't
   room for them all in one — the console is a browser window, not a fixed
   projector shape, so wrapping (rather than shrinking, which is what the
   pop-out board does with `--teams`) is the right answer here. */
.backs-scores {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  flex: none;
}
.backs-score-card {
  flex: 1 1 clamp(150px, 30%, 240px);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  background: var(--card);
  border: 2px solid var(--line-strong);
  border-radius: var(--r-md);
  padding: 8px 14px;
  font-weight: 700;
}
.backs-score-card.on {
  border-color: var(--sq-green);
  box-shadow: 0 0 0 3px rgba(46, 158, 82, 0.22);
}
.backs-score-card b {
  font-size: 1.5rem;
  line-height: 1;
}
/* The console's own Hot Seat scoreboard, laid out like the board's. Adam,
   after running a class on it: "the numbers do not line up with the team
   names. They should be on top of one another." Side by side, every card's
   number sat at whatever x its own name's length left it at, so a row of
   them lined up with nothing. The name goes in a strip of its own across
   the top — absolutely positioned, so it costs the score/buttons line no
   height, the same fix and the same reason as the board (see
   .lp-backs.is-classic .backs-score-card). */
#backs-live-scores.is-classic .backs-score-card {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas:
    'name buttons'
    'score buttons';
  align-items: center;
  gap: 2px 10px;
  text-align: center;
}
#backs-live-scores.is-classic .backs-score-card > span {
  grid-area: name;
  font-size: 0.95rem;
  line-height: 1.1;
  overflow-wrap: anywhere;
}
#backs-live-scores.is-classic .backs-score-card > b {
  grid-area: score;
  font-size: 2rem;
  line-height: 1;
}
#backs-live-scores.is-classic .backs-score-buttons { grid-area: buttons; }

/* The card itself — the thing being described. It grows to fill whatever
   room is left and its word scales with the viewport, since this same
   markup is read from a child's lap on an iPad and from the back of a
   classroom on a television. */
/* NOTHING MAY LEAVE THIS BOX. Adam, from a live board: "the words and
   pictures sometimes don't stay in the square." They could, because the
   picture had a fixed 42dvh cap and the word was sized purely off viewport
   *width* — so on a wide, short board (Classic's middle column is short: it
   shares the height with the timer bar and the Next-word bar) the two of
   them together were simply taller than the card, and a flex column happily
   overflows rather than shrinking its children. Three things fix it
   together, and all three are needed:
     · the card clips, so an overflow can never cross the border;
     · the picture is a flex item that may shrink (min-height:0), capped by
       the room actually left rather than by a share of the viewport;
     · the word is sized by height as well as width, so a short screen
       shrinks the type instead of pushing it out of the bottom. */
.backs-card {
  /* A GRID, not a flex column, and that is the whole point. As a flex column
     the picture was the only child that could shrink, so every pixel the
     card was short by came out of the picture — at a classroom board's own
     proportions (much wider than tall) that left a postage stamp above an
     enormous word. Two rows instead: the picture gets whatever is left over,
     bounded by `minmax(0, 1fr)` so it can shrink without vanishing, and the
     word takes its natural height underneath. */
  flex: 1 1 auto;
  min-height: 0;
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
  justify-items: center;
  align-items: center;
  gap: clamp(4px, 1.2dvh, 10px);
  background: #fff;
  border: 3px solid var(--line-strong);
  border-radius: var(--r-md);
  padding: clamp(8px, 1.8dvh, 16px);
  text-align: center;
  /* The backstop. Adam, from a live board: "the words and pictures sometimes
     don't stay in the square." Whatever else happens, nothing crosses the
     border. */
  overflow: hidden;
}
/* A card with no picture (a written-word set) has only the word, which then
   belongs in the middle rather than pinned to the bottom of a blank box. */
.backs-card:not(:has(img:not(.hidden))) {
  grid-template-rows: auto;
}
.backs-card img {
  min-height: 0;
  min-width: 0;
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
  object-fit: contain;
}
.backs-card img.hidden { display: none; }
.backs-card-word {
  max-width: 100%;
  font-weight: 800;
  /* Sized by the screen's HEIGHT as well as its width. A board is usually
     much wider than it is tall, and a width-only clamp asked for type far
     bigger than the card had room for. */
  font-size: clamp(1.2rem, min(7vw, 9dvh), 4.5rem);
  /* 1.05 clipped descenders against the border on the tightest screens. */
  line-height: 1.15;
  padding-bottom: 0.06em;
  color: var(--ink);
  overflow-wrap: anywhere;
}

.backs-meta {
  display: flex;
  justify-content: space-between;
  gap: 10px;
  font-size: 0.85rem;
  opacity: 0.75;
  flex: none;
}

.backs-controls {
  display: grid;
  grid-template-columns: 1fr 2fr 1fr;
  gap: 10px;
  flex: none;
}
.backs-controls .btn {
  font-size: 1.05rem;
  padding: 16px 12px;
  font-weight: 800;
}
.backs-btn-correct {
  background: var(--sq-green);
  border-color: var(--sq-green);
  color: #fff;
}
.backs-btn-pass {
  background: var(--sq-red);
  border-color: var(--sq-red);
  color: #fff;
}
.backs-btn-pass:disabled {
  opacity: 0.45;
}
.backs-btn-back {
  background: var(--card);
  color: var(--ink);
}

.backs-over {
  text-align: center;
  background: var(--card);
  border: 3px solid var(--line-strong);
  border-radius: var(--r-md);
  padding: 18px;
}
.backs-over h2 {
  margin: 0 0 6px;
}
.backs-over-points {
  font-size: 1.4rem;
  font-weight: 800;
  margin: 8px 0 4px;
}

/* ---- the pop-out's own copy ---- */

.lp-backs {
  display: flex;
  flex-direction: column;
  gap: 14px;
  width: 100%;
  height: 100%;
  min-height: 0;
}
/* The placeholder text this used to be ("there is currently quite a lot of
   dead space above the timer") is now the game's own logo, centred the same
   way, sized against the room it has rather than a fixed height. */
.lp-backs-title {
  flex: none;
  display: block;
  margin: 0 auto;
  /**
   * Adam has asked for this three times: "the logo needs to be much bigger so
   * it fills up a lot of the height of that top toolbar", again mid-game once
   * it was 110px, and again at 132px — "make the Hot Seat logo on the game
   * board much bigger".
   *
   * What limits it is the SCOREBOARD down the side. Those cards have a
   * minimum they will not go below, so the logo gets whatever is left, and
   * that leftover grows very fast with the height of the board while barely
   * existing on a short one. Measured inside the browser run's own board at
   * three teams — the binding case, because fewer teams means BIGGER score
   * cards, which is the opposite of what you would guess and the reason two
   * attempts at this failed — the most that fits without the scoreboard being
   * pushed out of the card is about 70px on a 1280x720 board, 100px at 800,
   * 140px at 900 and 280px at 1920x1080.
   *
   * Hence `max()` of the fraction this used to be and a much steeper line:
   * unchanged up to about 850px tall, where there is genuinely no room going
   * spare, and half again as big on a classroom projector, where there is.
   */
  height: clamp(60px, max(12.5dvh, calc(45dvh - 280px)), 210px);
  /* A wordmark is wide. On any real projector the height is what binds, but
     a narrow window must letterbox it rather than squash it. */
  max-width: 92%;
  object-fit: contain;
}
/* v4.67.0: the logo moved into the scoreboard's column, beside the timer, so
   the arithmetic above no longer binds it — the row is as tall as the logo
   and takes nothing from the score cards. It is the width of that column
   that limits it now, which is why it is sized by width. */
.lp-backs-top {
  flex: none;
  display: grid;
  grid-template-columns: clamp(180px, 22vw, 320px) minmax(0, 1fr) auto;
  gap: clamp(12px, 2vw, 26px);
  align-items: center;
}
.lp-backs-top .lp-backs-title {
  width: 100%;
  max-width: 100%;
  height: auto;
  max-height: clamp(96px, 20dvh, 220px);
  margin: 0;
}
.lp-backs-top .lp-backs-topbar { min-width: 0; }
.lp-backs-top #lp-btn-backs-dashboard { align-self: start; white-space: nowrap; }
/* HOT SEAT'S LOGO, big and centred (v4.73.0). Adam: "the Hot Seat logo on
   the game board that is live needs to be much bigger and centralised."
   The top is two rows now — the logo across the middle, the timer under it
   — with the dashboard button pinned to the corner. The logo's height comes
   off the card; on a projector there is room, and it is what he asked for. */
.lp-backs.is-classic .lp-backs-top {
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: auto auto;
  justify-items: center;
  position: relative;
  gap: 4px;
}
.lp-backs.is-classic .lp-backs-top .lp-backs-title {
  width: auto;
  /* 22dvh on a small board — three teams' score cards still fit a
     1280x900 board under it (measured in test/e2e-backs.mjs; the binding
     case, see v4.50.1) — and half of everything above 260px tall on a
     projector, where the room is: 226px at 1080p, 300px at most.
     A tenth bigger since v4.108.0 (Adam: "the Hot Seat logo could just be
     a tiny bit bigger without disrupting any of the rest of the game
     screen"), and its bottom margin pulled in: the logo file is padded so
     its centroid sits centred on the picker tile (v4.39.2), which leaves
     the bottom 9% of its box empty — the "dead space between the Hot Seat
     logo and the board" — so that much is taken back with a negative
     margin. */
  height: var(--hs-logo);
  max-height: none;
  margin-bottom: calc(var(--hs-logo) * -0.09);
}
.lp-backs.is-classic { gap: 8px; --hs-logo: clamp(110px, max(24.5dvh, calc(50dvh - 290px)), 330px); }
.lp-backs.is-classic .lp-backs-top .lp-backs-topbar { width: 100%; }
.lp-backs.is-classic .lp-backs-top .lp-backs-corner { position: absolute; top: 0; right: 0; display: grid; justify-items: end; gap: 6px; }
.lp-backs-corner .join-mini { padding: 4px 4px 4px 14px; gap: 10px; }
.lp-backs-corner .join-mini img { width: clamp(56px, 9dvh, 96px); height: clamp(56px, 9dvh, 96px); }
.lp-backs-corner .join-mini-text b { font-size: clamp(1.2rem, 3.4dvh, 2rem); }
/* What's That Word's board (v4.87.0), Adam: "Make the QR code, the joining
   code on the website, bigger so it is roughly the same height as the
   'What's That Word' logo" — the QR is the logo's own height. */
.lp-backs.is-quickfire .lp-backs-corner .join-mini { padding: 6px 6px 6px 18px; gap: 14px; }
.lp-backs.is-quickfire .lp-backs-corner .join-mini img { width: var(--hs-logo); height: var(--hs-logo); }
.lp-backs.is-quickfire .lp-backs-corner .join-mini-text b { font-size: clamp(1.4rem, 4.4dvh, 2.8rem); }
.lp-backs.is-quickfire .lp-backs-corner .join-mini-text span { font-size: clamp(0.85rem, 1.8dvh, 1.2rem); }
/* The deck's progress, down the right (v4.73.0). */
.lp-backs-progress {
  flex: none;
  width: clamp(64px, 7vw, 96px);
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto;
  justify-items: center;
  gap: 6px;
  text-align: center;
}
.lp-backs-progress-label { font-family: var(--font-display); font-weight: 800; font-size: clamp(0.9rem, 1.6vw, 1.2rem); color: var(--ink-soft); text-transform: uppercase; letter-spacing: 0.04em; }
.lp-backs-progress-bar {
  width: clamp(28px, 3vw, 44px);
  min-height: 0;
  height: 100%;
  border-radius: 999px;
  background: var(--card-sunk);
  border: 2px solid var(--line);
  position: relative;
  overflow: hidden;
}
.lp-backs-progress-fill {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 0%;
  background: var(--go);
  border-radius: 999px;
  transition: height 400ms ease;
}
.lp-backs-progress-used { font-family: var(--font-display); font-weight: 800; font-size: clamp(0.95rem, 1.7vw, 1.5rem); line-height: 1; white-space: nowrap; }
.lp-backs-progress-sub { font-size: clamp(0.7rem, 1.2vw, 0.9rem); color: var(--ink-soft); margin-top: -4px; }
.lp-backs-progress-left { font-weight: 700; font-size: clamp(0.8rem, 1.4vw, 1.05rem); color: var(--brand-dark); }
/* Game Over, in the card. */
.lp-backs-over {
  position: absolute;
  inset: 0;
  display: grid;
  place-content: center;
  justify-items: center;
  gap: 10px;
  text-align: center;
  background: var(--card-raised);
}
.lp-backs-over-title { font-family: var(--font-display); font-weight: 900; font-size: clamp(2.4rem, 8vw, 6rem); color: var(--stop); line-height: 1; }
.lp-backs-over-sub { margin: 0; color: var(--ink-soft); font-size: clamp(1rem, 2vw, 1.4rem); }
/* What's That Word's board keeps the logo centred over everything, as it was;
   only Hot Seat has a score column to put it beside. */
.lp-backs:not(.is-classic) .lp-backs-top { grid-template-columns: minmax(0, 1fr); justify-items: center; }
.lp-backs:not(.is-classic) .lp-backs-top .lp-backs-title { width: auto; height: clamp(60px, max(12.5dvh, calc(45dvh - 280px)), 210px); }
.lp-backs:not(.is-classic) .lp-backs-top .lp-backs-topbar { width: 100%; }

/* The clock on the television. This is the one number in the whole app that
   is read from the back of a classroom by a child who is mid-sentence, so it
   is deliberately enormous — the bar underneath is the decoration and the
   number is the message, which is the opposite of the console's own copy. */
.lp-backs .backs-timer {
  height: clamp(52px, 9dvh, 96px);
}
.lp-backs .backs-timer-text {
  font-size: clamp(2rem, 6dvh, 4.5rem);
  font-variant-numeric: tabular-nums; /* so the width doesn't jitter each second */
  letter-spacing: 0.02em;
}
/* The last ten seconds. The bar has already gone red by here; this makes the
   number itself move, which is what carries across a room to someone not
   looking directly at it. Nothing animates the bar's own width — that is
   re-anchored on every broadcast and an animation would fight it. */
.lp-backs .backs-timer-fill.low + .backs-timer-text {
  animation: backs-urgent 1s steps(1, end) infinite;
}
@keyframes backs-urgent {
  0%, 55% { transform: scale(1.12); }
  56%, 100% { transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .lp-backs .backs-timer-fill.low + .backs-timer-text { animation: none; }
}
/* Once the turn is over the bar is empty and the text reads "Time's up",
   which needs to fit rather than fill. */
.lp-backs .backs-timer.over .backs-timer-text {
  font-size: clamp(1.4rem, 3.4dvh, 2.6rem);
}
.lp-backs-card {
  /* Bigger than the console's: this one is being read across a classroom.
     A minimum height, not a fixed one — it still has to be able to shrink
     inside a board that never scrolls. */
  min-height: 0;
  /* The start circle and the 3-2-1 are laid over this box rather than put
     beside it, so the button the teacher presses is in exactly the place the
     first word then appears — Adam's own description of it. */
  position: relative;
}
/* Bounded by the screen's height as well as its width, for the reason given
   on .backs-card above: a board is often much wider than it is tall. */
.lp-backs-card .backs-card-word {
  font-size: clamp(1.6rem, min(11vw, 12dvh), 9rem);
}
.lp-backs-note {
  text-align: center;
  font-size: 1.1rem;
  margin: 0;
  opacity: 0.85;
}
/* "Team 1, it's your turn." on What's That Word's board (v4.77.0) — since
   v4.87.0 in a big green circle, shown while the team's go is dealt and
   waiting on their own iPad, and gone once it is running. Adam: "put that
   in a big green circle… Whilst the game is running, remove the text." */
.lp-backs-note.is-turn {
  opacity: 1;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(1.3rem, 3.6dvh, 2.4rem);
  color: #fff;
  background: var(--sq-green);
  width: clamp(180px, 36dvh, 360px);
  aspect-ratio: 1;
  border-radius: 50%;
  display: grid;
  place-items: center;
  text-align: center;
  padding: 1em;
  margin: clamp(6px, 2dvh, 20px) auto;
  box-shadow: var(--shadow-lg);
  line-height: 1.15;
}

/* Classic's board, in the shape Adam described: scores down the left, the
   picture in the middle, one long bar along the bottom. The scoreboard is a
   column here rather than the row it is everywhere else, which is the only
   structural difference — everything inside a score card is unchanged. */
.lp-backs-body {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  gap: clamp(12px, 2vw, 26px);
}
.lp-backs-middle {
  flex: 1 1 auto;
  min-width: 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
/* Every team on the screen at once, however many there are — a projected
   board cannot be scrolled by the class. The column is a grid of equal rows
   and everything inside a row is sized against `--teams` (set by
   renderBacksBoardScores), so eight teams simply draw smaller rather than
   running off the bottom. */
.lp-backs.is-classic .lp-backs-body > .backs-scores {
  flex: 0 0 clamp(180px, 22vw, 320px);
  display: grid;
  grid-auto-rows: minmax(0, 1fr);
  gap: clamp(3px, 0.7dvh, 10px);
  overflow: hidden;
  min-height: 0;
}
/* Adam's own arrangement: "the team name and the team score should be big,
   and the + and − buttons should be on top of one another to the right of
   where the scores are currently shown." So the name and the score stack down
   the left of the card and the two buttons stack down the right of it. */
/* Adam's own arrangement, in two rounds: the name and the score both big
   with the + and − stacked beside them, and then "put the current number of
   points the children have much larger and centralise it." So the number is
   the card — centred, as big as the column will take — and the buttons are
   taken out of the flow and pinned to the right so they cannot push it off
   centre. */
/* The buttons are IN the grid, not laid over it. Adam: "the plus and minus
   buttons currently bleed outside the score box." Absolutely positioning them
   kept the number dead centre but took them out of the card's own sizing, so
   two stacked circles on a short card simply hung over the edge. In a column
   of their own the card grows to hold them, and the name and score centre
   themselves in the space that is left. */
.lp-backs.is-classic .backs-score-card {
  /* Two rows, the buttons spanning both: the name sits DIRECTLY above the
     score rather than centred across the whole card. Adam: "mid-game, the
     team score should appear directly beneath the team name. At the moment,
     they are offset." It was offset because the name was absolutely
     positioned across the full width while the number was centred in the
     score column only, which is narrower than the card by the width of the
     +/- beside it.
     An earlier version of this fix (v4.35.0) put the name in its own row
     and DID break the board at six teams — the row's height came on top of
     what score+buttons already needed. What makes it safe now is that the
     padding-top which used to reserve room for the floating name is gone:
     that space becomes the name's own row instead of being added to it. The
     browser run measures both — nothing spilling out of a card, and every
     team fitting on a short board — so a regression here is caught. */
  flex: 0 0 auto;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas:
    'name buttons'
    'score buttons';
  align-items: center;
  gap: 0 clamp(8px, 1.2vw, 16px);
  /* Scaled off the board's height as well: eight teams on a short screen
     have to fit down the side without the column having to scroll, which is
     awkward to use mid-game. */
  font-size: clamp(0.85rem, min(1.5vw, 2.4dvh), 1.5rem);
  padding: clamp(4px, 0.8dvh, 12px) clamp(10px, 1.4vw, 18px);
  text-align: center;
}
.lp-backs.is-classic .backs-score-card > span {
  grid-area: name;
  font-size: clamp(0.8rem, min(2vw, calc(9dvh / var(--teams, 3))), 1.6rem);
  line-height: 1.1;
  overflow-wrap: anywhere;
}
.lp-backs.is-classic .backs-score-card > b {
  grid-area: score;
  justify-self: center;
  font-size: clamp(1rem, min(5.5vw, calc(25dvh / var(--teams, 3))), 5rem);
  line-height: 1;
}
.backs-score-buttons {
  grid-area: buttons;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
}
/* One above the other on the board, side by side everywhere else — and side
   by side here too once there are enough teams that a row is shorter than two
   stacked circles (see renderBacksBoardScores). */
.lp-backs.is-classic .backs-score-buttons {
  grid-template-columns: auto;
  gap: clamp(6px, 1dvh, 10px);
  align-self: center;
}
.lp-backs.is-classic .backs-scores.is-tight .backs-score-buttons {
  grid-template-columns: auto auto;
}
/* Same two rows, just tighter — with the +/- side by side the card is wide
   rather than tall, so there is less room to spend on padding. */
.lp-backs.is-classic .backs-scores.is-tight .backs-score-card {
  padding: clamp(2px, 0.5dvh, 10px) clamp(8px, 1.2vw, 14px);
  gap: 0 clamp(6px, 1vw, 14px);
}
/* Big enough to hit accurately from arm's length at the front of a class,
   which is where the teacher is standing when they use them. */
/* Circles, and bigger — Adam: "make the plus and minus buttons a little bit
   bigger. Maybe make them into circles rather than rectangles so they are
   easier to click." They are pressed constantly, standing up, from arm's
   length. Allowed to come down a little on a short board rather than pushing
   the last team off the bottom of the column. */
.backs-score-buttons {
  justify-items: center;
}
.backs-score-buttons button {
  appearance: none;
  /* Grid centring, not line-height centring — Adam, at six teams on the
     board: "the plus and minus icons are ever so slightly misplaced. They are
     not perfectly in the middle of the circles." A UA default button isn't
     guaranteed to centre its own text either way, and "+"/"−" have different
     glyph metrics from each other, so `line-height` alone left them a
     hair off. `display: grid; place-items: center` plus zeroed padding pins
     the glyph to the exact centre of the box regardless of font metrics. */
  display: grid;
  place-items: center;
  padding: 0;
  border: 2px solid var(--line-strong);
  background: var(--card-sunk);
  color: var(--ink);
  border-radius: 50%;
  font: inherit;
  font-weight: 800;
  font-size: 1.5em;
  line-height: 1;
  width: clamp(46px, 8dvh, 68px);
  height: clamp(46px, 8dvh, 68px);
  cursor: pointer;
}
/* Sized against the number of teams for the same reason the numbers are. */
.lp-backs.is-classic .backs-score-buttons button {
  /* A touch smaller than they were (22dvh → 20dvh per team, v4.73.0): the
     two stacked circles are what set a score card's height at three teams,
     and that height is what the logo above has to leave room for. */
  width: clamp(30px, min(7.5dvh, calc(20dvh / var(--teams, 3))), 68px);
  height: clamp(30px, min(7.5dvh, calc(20dvh / var(--teams, 3))), 68px);
}
.backs-score-buttons button:hover { filter: brightness(0.95); }
.backs-score-buttons .plus { background: var(--accent-tint); border-color: var(--sq-green); }
.backs-score-buttons .minus { background: var(--danger-tint); border-color: var(--sq-red); }

/* The timer across the very top, with Finish beside it. */
.lp-backs-topbar {
  flex: none;
  display: flex;
  align-items: stretch;
  gap: 12px;
}
.lp-backs-topbar .backs-timer { flex: 1; }
/* "A nice big red bar beneath the word, same width as the box with the words
   in it." It is a sibling of the card inside the same column, so "same
   width" costs nothing to keep true. */
/* In the top right-hand corner under "Teacher dashboard" since v4.88.0
   (it was a full-width bar under the card); still red, a hand's width. */
.lp-backs-finish {
  flex: none;
  background: var(--sq-red);
  color: #fff;
  font-weight: 800;
  font-size: clamp(1rem, 2.6dvh, 1.5rem);
  padding: clamp(8px, 1.6dvh, 14px) clamp(18px, 3vw, 40px);
  min-width: 200px;
}
.lp-backs-corner .lp-backs-finish { justify-self: stretch; }

/* "A big green circle where the first picture is going to go, with the word
   'Start the game'." It sits exactly where the card will be, so pressing it
   and the first word appearing are the same place on the screen. */
.lp-backs-start {
  /* Inside the card, dead centre — the circle occupies the space the word is
     about to. */
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  aspect-ratio: 1;
  width: clamp(160px, 32dvh, 340px);
  max-width: 80%;
  border-radius: 50%;
  border: none;
  background: var(--sq-green);
  color: #fff;
  font: inherit;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(1.1rem, 3.4dvh, 2.2rem);
  line-height: 1.15;
  padding: 1em;
  cursor: pointer;
  box-shadow: var(--shadow-lg);
}
.lp-backs-start:hover { filter: brightness(1.05); transform: translate(-50%, -50%) scale(1.02); }
/* "Tab this down so that 'Next' is on top and 'Word' is on the bottom" —
   every turn after the first, in place of the sentence "The next word
   is…", which fought the circle's own shape trying to wrap inside it. */
.lp-backs-start-stack {
  display: grid;
  gap: 0.05em;
  line-height: 1.05;
}

/* 3… 2… 1… */
.lp-backs-count {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  font-family: var(--font-display);
  font-weight: 850;
  font-size: clamp(6rem, 34dvh, 20rem);
  line-height: 1;
  color: var(--sq-green);
}
.lp-backs-count.pop { animation: backs-count-pop 340ms ease-out; }
@keyframes backs-count-pop {
  from { transform: scale(0.55); opacity: 0.2; }
  to { transform: scale(1); opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .lp-backs-count.pop { animation: none; }
}

/* The end of a turn: the scores, big, and nothing else on the screen. */
.lp-backs-final {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: clamp(12px, 2.5dvh, 28px);
}
/* "You can have the cards side by side, as opposed to on top of one another,
   so there can be multiple teams in a single game." They share the width
   equally and wrap only when there are too many to fit. */
.lp-backs-final-scores {
  width: 100%;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: clamp(10px, 2vw, 24px);
}
/* "The team name at the top, and then the score they currently sit on quite a
   little bit bigger beneath it." Nothing else — no + or −.
   Addressed by id rather than by class: the side column's own shape (the
   rule above, three classes deep) would otherwise win on specificity and
   these cards would keep its columns. */
#lp-backs-final .backs-score-card {
  flex: 1 1 clamp(160px, 20vw, 420px);
  max-width: 30rem;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  grid-template-areas: 'name' 'score';
  justify-items: center;
  align-content: center;
  gap: clamp(4px, 1dvh, 12px);
  font-size: clamp(1.1rem, min(2.6vw, 4dvh), 2.4rem);
  padding: clamp(12px, 3dvh, 32px) clamp(14px, 2vw, 28px);
  text-align: center;
}
#lp-backs-final .backs-score-card > span {
  grid-area: name;
  overflow-wrap: anywhere;
}
#lp-backs-final .backs-score-card > b {
  grid-area: score;
  font-size: clamp(2.4rem, min(7vw, 12dvh), 7rem);
  line-height: 1;
}
/* ---- the look-back at the words a turn reached ---- */
/* One component, three sizes. The markup is built once (renderBacksReview in
   js/backs-ui.js) and used on the console, the child's own pop-up and the
   television; only the scale changes, which is why the chip sizes below are
   in em and set from each container rather than baked in here. */
.backs-review {
  display: flex;
  flex-direction: column;
  gap: 10px;
  text-align: left;
  margin-top: 12px;
  min-height: 0;
  overflow-y: auto;
}
.backs-review-group h4 {
  margin: 0 0 6px;
  font-size: 0.85em;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  opacity: 0.7;
}
.backs-review-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
.backs-review-chip {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 12px 6px 8px;
  border-radius: 999px;
  border: 2px solid var(--line-strong);
  background: var(--card);
  font-weight: 700;
  font-size: 1em;
}
.backs-review-chip img {
  width: 2.2em;
  height: 2.2em;
  object-fit: contain;
}
/* Got and missed are told apart by colour AND by the tick/cross-like weight
   of the border, never by colour alone — the same rule the rest of this app
   follows for anything a child has to read at a glance. */
.backs-review-chip.got {
  border-color: var(--sq-green);
  background: rgba(46, 158, 82, 0.1);
}
.backs-review-chip.missed {
  border-color: var(--sq-red);
  background: rgba(198, 58, 58, 0.09);
  border-style: dashed;
}
/* Classic's list: neither got nor missed, because nothing on that screen
   ever said which. Plainly neutral rather than a third colour that would
   invite reading a verdict into it. */
.backs-review-chip.shown {
  border-color: var(--line-strong);
  background: var(--card-sunk);
}
.lp-backs-review {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  font-size: clamp(1rem, 2.4dvh, 1.9rem);
}
.lp-backs-review .backs-review {
  width: 100%;
  margin-top: 0;
}

/* The child's own live screen. It sits inside the pinned, exact-viewport
   .game-body.is-live layout (see above), so the stage has to be told to take
   the space that is left — otherwise everything bunches at the top and the
   card ends up a thin strip with two-thirds of an iPad left empty under it.
   position:relative is what the corner-pinned connection pill and Leave
   button hang off, the same as #screen-game and #screen-draw. */
#screen-backs {
  position: relative;
  min-height: 0;
}
#screen-backs .backs-play-stage {
  flex: 1 1 auto;
  min-height: 0;
}
/* A reserved strip along the top for the two corner-pinned controls (the
   connection pill and Leave). Without it the full-width timer bar is drawn
   straight over the top of them — they are absolutely positioned, so they
   take no space of their own and simply end up underneath. */
#screen-backs {
  padding-top: 46px;
}

@media (max-width: 640px) {
  .backs-setup-grid {
    grid-template-columns: 1fr;
  }
  .backs-controls {
    grid-template-columns: 1fr 1fr;
  }
  /* The green button is the one being hit constantly — give it the full
     width of its own row rather than a third of a cramped one. */
  .backs-btn-correct {
    grid-column: 1 / -1;
    order: -1;
  }
}

/* =========================================================================
   WHERE'S GRANDMA?

   One picture of a house with the roof cut away, and eight rooms marked out
   over it. Every room's box is a PERCENTAGE of the picture (see
   js/grandma-house.js), never a pixel measurement, because the same house
   is drawn at whatever size a landscape iPad, a phone or a teacher's
   laptop happens to allow — a drop target in pixels would only ever be
   right on one of them.

   The rooms are deliberately invisible until a card is being dragged. A
   house covered in dashed outlines stops looking like a house, and the
   whole game is a child looking at a picture and saying where somebody is.
   ========================================================================= */

/* Scoped to the CHILD's screen by its tag, because #screen-grandma is an id
   that exists in BOTH teacher.html and play.html — the teacher's is a
   <section>, this one is a <main>. An unscoped rule here laid the teacher's
   whole live dashboard out as the child's game grid, which squashed its top
   bar into a 24px strip with its own buttons hanging out of it. The same
   trap #screen-backs has (see .wrap > #screen-backs). */
main#screen-grandma {
  position: relative;
  display: grid;
  grid-template-rows: var(--draw-status-h) minmax(0, 1fr) auto auto;
  gap: var(--game-gap);
  min-height: 0;
  /* Nothing on this screen scrolls, and every touch on it is either a tap or
     the start of a drag. Adam, on an iPad: a card could not be swiped UP off
     the tray until it had first been dragged down "to unlock it". The cards
     already had this; the house the pointer is captured to, and the gaps
     between everything, did not — and a browser that decides what a touch
     is for by looking at the element holding the capture found one that
     allowed panning. The whole screen refuses it now. play.js carries the
     touchmove guard that backs this up. */
  touch-action: none;
}

.grandma-stage {
  min-height: 0;
  min-width: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--card-sunk);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-sm);
  padding: clamp(4px, 1vw, 10px);
}

/* The house keeps its own shape at every size — the rooms are positioned
   against this box, so it has to be exactly the picture's own aspect ratio
   or every room would drift off its furniture. */
.grandma-house {
  /* One size for everybody in the house, set here rather than on each
     person: a percentage would resolve against the ROOM, so the bathroom's
     occupants would come out smaller than the garden's for no reason. The
     small copies on the teacher's dashboard and the result screen override
     this one value and nothing else. */
  --grandma-person: 4.2cqw;
  position: relative;
  /* The ratio and the two caps are the first paint and the fallback. On the
     child's board play.js measures the stage and sets the width AND height in
     pixels (layoutGrandmaHouse), because a height-bound box that keeps its
     full width letterboxes the picture and pulls every room off its walls —
     which is what Adam's iPad did. See that function for the account. */
  aspect-ratio: 1760 / 960;
  max-width: 100%;
  max-height: 100%;
  border-radius: var(--r-md);
  overflow: hidden;
  box-shadow: var(--shadow-sm);
  /* Said again here, on the element the pointer is captured to during a
     drag, rather than left to the screen's own rule above — the property is
     not inherited, and this is the element a browser that looks at the
     capturer rather than the touched card would consult. */
  touch-action: none;
}
.grandma-house-img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
  -webkit-user-select: none;
  user-select: none;
  -webkit-user-drag: none;
}

.grandma-zones {
  position: absolute;
  inset: 0;
  /* The container the person sizes are measured against. It goes HERE and
     not on .grandma-house: inline-size containment makes an element's width
     independent of its contents, and the house's width comes from its own
     picture inside a flex stage — containing it collapsed the whole board
     to nothing. This layer's width comes from inset:0, which is definite
     whatever the house turns out to be. */
  container-type: inline-size;
}

.grandma-zone {
  position: absolute;
  display: flex;
  flex-wrap: wrap;
  align-content: flex-end;
  justify-content: center;
  gap: 2px;
  padding: 2px;
  border-radius: var(--r-sm);
  border: 3px solid transparent;
  transition: border-color 0.12s ease, background 0.12s ease;
}
/* Only while something is actually being carried — see .is-dropping on the
   house itself, set by play.js for the length of one drag. */
.grandma-house.is-dropping .grandma-zone {
  border-color: rgba(63, 107, 94, 0.5);
  background: rgba(255, 255, 255, 0.32);
}
.grandma-house.is-dropping .grandma-zone.is-over {
  border-color: var(--go);
  background: rgba(46, 158, 82, 0.28);
}

/* The rooms are never named on the board. Adam: "remove the room names from
   the rooms so the children cannot remember what the room names are." That
   is the whole vocabulary this game teaches — a label on the picture hands
   it over, and a child who has read it once does not have to ask. The names
   do still appear in the look-back at the end, which is the marking rather
   than the game. */

/* Reserved from the first paint, so the house never resizes under a finger
   the moment the last card goes down — Adam: "change this so that the
   Unfinished button appears without the whole game changing shape." */
.is-invisible {
  visibility: hidden;
}

/* A person already in a room. Small, because a room can hold any number of
   them (Adam: "any number in a room") and three of them still have to fit
   inside the bathroom. */
.grandma-in-room {
  width: var(--grandma-person);
  border-radius: 50%;
  border: 2px solid #fff;
  box-shadow: var(--shadow-sm);
  aspect-ratio: 1;
  object-fit: cover;
  object-position: 50% 42%;
  background: var(--card);
  touch-action: none;
  cursor: grab;
}
/* A picture held under a finger on an iPad is an invitation to iPadOS to lift
   it into a drag-and-drop of its own, and a long press on one is a callout
   menu — both of which take the touch off this page mid-drag. Every picture
   a child can pick up declines both. */
.grandma-in-room,
.grandma-card,
.grandma-card img,
.grandma-house-img {
  -webkit-user-drag: none;
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
}

/* ---- the cards still to place ---------------------------------------- */

.grandma-tray {
  display: flex;
  gap: clamp(4px, 1vw, 10px);
  justify-content: center;
  flex-wrap: wrap;
  min-height: clamp(58px, 11vh, 96px);
}

.grandma-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: 4px 6px;
  border: none;
  border-radius: var(--r-md);
  background: var(--card-raised);
  box-shadow: var(--shadow-sm);
  font: inherit;
  color: var(--ink);
  touch-action: none;
  cursor: grab;
}
.grandma-card.is-carried { opacity: 0.35; }
.grandma-card img {
  width: clamp(34px, 7vh, 62px);
  aspect-ratio: 1;
  object-fit: cover;
  object-position: 50% 42%;
  border-radius: 50%;
  background: var(--card);
}
.grandma-card b {
  font-size: clamp(0.55rem, 1.3vw, 0.74rem);
  font-weight: 700;
  white-space: nowrap;
}

/* The card under the finger while it is being dragged — one element reused
   for every drag, parked off-screen the rest of the time. */
.grandma-drag {
  position: fixed;
  z-index: 60;
  pointer-events: none;
  width: clamp(44px, 9vh, 74px);
  aspect-ratio: 1;
  border-radius: 50%;
  border: 3px solid #fff;
  object-fit: cover;
  object-position: 50% 42%;
  box-shadow: var(--shadow-md);
  transform: translate(-50%, -50%);
}

/* ---- the result ------------------------------------------------------- */

.grandma-result-houses {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr));
  gap: 12px;
}
.grandma-result-house h3 {
  margin: 0 0 6px;
  font-size: 0.9rem;
  text-align: center;
}
.grandma-result-house .grandma-house { width: 100%; }
.grandma-result-house .grandma-house { --grandma-person: 7cqw; }
.grandma-seat-count { white-space: nowrap; }

/* ---- the teacher's own dashboard --------------------------------------
   Adam, from the first look at it: "the two game screens are too small, and
   they are different sizes." Both, and they were the same cause twice over.
   SIZE: the shared .dash grid packs three cards across a 1180px console, so
   a group's card was 365px and each house inside it about 100px — a house
   is 11:6, so a third of a third of the screen is nothing. This game's own
   dash gets far wider cards; two only fit side by side on a genuinely wide
   monitor, and on a laptop it is one group per row.
   EQUAL: the houses were bounded by `max-height: 100%` as well as width, so
   a pair's card and a three's — which are different heights — resolved the
   same house to two different sizes. Here it is width and the aspect ratio
   alone, and nothing else, so every house on the screen is identical. */
#grandma-dash {
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 620px), 1fr));
}
/* The same change, for the same reason, on Drawing Dictation's own paired
   dashboard. Adam: "I like the size of the teacher's dashboard live feed
   [in class mode]. This is how big I want the live feed to be once the
   children have been moved into pairs... and then the teacher can scroll up
   and down the children in pairs." Class mode gets one card per CHILD, so a
   drawing fills a whole card; pairs mode gets one card per GROUP with two
   or three drawings side by side inside it, which at three cards across the
   console left each drawing about a third of a third of the screen. One
   group per row on a laptop, two on a wide monitor, and the page scrolls —
   which he asked for explicitly. */
#drawing-dash {
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 620px), 1fr));
}
/* And the same divider fix: it is a left border plus 12px of padding taken
   out of that column's own content width, so the first drawing in a card
   came out 13px wider than the ones beside it — which matters much more now
   they are big enough to compare properly. */
#drawing-dash .dash-seat-col + .dash-seat-col {
  border-left: 0;
  padding-left: 0;
}
.dash-drawing-thumb .grandma-house {
  width: 100%;
  max-width: none;
  max-height: none;
  --grandma-person: 10cqw;
}
/* The shared divider between seats is a left border plus 12px of padding,
   which comes out of that column's own content width — so the first house
   in a card was 13px wider than every one after it. The gap between the
   columns already separates them here; the rule is only dropped for this
   game, where the columns hold pictures that have to be compared with each
   other and so must be identical. */
#grandma-dash .dash-seat-col + .dash-seat-col {
  border-left: 0;
  padding-left: 0;
}

/* Seven rows plus two houses plus a big score is genuinely taller than a
   landscape iPad, so this one sheet scrolls inside itself rather than
   running off the bottom of the screen with "wait for your teacher" on the
   part nobody can reach. */
#result-grandma .sheet {
  max-height: 92dvh;
  overflow-y: auto;
}

/* Everybody, and whether the group agreed about them — the actual marking. */
.grandma-over-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 6px 10px;
  border-radius: var(--r-md);
  background: var(--card-sunk);
  margin-bottom: 6px;
}
.grandma-over-row img {
  width: 34px;
  aspect-ratio: 1;
  border-radius: 50%;
  object-fit: cover;
  object-position: 50% 42%;
}
.grandma-over-row b { flex: 1; }
.grandma-over-row .mark { font-weight: 800; }
.grandma-over-row.agreed { background: var(--accent-tint); }
.grandma-over-row.agreed .mark { color: var(--go); }
.grandma-over-row:not(.agreed) .mark { color: var(--stop); }

/* =========================================================================
   SQUAREZ, WHOLE CLASS — everyone's board on the class screen

   Adam: "at the end of the game, I should be able to press a button on the
   teacher's dashboard and show all pictures at the same time on the game
   screen that is shared with the class." Thirty boards on a projector, so
   they are sized to fill the screen rather than to a fixed thumbnail size —
   the fewer children there are, the bigger each board gets.
   ========================================================================= */

.lp-class-boards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 220px), 1fr));
  /* Centred rather than hugging the top, so a small class does not sit in a
     strip along the top of the screen. The boards themselves stay SQUARE
     (see .mini below) — this is the same board each child has in front of
     them, and the whole point of putting them up is that they recognise
     their own, so the shape is not free to change. */
  align-content: center;
  gap: clamp(8px, 1.4vw, 18px);
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
}
.lp-class-board {
  background: var(--card-sunk);
  border-radius: var(--r-md);
  padding: clamp(6px, 1vw, 12px);
  border: 3px solid transparent;
}
/* The one board the rest of the class was following. */
.lp-class-board.is-leader {
  border-color: var(--accent);
  background: var(--accent-tint);
}
.lp-class-board-name {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-bottom: 6px;
  font-size: clamp(0.8rem, 1.4vw, 1.1rem);
}
.lp-class-board-name b {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.lp-class-boards .mini-board { gap: clamp(4px, 0.6vw, 10px); }
.lp-class-boards .mini {
  border-width: 2px;
  border-radius: 10px;
}
.lp-class-boards .mini .pic,
.lp-class-boards .mini svg,
.lp-class-boards .mini img {
  max-width: 62%;
  max-height: 62%;
}

/* The teacher's own two controls while playing along in whole-class mode —
   see #class-teacher-bar in play.html. Sits under the board rather than
   over it: this device is playing the same game as every other one, and
   the controls are an extra rather than the point of the screen. */
.class-teacher-bar {
  display: flex;
  gap: 10px;
  justify-content: center;
  flex-wrap: wrap;
  margin-top: 10px;
}

/* Each game's own wordmark on the teacher's console — see paintGameLogos in
   teacher.js. Sized by HEIGHT with object-fit, never by width, because the
   six logos have six different aspect ratios and they all have to sit on
   the same toolbar row without one of them setting its height. Hot Seat's
   set-up screen keeps its own bigger, centred #backs-setup-title. */
.dash-game-logo {
  flex: none;
  height: clamp(38px, 5.6vw, 72px);
  width: auto;
  max-width: 30%;
  object-fit: contain;
}

/* The game's wordmark on the projected screen, beside the title. Sized by
   height for the same reason the console's is — six logos, six shapes, one
   row. Hidden along with the rest of the head on Hot Seat's board. */
.lp-game-logo {
  flex: none;
  height: clamp(46px, 9vh, 108px);
  width: auto;
  max-width: 26%;
  object-fit: contain;
}

/* =========================================================================
   WHAT'S THAT WORD — the child's own iPad

   The iPad IS the board in this game, so it carries the two big circles the
   projector carries in Hot Seat: the green one a team presses to start, and
   the red one that says their minute is over. Both sit inside the card,
   where the picture is about to be or has just been, so nothing on the
   screen moves as they come and go.
   ========================================================================= */

.backs-start-circle,
.backs-timeup-circle {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  aspect-ratio: 1;
  width: clamp(150px, 34dvh, 320px);
  max-width: 82%;
  border-radius: 50%;
  border: none;
  display: grid;
  place-items: center;
  text-align: center;
  color: #fff;
  font-family: var(--font-display);
  font-weight: 800;
  line-height: 1.15;
  padding: 1em;
  box-shadow: var(--shadow-lg);
}
.backs-start-circle {
  background: var(--sq-green);
  font-size: clamp(0.95rem, 2.6dvh, 1.6rem);
  cursor: pointer;
  /* One centred stack, not two rows filling the circle — grid's default
     stretch pushed the team's name to the top and the instruction to the
     bottom with a hole between them. */
  grid-auto-rows: max-content;
  align-content: center;
  gap: 0.35em;
}
.backs-start-circle:hover { filter: brightness(1.05); }
/* The team's name on its own line above the instruction, so a child reads
   whose go it is first. */
.backs-start-circle b { display: block; font-size: 1.25em; }

/* "A big red circle with the words 'Time Up'." — since v4.87.0 "Turn over.
   Pass your device to Team B", the team's name on its own smaller line. */
.backs-timeup-circle {
  background: var(--stop);
  width: clamp(170px, 40dvh, 380px);
  font-size: clamp(1.5rem, 4.4dvh, 2.9rem);
  letter-spacing: 0.02em;
  padding: 0 8%;
}
.backs-timeup-circle b { display: block; }
.backs-timeup-circle small { display: block; font-size: 0.46em; font-weight: 700; margin-top: 0.4em; letter-spacing: 0; }

/* 3… 2… 1… in the same place, so the circle it replaces does not move. */
.backs-play-count {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(4rem, 22dvh, 12rem);
  color: var(--brand-dark);
  pointer-events: none;
}

/* =========================================================================
   WHAT'S THAT WORD — the projected screen

   Two faces. While a turn runs it is a scoreboard down the left and a
   growing list of the words this team has already banked. When the clock
   stops it is Adam's own summary: scores still on the left, what they got in
   the middle, what they passed on the right, and one big bar across the
   bottom naming whoever is up next.
   ========================================================================= */

.lp-backs-log {
  display: flex;
  flex-direction: column;
  gap: clamp(4px, 0.8dvh, 10px);
  align-content: start;
  min-height: 0;
  overflow-y: auto;
  /* Cards wrap across rather than stacking (v4.76.0), so a turn rarely
     needs to scroll — and when it does, Adam: "remove the scroll bar". */
  scrollbar-width: none;
  padding: clamp(8px, 1.4dvh, 16px);
  background: var(--card-sunk);
  border-radius: var(--r-lg);
}
.lp-backs-log::-webkit-scrollbar { display: none; }
.lp-backs-log-cards {
  display: flex;
  flex-wrap: wrap;
  gap: clamp(6px, 1dvh, 12px);
  align-content: flex-start;
}
.lp-backs-log-head {
  font-weight: 800;
  color: var(--ink-soft);
  font-size: clamp(0.9rem, 2dvh, 1.3rem);
}
.lp-backs-log-word {
  display: flex;
  align-items: center;
  gap: 12px;
  background: var(--card-raised);
  border-radius: var(--r-md);
  padding: clamp(6px, 1dvh, 12px) clamp(10px, 1.4vw, 18px);
  font-weight: 800;
  /* Slightly bigger cards and pictures (v4.87.0, Adam). */
  font-size: clamp(1.15rem, 3.1dvh, 2.3rem);
}
.lp-backs-log-word img {
  width: clamp(34px, 5.4dvh, 70px);
  aspect-ratio: 1;
  object-fit: contain;
  background: #fff;
  border-radius: 8px;
  flex: none;
}

.lp-backs-summary {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr));
  gap: clamp(10px, 1.6vw, 22px);
  min-height: 0;
  align-content: start;
}
.lp-backs-summary-card {
  background: var(--card-sunk);
  border-radius: var(--r-lg);
  padding: clamp(10px, 1.6dvh, 20px);
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: clamp(6px, 1dvh, 12px);
}
.lp-backs-summary-card.is-got { background: var(--accent-tint); }
.lp-backs-summary-card h2 {
  margin: 0;
  font-size: clamp(1.1rem, 3dvh, 2rem);
  line-height: 1.15;
}
.lp-backs-summary-words {
  display: flex;
  flex-direction: column;
  gap: clamp(4px, 0.7dvh, 9px);
  min-height: 0;
  overflow-y: auto;
}

/* "Across the bottom is a big bar." It is the one thing to press on this
   screen at that moment, so it is the width of the screen and unmissable. */
.lp-backs-next-team {
  flex: none;
  width: 100%;
  background: var(--sq-green);
  color: #fff;
  border-radius: var(--r-lg);
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(1.2rem, 4dvh, 2.6rem);
  padding: clamp(10px, 2dvh, 24px);
}

/* THE CIRCLE IS CENTRED ON THE CARD (v4.70.0). .backs-start-circle and the
   Time Up circle are absolutely positioned, and this card was never a
   positioned box, so they were centred on the nearest one — the whole
   screen — and sat a little off the white card. Adam: "make sure that this
   green circle is positioned perfectly in the middle of the large white
   card background." */
#backs-play-card { position: relative; }
/* The holding card before Start (v4.70.0): the game's logo, big, and one
   line — over the white card, in the middle. */
.backs-hold {
  position: absolute;
  inset: 0;
  display: grid;
  align-content: center;
  justify-items: center;
  gap: clamp(10px, 2.5dvh, 28px);
  padding: clamp(12px, 3dvh, 32px);
  background: #fff;
}
.backs-hold-logo {
  width: min(70%, 560px);
  max-height: 46dvh;
  object-fit: contain;
}
.backs-hold-text {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(1.1rem, 3.2dvh, 2rem);
  color: var(--brand-dark);
  text-align: center;
}

/* The game's own wordmark on a child's iPad — see #backs-play-logo. Kept
   deliberately small: the card underneath is what this screen is for. */
.backs-play-logo {
  display: block;
  margin: 0 auto;
  height: clamp(26px, 5.5dvh, 54px);
  width: auto;
  max-width: 45%;
  object-fit: contain;
  flex: none;
}

/* =========================================================================
   WHAT'S THAT WORD — the child's screen, laid out Adam's way

   "The word should be in the middle with the card. There should be no live
   scores. There should be a big circle with 'Correct' next to the word on
   the right-hand side. There should be two red circles to the left for each
   pass, these disappear as the words are used."

   So: passes down the left, card in the middle taking everything left over,
   Correct down the right. Both controls are beside the card rather than
   under it, so the hand that presses one never covers the word.
   ========================================================================= */

#screen-backs.is-quickfire .backs-play-stage {
  display: grid;
  /* The two side columns are the SAME width — the Correct circle's — so
     the card sits dead centre (v4.76.0). Adam: "make it so that the
     distance to the left and to the right of the game board is
     equidistant. The correct button at the moment is bigger than the two
     pass buttons, and this makes it look like it is offset." */
  --backs-side: clamp(90px, 19dvh, 200px);
  grid-template-columns: var(--backs-side) minmax(0, 1fr) var(--backs-side);
  /* The card takes the full height of the row — it is the thing being read
     from across a table — while the two controls stay circles, centred
     against it. */
  align-items: stretch;
  gap: clamp(8px, 2vw, 24px);
  min-height: 0;
}
#screen-backs.is-quickfire .backs-pass-tokens,
#screen-backs.is-quickfire .backs-correct-circle {
  align-self: center;
  justify-self: center;
}
/* The logo, much bigger on this screen (v4.76.0) — Adam asked for it both
   while the iPad waits and "once the game begins and the words appear".
   The card below is sized from what is left, so it gives a little. */
#screen-backs.is-quickfire .backs-play-logo {
  height: clamp(64px, 15dvh, 160px);
  max-width: 70%;
}
/* No Leave button in the corner any more, so the strip reserved for it
   shrinks to what the connection pill needs. */
#screen-backs.is-quickfire { padding-top: 12px; }
/* The clock on a card (v4.76.0): "put that timer on a white background so
   we can clearly see it, or a cream background, or a carded background."
   Hidden altogether when it has nothing to show — no clock and no line. */
.backs-timer-card {
  display: grid;
  gap: 6px;
  background: var(--card);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-md);
  padding: 12px 16px;
  flex: none;
}
.backs-timer-card:has(> .backs-timer.hidden):has(> .draw-status:empty) { display: none; }
.backs-timer-card .draw-status { margin: 0; }
.backs-timer-card .draw-status:empty { display: none; }
#screen-backs.is-quickfire .backs-timer { height: 34px; }
#screen-backs.is-quickfire .backs-timer-text { font-size: 1.05rem; }
/* The light arrow back to a passed word (v4.77.0), on the card's left edge. */
.backs-back-arrow {
  position: absolute;
  left: clamp(6px, 1.2vw, 14px);
  top: 50%;
  transform: translateY(-50%);
  width: clamp(44px, 7dvh, 64px);
  height: clamp(44px, 7dvh, 64px);
  border-radius: 50%;
  border: none;
  background: rgba(0, 0, 0, 0.06);
  color: var(--ink-faint);
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(1.6rem, 4.5dvh, 2.6rem);
  line-height: 1;
  display: grid;
  place-items: center;
  padding: 0 0 0.12em;
  cursor: pointer;
}
.backs-back-arrow:hover { background: rgba(0, 0, 0, 0.1); color: var(--ink-soft); }
.backs-back-arrow:disabled { opacity: 0.4; cursor: default; }
/* Time up (v4.77.0): the circles stay where they are, greyed. */
#screen-backs.is-quickfire .backs-correct-circle:disabled,
#screen-backs.is-quickfire .backs-pass-tokens.is-over .backs-pass-token { opacity: 0.45; cursor: default; filter: none; }

.backs-pass-tokens {
  display: flex;
  flex-direction: column;
  gap: clamp(8px, 1.6dvh, 16px);
  justify-content: center;
}
.backs-pass-token {
  aspect-ratio: 1;
  /* As big as Correct (v4.77.0): "make the two 'pass' buttons as big as the
     correct button" — all three share --backs-side, sized so two passes
     stack in the card's height. */
  width: var(--backs-side, clamp(56px, 11dvh, 108px));
  border-radius: 50%;
  border: none;
  background: var(--stop);
  color: #fff;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(1rem, 3.2dvh, 2rem);
  cursor: pointer;
  box-shadow: var(--shadow-md);
}
.backs-pass-token:active { filter: brightness(0.94); }

.backs-correct-circle {
  aspect-ratio: 1;
  width: var(--backs-side, clamp(96px, 22dvh, 220px));
  border-radius: 50%;
  border: none;
  background: var(--sq-green);
  color: #fff;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(1rem, 3.2dvh, 2rem);
  cursor: pointer;
  box-shadow: var(--shadow-lg);
}
.backs-correct-circle:active { filter: brightness(0.96); }

/* The card number and the way back, kept small and out of the way — the
   three-across row of buttons that used to be here is gone. */
.backs-play-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 0 4px;
}

/* The projected scoreboard before a What's That Word lesson's first turn. */
.lp-backs-waiting {
  margin: 0;
  text-align: center;
  color: var(--ink-faint);
  font-size: clamp(1rem, 3dvh, 1.8rem);
}

/* =========================================================================
   PAIRS

   Sixteen cards on one iPad, two children either side of it. The board is a
   4x4 grid that keeps its shape at every size — a card that changed shape as
   the room filled would be a different game on every device — and a card is
   only ever a picture or a word, never both.
   ========================================================================= */

/* ---- the child's screen ------------------------------------------------ */

/* Adam's own layout: "on the left-hand side, we will have the Pairs logo nice
   and big, with the scoreboard below it... shift the actual cards to the right
   alongside the scoreboard." */
main#screen-pairs {
  position: relative;
  display: grid;
  grid-template-rows: minmax(0, 1fr);
  min-height: 0;
}

/* The holding screen, before the teacher has started. */
.pairs-wait {
  display: grid;
  place-content: center;
  padding: 24px;
}
/* Adam: "that text and the logo should be on a carded background." */
.pairs-wait-card {
  display: grid;
  justify-items: center;
  gap: clamp(10px, 2dvh, 22px);
  text-align: center;
  padding: clamp(22px, 4dvh, 46px) clamp(26px, 6vw, 72px);
  max-width: min(90vw, 40rem);
}
.pairs-wait-logo { height: clamp(70px, 14dvh, 150px); width: auto; object-fit: contain; }
.pairs-wait h2 {
  margin: 0;
  font-size: clamp(1.3rem, 4.4vw, 2.4rem);
  color: var(--ink);
}
.pairs-wait p {
  margin: 0;
  font-size: clamp(1rem, 2.6vw, 1.5rem);
  color: var(--ink-soft);
}
.pairs-wait-dots { display: flex; gap: 10px; }
.pairs-wait-dots i {
  width: 12px; height: 12px; border-radius: 999px;
  background: var(--brand);
  animation: pairs-wait-dot 1.3s ease-in-out infinite;
}
.pairs-wait-dots i:nth-child(2) { animation-delay: 0.18s; }
.pairs-wait-dots i:nth-child(3) { animation-delay: 0.36s; }
@keyframes pairs-wait-dot {
  0%, 70%, 100% { opacity: 0.25; transform: translateY(0); }
  35% { opacity: 1; transform: translateY(-6px); }
}

.pairs-stage {
  display: grid;
  /* Wide enough for a box that holds all eight pairs without any of them
     hanging out of it — see .pairs-won. */
  /* Wider since v4.105.0 (Adam: "make the two cards used for the scoreboard
     bigger. This will give you more space to make the cards that go into
     them slightly bigger"). */
  grid-template-columns: minmax(220px, 30%) minmax(0, 1fr);
  gap: clamp(10px, 2vw, 24px);
  min-height: 0;
}
.pairs-side {
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  gap: clamp(8px, 2dvh, 18px);
  min-height: 0;
  align-content: start;
}
/* "On the left-hand side, we will have the Pairs logo nice and big." */
.pairs-logo {
  width: 100%;
  height: clamp(56px, 16dvh, 190px);
  object-fit: contain;
}
.pairs-scores { display: grid; gap: clamp(8px, 1.6dvh, 14px); align-content: start; }

/* One child: name and score, with the pairs they have won stacked beneath. */
.pairs-player {
  display: flex;
  flex-direction: column;
  padding: clamp(6px, 1.2dvh, 12px) clamp(8px, 1vw, 14px);
  border-radius: var(--r-lg);
  background: var(--card-sunk);
  border: 3px solid transparent;
  min-width: 0;
  /* Big enough for all eight pairs from the very first paint, so it never
     changes size as they arrive. */
  min-height: clamp(150px, 30dvh, 300px);
  transition: border-color 140ms ease, background 140ms ease;
}
/* "You can also have a green border highlighting whoever's turn it is on the
   scoreboard. This makes it clear whose turn it is." */
.pairs-player.is-turn {
  border-color: var(--go);
  background: var(--accent-tint);
}
.pairs-player-row {
  display: flex;
  align-items: baseline;
  gap: 8px;
  min-width: 0;
}
.pairs-player-row b {
  font-size: clamp(0.85rem, 1.6vw, 1.35rem);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}
.pairs-player-score {
  margin-left: auto;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(1.1rem, 2.6vw, 2rem);
  color: var(--brand-dark);
}
/* Where a child's won pairs live. Adam: "the score boxes at the side should
   not get bigger and smaller as the cards enter them. Please start with the
   boxes nice and big, so there's space for the cards to enter." So this has
   the same height whether it holds nothing or all eight pairs, and the pairs
   land inside it rather than piling on top of it — "where they remain visible
   side by side". */
.pairs-won {
  flex: 1;
  min-height: 0;
  margin-top: 8px;
  padding: 6px;
  border-radius: var(--r-md);
  background: rgba(255, 255, 255, 0.5);
  box-shadow: inset 0 0 0 2px rgba(0, 0, 0, 0.05);
  display: flex;
  flex-wrap: wrap;
  align-content: flex-start;
  gap: 6px;
  overflow: hidden;
}
/* One won pair: its two cards, side by side. */
.pairs-won-pair {
  display: flex;
  gap: 2px;
  padding: 3px;
  border-radius: 6px;
  background: var(--accent-tint);
  box-shadow: inset 0 0 0 2px var(--go);
}
/* Made the moment the server reports the pair, but held back until the real
   cards have finished flying into it — otherwise the box fills up in front of
   the animation that is heading for it. */
.pairs-won-pair.is-landing { visibility: hidden; }
.pairs-won-pair.just-landed { animation: pairs-landed 520ms cubic-bezier(0.18, 1.12, 0.4, 1); }
/* A small pop as it lands, kept small on purpose: the box clips what
   overflows it, and a bigger bounce than this visibly loses its corners. */
/* The little pair settling into the box. It arrives at the size the real
   cards were when they got there and eases down to its own — so the handover
   from the flying cards to the pair in the box has nothing to see, which is
   what "lands in place instead of snapping" means. The old version started at
   1.18 and bounced past its resting size, which read as a pop. */
@keyframes pairs-landed {
  0% { transform: scale(1.1); opacity: 0.85; }
  100% { transform: scale(1); opacity: 1; }
}
.pairs-won-card {
  display: grid;
  place-items: center;
  width: clamp(24px, 3.4vw, 50px);
  aspect-ratio: 5 / 7;
  border-radius: 5px;
  background: var(--card-raised);
  padding: 3px;
  overflow: hidden;
}
.pairs-won-card img { width: 100%; height: 100%; object-fit: contain; }
.pairs-won-word {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: min(0.68rem, calc(4.8rem / var(--chars, 6)));
  line-height: 1.05;
  text-align: center;
  overflow-wrap: break-word;
  color: var(--ink);
}

.pairs-play {
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  gap: clamp(8px, 1.6dvh, 16px);
  min-height: 0;
}
/* "At the top of this part of the screen, there should be a big green bar
   with 'It's Adam's turn'." */
.pairs-turnbar {
  display: grid;
  place-items: center;
  padding: clamp(8px, 1.6dvh, 16px) 16px;
  border-radius: var(--r-lg);
  background: var(--go);
  color: #fff;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(1rem, 2.6vw, 1.9rem);
  text-align: center;
  line-height: 1.15;
  transition: background 160ms ease;
}
.pairs-turnbar.is-stop { background: var(--stop); }

/* ---- the cards --------------------------------------------------------- */

.pairs-board {
  display: grid;
  /* Both the column count and the card width are worked out in play.js — see
     layoutPairsBoard for why a fixed-shape card cannot size itself from a
     grid cell. */
  grid-template-columns: repeat(var(--pairs-columns, 4), var(--pairs-card-w, 1fr));
  gap: var(--pairs-gap, 12px);
  min-height: 0;
  align-content: center;
  /* Against the score boxes, not centred in the leftover (v4.109.0). On a
     browser window the height-bound board is narrower than its column and
     sat in the middle of it — Adam: "there is a gap between the score
     boxes on the left and the cards on the right. I want it to look the
     same on a browser as it does on an iPad, no space between the cards
     and the score boxes." */
  justify-content: start;
}
/* A slot keeps its place in the grid whatever the card in it is doing, so a
   pair flying off to the stack cannot make the rest of the board reflow
   underneath the other child's finger. */
.pairs-slot {
  position: relative;
  min-width: 0;
  min-height: 0;
  display: grid;
  place-items: center;
}
.pairs-card {
  /* Adam: "change the shape of the cards into rectangles like playing cards."
     The ratio of a real playing card, near enough.

     Width-driven with the height capped, not the other way round: `height:
     100%` plus `aspect-ratio` computes a width the ratio wants and then
     `max-width` never gets a look in, so the cards grew straight through the
     sides of their own slots and into each other. */
  width: 100%;
  height: auto;
  aspect-ratio: 5 / 7;
  /* The perspective the flip is seen in, and it has to be HERE, on the card
     whose child does the rotating. It used to be on .pairs-board, two levels
     up — and `perspective` only reaches a descendant through ancestors that
     preserve 3D, which .pairs-slot does not. So the rotation was drawn flat:
     the card squashed to nothing and came back, which is why Adam saw
     "simply the picture changing" rather than a card turning over. */
  perspective: 900px;
  padding: 0;
  border: none;
  background: none;
  cursor: pointer;
  transform-style: preserve-3d;
  transition: transform 180ms ease;
}
.pairs-card:disabled { cursor: default; }
.pairs-card:not(:disabled):hover { transform: translateY(-4px); }

/* The flip. Both faces are drawn back to back and the whole thing is turned
   over — which is why the card keeps its face once it has had one, so there
   is something to turn back to on a miss. */
/* The card turning over.
   Deliberately NOT the textbook two-faces-back-to-back trick. That needs
   `transform-style: preserve-3d` and `backface-visibility: hidden`, both of
   which measured as working perfectly here and still did nothing on Adam's
   iPad — twice. They are the two properties most likely to be quietly
   declined on iOS Safari, especially inside a <button>. So only one side is
   in the document at a time and this is one plain rotation of one element,
   with play.js swapping the side at the halfway point (see turnPairsCard).
   There is nothing left in it a browser can refuse. */
.pairs-card-inner {
  position: relative;
  display: block;
  width: 100%;
  height: 100%;
}
.pairs-card.is-turning .pairs-card-inner {
  animation: pairs-turn 460ms cubic-bezier(0.42, 0, 0.58, 1);
}
@keyframes pairs-turn {
  0% { transform: rotateY(0deg); }
  /* Edge-on: the instant the side is swapped underneath. */
  50% { transform: rotateY(90deg); }
  100% { transform: rotateY(0deg); }
}

/* One side at a time — which is what makes the swap possible at all. */
.pairs-card-back,
.pairs-card-face {
  position: absolute;
  inset: 0;
  border-radius: clamp(6px, 1.1vw, 12px);
  overflow: hidden;
  box-shadow: var(--shadow-md);
}
.pairs-card.is-up .pairs-card-back { display: none; }
.pairs-card:not(.is-up) .pairs-card-face { display: none; }

/* The back of a playing card, drawn rather than photographed: a deep red
   field, a white margin, and a lattice of diamonds. Two crossed repeating
   gradients make the lattice; the inset ring is the white border every real
   card back has. Drawn in CSS on purpose — it is crisp at every size a board
   of four to sixteen cards can put it at, costs nothing to load, and cannot
   arrive late on a classroom wifi. */
.pairs-card-back {
  background-color: var(--pairs-back);
  background-image:
    repeating-linear-gradient(45deg, rgba(255, 255, 255, 0.16) 0 2px, transparent 2px 9px),
    repeating-linear-gradient(-45deg, rgba(255, 255, 255, 0.16) 0 2px, transparent 2px 9px);
  border: clamp(3px, 0.7vw, 7px) solid #fff;
  box-shadow: var(--shadow-md), inset 0 0 0 2px rgba(255, 255, 255, 0.5);
}
/* A small diamond in the middle, so the back has a centre to it. */
.pairs-card-back::after {
  content: '';
  position: absolute;
  inset: 22%;
  border: 2px solid rgba(255, 255, 255, 0.5);
  border-radius: 3px;
  transform: rotate(45deg);
}

.pairs-card-face {
  background: var(--card-raised);
  border: clamp(3px, 0.7vw, 7px) solid #fff;
  display: grid;
  place-items: center;
  padding: 7%;
}
.pairs-card-face img {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
}
/* The word carries its own length as `--chars`, and the type shrinks for a
   long one rather than running off the card. Adam: "check that all the words
   always fit on the cards. I played just now, and the word 'elephant' did not
   fit on a card and went off the edge." Same trick Story Blocks' own faces
   use, and for the same reason: a fixed clamp cannot know how long the word
   is. `min()` takes whichever is smaller — the ordinary size, or the size at
   which this particular word spans the card. */
.pairs-card-word {
  font-family: var(--font-display);
  font-weight: 800;
  /* It SHRINKS, it does not break. Adam: "the words that are now too long
     have not been shrunk, but they do break down to the next line, like this
     for 'chicken': Chick- / en. I don't want this to happen. Simply change it
     so that the word shrinks in font size and fits onto the card." So the
     line may not wrap at all, and the size is chosen so it does not have to:
     1.05 × the card's width divided by the longest run of letters, which is
     comfortably under what this typeface needs even for "toothbrush", the
     longest word in any of the thirty-eight sets. */
  font-size: min(clamp(0.7rem, 2vw, 1.5rem), calc(var(--pairs-card-w, 120px) * 1.05 / var(--chars, 6)));
  line-height: 1.1;
  text-align: center;
  /* A two-word name may break between its words (v4.73.0: "tap them onto
     two lines"); a single word never breaks inside itself — fitPairsWords in
     play.js measures and shrinks it instead. */
  white-space: normal;
  overflow-wrap: normal;
  word-break: keep-all;
  max-width: 100%;
  color: var(--ink);
}

/* Right, and wrong. */
.pairs-card.is-win .pairs-card-face {
  border-color: var(--go);
  background: var(--accent-tint);
  box-shadow: 0 0 0 4px rgba(46, 158, 82, 0.35);
}
.pairs-card.is-miss .pairs-card-face {
  border-color: var(--stop);
  background: var(--danger-tint);
  animation: pairs-shake 380ms ease;
}
@keyframes pairs-shake {
  0%, 100% { transform: translateX(0); }
  25% { transform: translateX(-4%); }
  60% { transform: translateX(4%); }
}

/* A won pair in two moves — see gatherAndFly in play.js, which works out
   every distance at the moment it happens because they all depend on where
   these two cards happen to be. Move one: the word card slides underneath the
   picture card. */
.pairs-card.is-stacking {
  transition: transform 494ms cubic-bezier(0.4, 0.1, 0.25, 1);
  transform: translate(var(--stack-x, 0), var(--stack-y, 0));
  z-index: 2;
}
/* Move two: the stack crosses the board into the winner's box. The picture
   card rides on top of the word card the whole way. */
/* One transition over one transform, so the travel, the shrink and the spin
   all ease together and none of them can look bolted on.
   
   The curve ARRIVES rather than stops: it eases out into the box the way
   something being set down does, instead of running at speed and cutting to
   the little pair already sitting there. Adam: "when it gets to the box, it
   will be great if it lands in place instead of snapping." The two cards also
   stay solid nearly the whole way now — fading them out early is what made
   the arrival read as a cut. */
.pairs-card.is-flying {
  transition: transform 806ms cubic-bezier(0.22, 0.62, 0.2, 1), opacity 200ms ease-in 700ms;
  transform: translate(
      calc(var(--stack-x, 0px) + var(--fly-x, 0px)),
      calc(var(--stack-y, 0px) + var(--fly-y, 0px))
    )
    rotate(var(--fly-spin, 0deg))
    scale(var(--fly-scale, 0.3));
  opacity: 0;
  pointer-events: none;
  z-index: 3;
}
.pairs-card.is-win .pairs-card-inner { z-index: 4; }
.pairs-card.is-gone { visibility: hidden; }

/* Dealing in: each card drops onto the table a beat after the one before. */
.pairs-slot.is-dealing .pairs-card { animation: pairs-deal 380ms ease-out var(--deal, 0ms) both; }
@keyframes pairs-deal {
  from { opacity: 0; transform: translateY(-14%) scale(0.9); }
  to { opacity: 1; transform: none; }
}

/* A class that has asked for less motion still gets the card turning over and
   the pair going to the box — those are what the game is telling them, not
   decoration on top of it. What goes is the dealing flourish, the shake and
   the bounce, and everything that stays is quicker.

   This matters more than it looks: "Reduce Motion" is on by default on a
   good many school iPads, and the first version of this simply switched the
   flip off, which is one very plausible reason Adam kept seeing a card that
   "simply changes the picture". */
@media (prefers-reduced-motion: reduce) {
  .pairs-card.is-turning .pairs-card-inner { animation-duration: 260ms; }
  .pairs-card.is-stacking { transition-duration: 160ms; }
  .pairs-card.is-flying { transition-duration: 240ms; }
  .pairs-slot.is-dealing .pairs-card,
  .pairs-card.is-miss .pairs-card-face,
  .pairs-won-pair.just-landed { animation: none; }
}

/* The end of a Pairs game (v4.86.0): confetti on a canvas behind the sheet,
   the winner named at the top, a two-column scoreboard with the winner's
   column lit. */
.pairs-over { position: fixed; inset: 0; }
.pairs-confetti { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
.pairs-over-sheet { position: relative; max-width: 560px; width: min(92vw, 560px); border: 4px solid #f7c948; box-shadow: 0 0 0 6px rgba(247, 201, 72, 0.25), var(--shadow-lg, 0 20px 40px rgba(0,0,0,0.25)); }
.pairs-over-trophy { font-size: clamp(2.4rem, 8vmin, 4rem); line-height: 1; margin-bottom: 4px; animation: pairs-trophy 900ms cubic-bezier(0.2, 1.4, 0.4, 1) both; }
@keyframes pairs-trophy { from { transform: scale(0.3) rotate(-20deg); opacity: 0; } to { transform: none; opacity: 1; } }
.pairs-over-sheet h2 { font-size: clamp(1.5rem, 5vmin, 2.4rem); margin: 0 0 14px; color: var(--brand-dark, #2f5d50); }
.pairs-scoreboard { display: flex; align-items: stretch; justify-content: center; gap: 14px; margin: 0 0 22px; }
.pairs-score-col {
  position: relative; flex: 1 1 0; max-width: 220px; display: grid; justify-items: center; gap: 2px;
  padding: 16px 12px 12px; border-radius: var(--r-lg); background: var(--card-sunk, #f1ebe1); border: 3px solid var(--line);
}
.pairs-score-col.is-winner { background: #fff6d6; border-color: #f7c948; box-shadow: 0 0 0 4px rgba(247, 201, 72, 0.35); }
.pairs-score-crown { font-size: 1.6rem; line-height: 1; margin: -6px 0 2px; animation: pairs-crown 700ms 300ms cubic-bezier(0.2, 1.4, 0.4, 1) both; }
.pairs-score-col:not(.is-winner)::before { content: ''; height: 1.6rem; margin: -6px 0 2px; }
@keyframes pairs-crown { from { transform: translateY(-16px) scale(0.4); opacity: 0; } to { transform: none; opacity: 1; } }
.pairs-score-name { font-weight: 800; font-size: clamp(1rem, 3vmin, 1.3rem); }
.pairs-score-num { font-family: var(--font-display); font-weight: 900; font-size: clamp(2.4rem, 9vmin, 4rem); line-height: 1; color: var(--brand-dark, #2f5d50); }
.pairs-score-col.is-winner .pairs-score-num { color: #b8860b; }
.pairs-score-unit { font-size: 0.85rem; color: var(--ink-soft); }
.pairs-score-vs { align-self: center; font-weight: 800; color: var(--ink-faint); font-size: 1.1rem; }
@media (prefers-reduced-motion: reduce) { .pairs-over-trophy, .pairs-score-crown { animation-duration: 1ms; } }

/* ---- the teacher's own set picker ------------------------------------- */

.pairs-set-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 230px), 1fr));
  gap: 12px;
  margin-top: 12px;
}
.pairs-set-card {
  display: grid;
  gap: 8px;
  padding: 12px;
  border: 3px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--card-raised);
  font: inherit;
  color: var(--ink);
  text-align: left;
  cursor: pointer;
}
.pairs-set-card.on { border-color: var(--sq-green); background: var(--accent-tint); }
.pairs-set-card b { font-size: 1rem; }
.pairs-set-pics {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 4px;
}
.pairs-set-pics img {
  width: 100%;
  aspect-ratio: 1;
  object-fit: contain;
  background: #fff;
  border-radius: 6px;
}
.pairs-dash-scores { display: flex; gap: 10px; margin-top: 8px; }
.pairs-dash-score {
  flex: 1;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  padding: 6px 10px;
  border-radius: var(--r-md);
  background: var(--card-sunk);
  border: 2px solid transparent;
}
.pairs-dash-score.is-turn { border-color: var(--sq-green); background: var(--accent-tint); }
.pairs-dash-score b { font-size: 1.2rem; }

/* ---- the projected screen --------------------------------------------- */

/* The room's scoreboard: a card per iPad, four to a row. */
.lp-pairs {
  flex: 1;
  min-height: 0;
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  align-content: start;
  gap: clamp(8px, 1.4vw, 18px);
  overflow-y: auto;
}
@media (max-width: 900px) {
  .lp-pairs { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
.lp-pairs .lp-backs-waiting { grid-column: 1 / -1; }

.lp-pairs-card {
  display: grid;
  gap: clamp(6px, 1vh, 12px);
  padding: clamp(10px, 1.6vh, 18px) clamp(10px, 1vw, 16px);
  border-radius: var(--r-lg);
  background: var(--card-sunk);
  border: 3px solid transparent;
  min-width: 0;
}
.lp-pairs-card.is-done { border-color: var(--go); background: var(--accent-tint); }

.lp-pairs-progress {
  display: flex;
  align-items: baseline;
  gap: 8px;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(1rem, 2vw, 1.7rem);
  color: var(--brand-dark);
}
.lp-pairs-done {
  font-size: 0.62em;
  font-weight: 700;
  color: var(--go);
  margin-left: auto;
}

/* One child above the other, not side by side. Adam: "they should appear one
   on top of the other with their current score next to them." */
.lp-pairs-players {
  display: grid;
  gap: clamp(4px, 0.8vh, 10px);
}
.lp-pairs-player {
  display: flex;
  align-items: baseline;
  gap: 8px;
  padding: clamp(5px, 0.9vh, 10px) clamp(8px, 0.9vw, 14px);
  border-radius: var(--r-md);
  background: var(--card-raised);
  border: 3px solid transparent;
  min-width: 0;
}
.lp-pairs-player.is-turn { border-color: var(--go); }
.lp-pairs-player span {
  font-size: clamp(0.8rem, 1.5vw, 1.3rem);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}
.lp-pairs-player b {
  margin-left: auto;
  font-family: var(--font-display);
  font-size: clamp(1rem, 2vw, 1.7rem);
  color: var(--brand-dark);
}

/* =========================================================================
   STORY DICE

   Nine pictures on a classroom screen and a story made out of them. Two
   surfaces: the teacher's console, which is a blend of word classes and a
   flat readout, and the pop-out board, which is the blocks.

   The board's job is one thing above all — Adam, first: "the key for this
   will be animating it so it actually looks like dice are rolling", and then,
   having seen it: "I want you to animate the dice so they appear to roll in
   from the side of the screen... only the front of the dice that is being
   played with is visible. The children do not need to be able to see the dice
   in 3D." So a block is a flat face that rolls in from off the side, spinning
   as it travels — not the six-faced cube it was in v4.42.0. Two nested
   elements, one transform each, because a single element cannot travel on one
   timing curve and rotate on another: .sb-block-stage travels and
   .sb-block-face spins. See public/js/story-blocks.js for the other half.
   ========================================================================= */

/* ---- a picker tile with no artwork behind it yet ----------------------- */

/* Story Blocks used this until Adam drew it a logo, and nothing uses it now.
   It stays because the NEXT new game will be in exactly the same position —
   every logo in here is his own artwork and CLAUDE.md is explicit that it is
   not a house style to copy without asking him — and because the browser run
   still checks that a tile may be marked by its name rather than a picture.
   Sized to exactly the same box .game-logo occupies, so a row of tiles stays
   level either way. */
.game-wordmark {
  width: 100%;
  height: clamp(80px, 12vw, 130px);
  display: grid;
  place-content: center;
  justify-items: center;
  gap: 8px;
}
.game-wordmark b {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(1.3rem, 2.6vw, 1.9rem);
  letter-spacing: 0.01em;
  color: var(--brand-dark);
}
/* A nine-pip block face, drawn rather than drawn-up: it is three rows of
   three, which is the game's own default blend. Paired with .game-wordmark
   above and, like it, kept for the next game to arrive without artwork. */
.game-wordmark-pips {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 3px;
  padding: 6px;
  border-radius: 8px;
  background: var(--brand-dark);
}
.game-wordmark-pips i {
  width: 7px;
  height: 7px;
  border-radius: 999px;
  background: var(--card-raised);
}

/* ---- the console ------------------------------------------------------- */

/* One row per word class: what it is, then a stepper. */
.sb-blend {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 230px), 1fr));
  gap: 14px;
  margin-top: 14px;
}
.sb-blend-row {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: 12px;
  padding: 14px 16px;
  border: 3px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--card-raised);
}
.sb-blend-row.is-empty { opacity: 0.62; }
.sb-blend-name { display: grid; gap: 2px; min-width: 0; }
.sb-blend-name b { font-size: 1.05rem; }
.sb-blend-name span { font-size: 0.78rem; color: var(--ink-faint); }
.sb-stepper {
  display: flex;
  align-items: center;
  gap: 6px;
}
.sb-stepper button {
  width: 40px;
  height: 40px;
  border-radius: 999px;
  border: 2px solid var(--line-strong);
  background: var(--card);
  color: var(--ink);
  font-size: 1.2rem;
  font-weight: 800;
  cursor: pointer;
  display: grid;
  place-items: center;
  padding: 0;
}
.sb-stepper button:hover:not(:disabled) { background: var(--brand-tint); border-color: var(--brand); }
.sb-stepper button:disabled { opacity: 0.35; cursor: default; }
.sb-stepper output {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 1.6rem;
  min-width: 2ch;
  text-align: center;
  color: var(--brand-dark);
}

/* The two set-up cards side by side: the random blend, and what is printed
   on a block. */
.sb-setup-row {
  display: grid;
  grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);
  gap: 18px;
  align-items: stretch;
}
@media (max-width: 900px) {
  .sb-setup-row { grid-template-columns: minmax(0, 1fr); }
}
.sb-look-card { display: grid; align-content: start; }
.sb-look-options { display: grid; gap: 8px; margin-top: 10px; }

/* The flat readout of what is on the board. Small on purpose — it is a
   check, not a second board. */
.sb-console-blocks {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
  gap: 10px;
  margin-top: 12px;
}
.sb-flat {
  display: grid;
  justify-items: center;
  align-content: start;
  gap: 4px;
  padding: 8px 6px;
  border-radius: var(--r-md);
  background: var(--card-raised);
  border: 3px solid var(--line);
  min-width: 0;
}
.sb-flat .sb-face-pic { width: 100%; height: 62px; object-fit: contain; }
.sb-flat .sb-face-word {
  font-size: min(0.82rem, calc(7.2rem / var(--chars, 6)));
  font-weight: 700;
  text-align: center;
  overflow-wrap: break-word;
  line-height: 1.15;
}

/* Each class has its own colour, on both surfaces, so "these three are the
   doing words" is readable across a room without anybody reading a label. */
.sb-adjective { --sb-ink: #7a4cc0; --sb-tint: #efe7fb; }
.sb-noun      { --sb-ink: #2f6fd0; --sb-tint: #e3edfc; }
.sb-verb      { --sb-ink: #14806a; --sb-tint: #ddf0ea; }
/* A block out of a set the teacher built has no word class to colour it by:
   it may be a word she typed, which has none at all. */
.sb-plain     { --sb-ink: var(--brand-dark); --sb-tint: var(--brand-tint); }
.sb-flat { border-color: color-mix(in srgb, var(--sb-ink) 34%, var(--line)); }
.sb-flat .sb-face-word { color: var(--sb-ink); }

/* ---- the board ---------------------------------------------------------- */

.lp-storyblocks {
  flex: 1;
  min-height: 0;
  display: grid;
  grid-template-rows: minmax(0, 1fr);
}
.sb-waiting {
  display: grid;
  place-items: center;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(1.4rem, 4vw, 2.6rem);
  color: var(--ink-faint);
  text-align: center;
}
.sb-board {
  min-height: 0;
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
  gap: clamp(10px, 2vh, 22px);
  /* The blocks are tilted, so each one's right-hand face sticks out past its
     own grid cell — which is what makes them look solid, and what would
     otherwise let the last column touch the edge of the card. The padding is
     here rather than on the tray so layoutTray still measures a box it can
     fill completely. */
  padding: 0 clamp(8px, 2.5vw, 40px);
}

/* Story Blocks' board, page furniture and all.

   Everything on this screen except the blocks is overhead: the page's own
   margin, the card's padding, the gap under the logo, the gap above the
   button. On a 900px screen those four came to about a hundred pixels, which
   is a third of one row of blocks — so on this screen, and only on this
   screen, they are pulled in as far as they will go. Adam: "I want you to
   make the pictures nice and big so everyone can see what they are." */
body.sb-page .leaderboard-page { padding: clamp(10px, 1.2vw, 18px) clamp(20px, 3vw, 40px); }
body.sb-page .leaderboard-page .lp-card { padding: 14px clamp(16px, 2.4vw, 26px); gap: 10px; }
body.sb-page .sb-board { gap: clamp(8px, 1.2vh, 14px); }

/* The table the blocks roll onto. Before the first roll it is empty and the
   button below has the whole screen; after it, this is the screen. */
.sb-tray {
  min-height: 0;
  display: grid;
  grid-template-columns: repeat(var(--block-columns, 3), var(--block, 150px));
  align-content: center;
  justify-content: center;
  gap: var(--block-gap, 18px);
  /* The blocks roll in from off the side of the screen, so nothing is allowed
     to clip them on the way. */
  overflow: visible;
}
.sb-tray:empty { display: none; }

/* Grouped by word class. Adam: "put the dice into their word classes, clearly
   marked in separate boxes with the words 'verbs', 'nouns', and 'adjectives'
   above." One box per class, stacked; a set the teacher built has no classes
   to group by and rolls as one plain row instead. */
/* Three boxes side by side, a third each. Adam: "I want you to have the three
   boxes alongside each other… verbs on the left third, nouns in the middle,
   adjectives on the right third." */
.sb-tray.is-grouped {
  grid-template-columns: repeat(var(--sb-boxes, 3), minmax(0, 1fr));
  align-content: stretch;
  align-items: stretch;
  justify-items: stretch;
}
/* Kept deliberately tight. Three of these stacked up, each with a heading
   above its row, is a lot of chrome on a screen whose whole job is to show
   pictures — every pixel spent here comes straight off the size of the
   blocks (see layoutTray, which measures this box rather than guessing). */
.sb-class-box {
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  justify-items: center;
  min-height: 0;
  gap: 3px;
  padding: 3px clamp(8px, 1.2vw, 16px) 7px;
  border-radius: var(--r-lg);
  border: 3px solid color-mix(in srgb, var(--sb-ink) 45%, #fff);
  background: color-mix(in srgb, var(--sb-tint) 60%, transparent);
}
.sb-class-name {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(1.15rem, 2.6vw, 2.1rem);
  line-height: 1.15;
  letter-spacing: 0.06em;
  color: var(--sb-ink);
  text-transform: uppercase;
}
/* Down the box rather than across the screen — which is what makes the
   pictures big. Usually one column; five in a class needs two, or a block
   is squeezed to a fifth of the height and nobody at the back can see it
   (layoutTray works out which and sets --block-columns). */
.sb-class-blocks {
  display: grid;
  grid-template-columns: repeat(var(--block-columns, 1), var(--block, 150px));
  justify-items: center;
  align-content: center;
  justify-content: center;
  gap: var(--block-gap, 18px);
}

/* One block: a single flat face. Adam, having seen the 3D version: "I want
   you to make it so that only the front of the dice that is being played with
   is visible. The children do not need to be able to see the dice in 3D."
   --block is still set in pixels by layoutTray, because the roll-in travels
   in viewport units and the two have to agree about how big things are. */
.sb-block {
  width: var(--block, 150px);
  height: var(--block, 150px);
  /* It is a <button> since v4.48.0 — stick or twist — so every one of the
     browser's own button clothes has to come off before it can look like a
     wooden block again. */
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  cursor: pointer;
  appearance: none;
  -webkit-tap-highlight-color: transparent;
}
/* Stick or twist. Adam: "any pictures I like, I want to be able to click on
   and highlight them in green, and they stick. When I press roll again, any
   pictures that are not highlighted in green will twist and roll again."
   The green is on the face rather than the button so it reads as the block
   itself being marked, and it survives the roll-in animation's transforms. */
.sb-block.is-stuck .sb-block-face {
  border-color: var(--go);
  box-shadow:
    0 0 0 6px color-mix(in srgb, var(--go) 30%, transparent),
    var(--shadow-md),
    inset 0 0 0 2px rgba(255, 255, 255, 0.7);
  background: color-mix(in srgb, var(--go) 12%, var(--card-raised));
}
/* A green tick in the corner, because a colour alone is a poor signal on a
   projector at the back of a classroom. */
.sb-block.is-stuck .sb-block-face::after {
  content: '✓';
  position: absolute;
  top: 4%;
  right: 6%;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: calc(var(--block, 150px) * 0.16);
  line-height: 1;
  color: var(--go);
}
@media (hover: hover) {
  .sb-block:not(.is-stuck):hover .sb-block-face {
    border-color: color-mix(in srgb, var(--go) 60%, #fff);
  }
}
.sb-block:focus-visible { outline: none; }
.sb-block:focus-visible .sb-block-face {
  outline: 4px solid var(--brand);
  outline-offset: 3px;
}
.sb-block-stage {
  position: relative;
  width: 100%;
  height: 100%;
}
.sb-block-face {
  position: absolute;
  inset: 0;
  display: grid;
  justify-items: center;
  gap: 3%;
  padding: 8%;
  border-radius: calc(var(--block, 150px) * 0.14);
  background: var(--card-raised);
  border: 3px solid color-mix(in srgb, var(--sb-ink) 45%, #fff);
  box-shadow: var(--shadow-md), inset 0 0 0 2px rgba(255, 255, 255, 0.7);
  overflow: hidden;
}
/* The picture takes whatever the word leaves, rather than a fixed share of
   the face: "get undressed" runs to two lines and used to push itself off the
   bottom of the block. A block showing only a picture gets the whole face. */
.sb-block-face { grid-template-rows: minmax(0, 1fr) auto; }
.sb-block-face .sb-face-pic {
  width: 100%;
  height: 100%;
  min-height: 0;
  object-fit: contain;
}
.sb-block-face .sb-face-word {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: min(calc(var(--block, 150px) * 0.135), calc(var(--block, 150px) * 1.35 / var(--chars, 6)));
  line-height: 1.08;
  text-align: center;
  color: var(--sb-ink);
  overflow-wrap: break-word;
  align-self: end;
}
/* A block showing nothing but its word centres it and lets it be much
   bigger — there is a whole face for it. */
.sb-block-face:not(:has(.sb-face-pic)) {
  grid-template-rows: minmax(0, 1fr);
  align-items: center;
}
.sb-block-face:not(:has(.sb-face-pic)) .sb-face-word {
  align-self: center;
  font-size: min(calc(var(--block, 150px) * 0.26), calc(var(--block, 150px) * 1.9 / var(--chars, 6)));
}

.sb-block-shadow {
  position: absolute;
  left: 8%;
  right: 8%;
  bottom: -6%;
  height: 10%;
  border-radius: 999px;
  background: rgba(20, 30, 28, 0.22);
  filter: blur(6px);
  z-index: -1;
}

/* ---- rolling in ---------------------------------------------------------- */

/* The block travels in from off the side of the screen while its face spins.
   Two elements, one transform each: a single element cannot translate on one
   timing curve and rotate on another. */
/* The roll itself is driven from public/js/story-blocks.js through the Web
   Animations API, not from here. A die tips over its own corners, each tip
   carrying it forward exactly its own width, so the number of keyframes is
   however many of its own widths that block happens to be from the edge of
   the screen — which is not known until the board has been laid out and
   therefore cannot be written down in a stylesheet.

   What is left here is the landing, and the shadow. The shadow sits inside
   the part that turns, so it would swing round with the block; it stays out
   of the way until the block is standing still. */
.sb-block.is-rolling .sb-block-shadow { opacity: 0; }
.sb-block.is-settled .sb-block-stage { animation: sb-land 300ms cubic-bezier(0.22, 1.4, 0.4, 1) both; }

@keyframes sb-land {
  0% { transform: scale3d(1.05, 0.94, 1); }
  60% { transform: scale3d(0.99, 1.02, 1); }
  100% { transform: scale3d(1, 1, 1); }
}

/* A class that has asked for no motion gets the words with no roll at all —
   the game is the words, and the animation is decoration on top of it. */
/* Reduced motion gets a SHORTER roll, not no roll.
   This is the whole game — Adam: "the key for this will be animating it so it
   actually looks like dice are rolling" — and a board where the pictures
   simply appear is not the game he asked for. `throwBlocks` cuts the distance,
   the spin and the duration when the setting is on; nothing here needs to
   switch anything off, and switching it off is exactly what hid the animation
   from him on his own laptop. */

/* ---- the button --------------------------------------------------------- */

.sb-actions {
  display: grid;
  justify-items: center;
}
/* Before the first throw the tray is empty and this fills the screen; after
   it, the blocks take the room and this becomes a bar underneath. Adam: "a
   big green button saying 'Roll the Dice'" — his own wording, from before the
   game was renamed; the button reads "Roll the Blocks" now, which is a one
   word change back if he wants it. */
.sb-roll {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: clamp(10px, 1.6vw, 20px);
  font-family: var(--font-display);
  font-weight: 800;
  /* One size, whether or not there are blocks on the table yet. It used to
     open at a screen-filling 3.4rem and shrink the moment the first throw
     landed — Adam: "the Roll Again button seems to appear all stretched out
     to begin with" — and Start always throws straight away, so the big
     version was only ever seen for the half-second before it shrank. */
  font-size: clamp(1rem, 2.2vw, 1.7rem);
  color: #fff;
  background: var(--go);
  border: none;
  border-radius: 999px;
  padding: clamp(10px, 1.4vh, 16px) clamp(22px, 3vw, 44px);
  cursor: pointer;
  box-shadow: 0 6px 0 #227640, var(--shadow-md);
  transition: transform 90ms ease, box-shadow 90ms ease;
}
.sb-roll:hover { background: #34ac5b; }
.sb-roll:active {
  transform: translateY(5px);
  box-shadow: 0 3px 0 #227640, var(--shadow-md);
}
.sb-roll:disabled { opacity: 0.55; cursor: default; transform: none; }

/* Three little blocks on the button itself, tumbling gently, so the control
   says what it does before anybody reads it. */
.sb-roll-blocks {
  display: flex;
  gap: 0.28em;
}
.sb-roll-blocks i {
  width: 0.62em;
  height: 0.62em;
  border-radius: 0.14em;
  background: #fff;
  box-shadow: inset 0 0 0 0.07em rgba(34, 118, 64, 0.5);
  animation: sb-hop 1.9s ease-in-out infinite;
}
.sb-roll-blocks i:nth-child(2) { animation-delay: 0.18s; }
.sb-roll-blocks i:nth-child(3) { animation-delay: 0.36s; }
.sb-roll:disabled .sb-roll-blocks i { animation-play-state: paused; }
@keyframes sb-hop {
  0%, 62%, 100% { transform: translateY(0) rotate(0deg); }
  72% { transform: translateY(-0.3em) rotate(-18deg); }
  84% { transform: translateY(0) rotate(12deg); }
  92% { transform: translateY(-0.08em) rotate(0deg); }
}
@media (prefers-reduced-motion: reduce) {
  .sb-roll-blocks i { animation: none; }
}

/* ---- Pairs: the teacher's own set, and how a pair is shown ------------- */

/* Adam: "there should be a gap between the top card and the second card with
   the QR code in it. At the moment, they crash into one another." */


/* Adam: "the 'How the cards look' card needs to be the same height as the
   'Choose your cards' option next to it. You can actually make the 'Choose
   your cards' option smaller and bump everything up slightly." Stretch makes
   them level; the preview below only takes the room it needs. */
.pairs-build-row {
  display: grid;
  grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr);
  gap: 18px;
  margin-top: 18px;
  align-items: stretch;
}
/* .card carries a margin-top of its own, which inside a stretched grid row
   pushes one card down and shortens it — 16px, which is exactly what the two
   were out by. And the same border on both, so they read as a matched pair
   rather than one outlined and one not. */
.pairs-build-row > .card {
  margin: 0;
  padding: 14px 18px;
  border: 3px solid var(--line);
}
@media (max-width: 900px) {
  .pairs-build-row { grid-template-columns: minmax(0, 1fr); }
}

/* The whole card is the way in to the builder, so it is a button — the same
   pattern .game-card uses on the picker. */
.pairs-build-card {
  text-align: left;
  border: 3px solid var(--line);
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  gap: 4px;
}
/* One saved set (v4.73.0): the set itself is the button, Edit and ✕ beside. */
.pairs-own-set { display: flex; align-items: center; gap: 8px; width: 100%; }
.pairs-own-set-main {
  flex: 1;
  min-width: 0;
  display: grid;
  grid-template-columns: auto auto 1fr;
  align-items: center;
  gap: 10px;
  padding: 8px 12px;
  border: 2px solid var(--line);
  border-radius: var(--r-md);
  background: var(--card-raised);
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
}
.pairs-own-set.on .pairs-own-set-main { border-color: var(--go); box-shadow: 0 0 0 3px var(--accent-tint); }
.pairs-own-set-main:hover { border-color: var(--brand); }
.pairs-own-set-pics { display: flex; gap: 3px; justify-self: end; }
.pairs-own-set-pics img { width: 28px; height: 28px; object-fit: contain; border-radius: 4px; background: #fff; }
.pairs-own-set-tools { display: inline-flex; gap: 4px; flex: none; }
.pairs-build-plus {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 1.6rem;
  line-height: 1;
  color: var(--brand);
}
/* Squarez's set cards, in a grid (v4.104.0). */
.pairs-build-preview {
  display: grid;
  gap: 14px;
  grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
  margin-top: 8px;
}
.pairs-build-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 10px 4px 4px;
  border-radius: 999px;
  background: var(--card-sunk);
  border: 2px solid var(--line);
  font-size: 0.82rem;
  font-weight: 650;
}
.pairs-build-chip img { width: 24px; height: 24px; object-fit: contain; }
.pairs-build-chip:not(:has(img)) { padding-left: 10px; }

.pairs-look-card { display: grid; grid-template-rows: auto minmax(0, 1fr); }
.pairs-look-options {
  display: grid;
  gap: 6px;
  margin-top: 8px;
  align-content: center;
}
/* Two little cards beside the words, so "one card has the picture, the other
   has the word" is shown as well as said. */
.pairs-look-faces { display: flex; gap: 3px; flex: none; }
.pairs-look-face {
  width: 20px;
  height: 27px;
  border-radius: 3px;
  display: grid;
  place-items: center;
  font-size: 10px;
  line-height: 1;
  background: var(--card-raised);
  border: 2px solid var(--line-strong);
}
.pairs-look-face.is-word { font-weight: 800; color: var(--ink); font-size: 9px; }
.pairs-look-text { display: grid; gap: 1px; min-width: 0; }
.pairs-look {
  display: flex;
  align-items: center;
  gap: 10px;
  text-align: left;
  padding: 8px 12px;
  border-radius: var(--r-md);
  border: 3px solid var(--line);
  background: var(--card-raised);
  font: inherit;
  color: inherit;
  cursor: pointer;
}
.pairs-look:hover { border-color: var(--line-strong); }
.pairs-look.on { border-color: var(--sq-green); background: var(--accent-tint); }
.pairs-look b { font-size: 0.98rem; }
.pairs-look span { font-size: 0.78rem; color: var(--ink-faint); }

/* Uploading a picture of your own into a Pairs card. A label rather than a
   button, because the file input it wraps is the control — the button is
   just what it looks like. */
.pairs-upload-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  margin-top: 8px;
  width: 100%;
  cursor: pointer;
}

/* ---- Where's Grandma?: the whole-class demonstration ------------------- */

/* One card per child on the teacher's screen, listing who they have put
   where. Not a miniature house: thirty of those is a lot of picture for a
   laptop to draw, and what a teacher actually wants to read is whether the
   class put Grandma in the garden. */
.grandma-class-list {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 8px;
}
.grandma-class-row {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 9px 3px 3px;
  border-radius: 999px;
  background: var(--card-sunk);
  border: 2px solid var(--line);
  font-size: 0.75rem;
  font-weight: 650;
  color: var(--ink-soft);
}
.grandma-class-row img { width: 22px; height: 22px; object-fit: contain; }

/* Kept on one line — an arrow that wraps on to its own line reads as a
   mistake rather than as part of the label. */
.nowrap { white-space: nowrap; }

/* Pairs' compact join card, in the header row beside its logo. The whole
   scoreboard is one card now, so the join details ride along in its heading
   rather than sitting in a card of their own above it. */
.lp-head-join {
  display: flex;
  align-items: center;
  gap: clamp(8px, 1vw, 14px);
  padding: 6px 10px 6px 6px;
  border-radius: var(--r-lg);
  background: var(--card-sunk);
}
.lp-head-join img {
  width: clamp(52px, 6vw, 84px);
  height: auto;
  border-radius: 6px;
  background: #fff;
  padding: 3px;
}
.lp-head-join-text { display: grid; gap: 1px; line-height: 1.15; }
.lp-head-join-text span { font-size: clamp(0.7rem, 1.1vw, 0.95rem); color: var(--ink-soft); }
.lp-head-join-text b {
  font-family: var(--font-display);
  font-size: clamp(1.1rem, 2.2vw, 1.9rem);
  letter-spacing: 0.06em;
  color: var(--brand-dark);
}

/* =========================================================================
   30-SECOND TALKS

   A board game. Adam: "laid out like Monopoly, with a square board that
   students go around the edge of repeatedly… 8 spaces on each side, plus the
   big square on either corner." The board is a ten-by-ten grid whose outer
   tracks are wider than the eight between them — so each corner is a big
   square and each side space is Monopoly's oblong — and the eight-by-eight
   in the middle is the die, the card and the thirty-second ring.

   The board's size is set in pixels by play.js (layoutTalksBoard): a token
   is positioned against its square's measured rectangle, and a ratio-sized
   box has already gone wrong twice in this file (Pairs' cards, Grandma's
   house). Scoped to <main> because #screen-talks also exists on the
   teacher's page.
   ========================================================================= */

main#screen-talks {
  position: relative;
  display: grid;
  grid-template-rows: minmax(0, 1fr);
  min-height: 0;
  touch-action: manipulation;
}
.talks-stage {
  min-height: 0;
  min-width: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: clamp(2px, 0.6vw, 8px);
}

/* The stage holds the board and, beside it, the gold (v4.69.0). */
.talks-stage-inner {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 14px;
  min-width: 0;
  min-height: 0;
  /* The chest's topic pop-up and the flying coins are laid over the whole
     of this (v4.76.0). */
  position: relative;
}
.talks-stage-inner.is-stacked { flex-direction: column; }
.talks-board {
  --corner: 1.55fr;
  position: relative;
  display: grid;
  grid-template-columns: var(--corner) repeat(8, 1fr) var(--corner);
  grid-template-rows: var(--corner) repeat(8, 1fr) var(--corner);
  gap: 3px;
  padding: 4px;
  /* Not Monopoly (v4.69.0): a sandy board with a rope-coloured edge, and
     every square a picture of what it does. */
  background: #b9834a;
  border: 4px solid #7a5230;
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-md), inset 0 0 0 2px #e6c28f;
}

.talks-square {
  position: relative;
  display: grid;
  place-items: center;
  background: #fbf0d6;
  border-radius: calc(var(--board, 600px) * 0.012);
  overflow: hidden;
  min-width: 0;
  min-height: 0;
  box-shadow: inset 0 0 0 1px rgba(122, 82, 48, 0.25);
}
.talks-square-icon {
  width: 78%;
  height: 78%;
  object-fit: contain;
  pointer-events: none;
}
.talks-square.is-chest { background: #fde9a6; }
.talks-square.is-monkey { background: #e9d9c3; }
.talks-square.is-corner {
  background: #d8efe4;
  grid-template-rows: minmax(0, 1fr) auto auto;
  gap: 0;
  padding: 3% 4%;
}
.talks-square.is-corner .talks-square-icon { width: 100%; height: 100%; min-height: 0; }
.talks-square.is-start { background: #cfe8f5; }
.talks-square.is-volcano { background: #f7d8c9; }
.talks-square.is-cave { background: #ddd6cf; }
.talks-corner-label {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: calc(var(--board, 600px) * 0.02);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--brand-dark);
  line-height: 1.1;
}
.talks-corner-hint {
  font-size: calc(var(--board, 600px) * 0.015);
  color: var(--ink-soft);
  line-height: 1.1;
}

/* ---- the middle ------------------------------------------------------- */
.talks-centre {
  grid-row: 2 / 10;
  grid-column: 2 / 10;
  min-width: 0;
  min-height: 0;
  display: grid;
  place-items: center;
  padding: calc(var(--board, 600px) * 0.02);
  /* The island (v4.69.0) — the game's card sits on its lagoon. */
  background: #cfe6ec url('/images/talks/island.jpg') center / cover no-repeat;
  border-radius: calc(var(--board, 600px) * 0.02);
  overflow: hidden;
}
/* THE PANEL NEVER OVERFLOWS. Three rows — logo, status, content — and the
   content row is minmax(0, 1fr): the question card scrolls inside it if it
   must, the logo never moves, nothing climbs out over the squares. Adam:
   "all of the text would not show up inside a question card… the Go button
   would push the logo off the top of the board." */
.talks-centre-panel {
  position: relative;
  width: 100%;
  height: 100%;
  min-height: 0;
  display: grid;
  grid-template-rows: auto auto minmax(0, 1fr);
  justify-items: center;
  gap: calc(var(--board, 600px) * 0.01);
  padding: calc(var(--board, 600px) * 0.02) calc(var(--board, 600px) * 0.03);
  background: rgba(255, 252, 244, 0.86);
  border: 2px solid rgba(122, 82, 48, 0.35);
  border-radius: calc(var(--board, 600px) * 0.03);
  text-align: center;
  overflow: hidden;
}
.talks-centre-logo {
  width: calc(var(--board, 600px) * 0.32);
  max-height: calc(var(--board, 600px) * 0.115);
  object-fit: contain;
}
.talks-centre-content {
  min-height: 0;
  min-width: 0;
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: calc(var(--board, 600px) * 0.01);
  overflow: hidden;
}
.talks-centre-content > * { flex: none; }
.talks-centre-content .talks-question-card {
  flex: 0 1 auto;
  min-height: 0;
  overflow-y: auto;
  width: 100%;
  box-sizing: border-box;
}
.talks-status {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: calc(var(--board, 600px) * 0.036);
  line-height: 1.15;
  color: var(--brand-dark);
}
.talks-hint {
  margin: 0;
  font-size: calc(var(--board, 600px) * 0.021);
  line-height: 1.2;
  color: var(--ink-soft);
}
/* The die: a real cube (v4.67.0), six pipped faces, tumbled by
   element.animate() in js/talks-board.js. `perspective` sits on the die whose
   CHILD rotates (the lesson of v4.45.0). */
.talks-die {
  --die: calc(var(--board, 600px) * 0.13);
  position: relative;
  width: var(--die);
  height: var(--die);
  margin: calc(var(--die) * 0.1) 0 calc(var(--die) * 0.25);
}
/* The body is what flies (v4.77.3); the perspective sits on it because its
   CHILD is what rotates (the lesson of v4.45.0). */
.talks-die-body {
  width: 100%;
  height: 100%;
  perspective: calc(var(--die) * 5);
}
/* The shadow on the table under the die — a separate element, not a
   filter: a drop-shadow filter on a 3D-transformed element re-rasterises
   every frame, which is half of what made the old roll jerky. */
.talks-die-shadow {
  position: absolute;
  left: 8%;
  right: 8%;
  bottom: -14%;
  height: 18%;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.26);
  filter: blur(2px);
  transform-origin: 50% 50%;
}
.talks-die-cube {
  position: relative;
  width: 100%;
  height: 100%;
  transform-style: preserve-3d;
}
.talks-die-face {
  position: absolute;
  inset: 0;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-template-rows: repeat(3, 1fr);
  place-items: center;
  padding: 12%;
  background: #fffdf7;
  border: 2px solid #2b2b2b;
  /* Squared off (v4.77.3): a 16% radius left the cube's edges open at
     every corner — "the corners of the dice appear to be detached". */
  border-radius: 4%;
  box-shadow: inset 0 0 0 2px rgba(0, 0, 0, 0.05);
  transform: var(--face) translateZ(calc(var(--die) / 2));
  backface-visibility: hidden;
  will-change: transform;
}
.talks-die-face i { display: block; width: 62%; height: 62%; border-radius: 50%; background: transparent; }
.talks-die-face i.on { background: #1f1f1f; }
/* A pressed Roll / Go / Next stays put, faded, until the server redraws
   the panel (v4.77.3). */
.talks-roll.is-pressed, .talks-go-board.is-pressed, .talks-continue.is-pressed { opacity: 0.55; }
.talks-roll {
  font-size: calc(var(--board, 600px) * 0.045);
  padding: 0.5em 1.4em;
}
.talks-question-card {
  background: var(--card-raised);
  border: 3px solid var(--line);
  border-radius: var(--r-lg);
  padding: calc(var(--board, 600px) * 0.015) calc(var(--board, 600px) * 0.03);
  max-width: 100%;
  box-shadow: var(--shadow-md);
}
.talks-question-text {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: calc(var(--board, 600px) * 0.034);
  line-height: 1.25;
  color: var(--ink);
}

/* ---- the ring — shared by the board and the console ------------------- */
.talks-timer {
  position: relative;
  /* Sized so a three-line question, the ring, Go and the hint all fit the
     panel on an iPad (measured in test/e2e-talks.mjs) — the ring is the
     part that can afford to be smaller. */
  width: calc(var(--board, 600px) * 0.125);
  aspect-ratio: 1;
  display: grid;
  place-items: center;
}
.talks-ring {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  transform: rotate(-90deg);
}
.talks-ring-track {
  fill: none;
  stroke: var(--line);
  stroke-width: 9;
}
.talks-ring-fill {
  fill: var(--card-raised);
  stroke: var(--go);
  stroke-width: 9;
  stroke-linecap: round;
  stroke-dasharray: 276.46;
  stroke-dashoffset: 0;
}
.talks-timer.is-over .talks-ring-fill { stroke: var(--stop); }
.talks-timer-text {
  position: relative;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: calc(var(--board, 600px) * 0.052);
  line-height: 1;
  color: var(--brand-dark);
}
.talks-timer.is-over .talks-timer-text { color: var(--stop); }

/* ---- tokens ----------------------------------------------------------- */
.talks-token-layer {
  position: absolute;
  inset: 0;
  pointer-events: none;
  /* Above the squares, which are positioned too and come later in the DOM
     (they are built by play.js after this layer already exists). */
  z-index: 3;
}
.talks-token {
  position: absolute;
  width: calc(var(--board, 600px) * 0.052);
  height: calc(var(--board, 600px) * 0.052);
  border-radius: 50%;
  background: var(--token, #888);
  color: #fff;
  border: 3px solid #fff;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
  display: grid;
  place-items: center;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: calc(var(--board, 600px) * 0.026);
  transform: translate(-50%, -50%);
  transition: left 0.16s ease-out, top 0.16s ease-out;
}
.talks-token.is-current { box-shadow: 0 0 0 4px rgba(46, 158, 82, 0.55), 0 2px 6px rgba(0, 0, 0, 0.3); }
.talks-token.is-hopping { transform: translate(-50%, -50%) scale(1.18); z-index: 2; }
.talks-token.is-away { opacity: 0.45; }
/* The small copy on the console's seat list. */
.talks-token.is-mini {
  position: static;
  width: 22px;
  height: 22px;
  font-size: 0.7rem;
  border-width: 2px;
  transform: none;
  transition: none;
  box-shadow: none;
}

/* ---- the console ------------------------------------------------------ */
.talks-level-card.is-chosen { border-color: var(--brand); box-shadow: 0 0 0 4px var(--brand-tint); }
.talks-dash { --board: 420px; }
/* Cards on one row are made the same height; the content stays at the top
   rather than being spread down a tall neighbour's height. */
.talks-card { align-content: start; }
.talks-seats { display: grid; gap: 6px; margin: 4px 0; align-content: start; }
.talks-seat {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 8px;
  border-radius: var(--r-sm);
}
.talks-seat.is-current { background: var(--accent-tint); }
.talks-seat .nm { font-weight: 700; }
.talks-console-turn {
  display: grid;
  gap: 12px;
  margin-top: 4px;
}
.talks-console-turn .talks-question-card { padding: 10px 14px; text-align: left; }
.talks-console-turn .talks-question-text { font-size: 1.1rem; }
.talks-console-controls {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 18px;
  flex-wrap: wrap;
}
.talks-console-controls .talks-timer { width: 96px; }
.talks-console-controls .talks-timer-text { font-size: 1.7rem; }
.talks-go { font-size: 1.3rem; padding: 0.4em 1.6em; }

/* =========================================================================
   The Squarez leaderboard, compacted (v4.61.0)

   Adam, from a lesson: "The text on the popout leaderboard needs changing.
   It's too big, and at the moment it's scrolling. Shrink it all down so it
   appears without having to scroll. Reduce the size of the top card with the
   QR code in it… about half the size it is now. Move the Squarez logo into
   this top card. Reduce the size of the text that says 'leaderboard'."

   .is-compact is put on the page by leaderboard.js for a Squarez round; the
   rows are scaled by --lp-scale, which fitLeaderboard() lowers until the
   list fits the room it has.
   ========================================================================= */
.leaderboard-page.is-compact { padding: clamp(12px, 1.6vw, 22px); }
.leaderboard-page.is-compact .lp-join-mini {
  gap: clamp(14px, 2.4vw, 28px);
  padding: clamp(6px, 1vw, 10px) clamp(16px, 2.4vw, 30px);
  margin-bottom: clamp(10px, 1.4vw, 16px);
}
.leaderboard-page.is-compact .lp-join-mini img#lp-join-mini-qr {
  width: clamp(64px, 7vw, 100px);
  height: clamp(64px, 7vw, 100px);
}
/* Written with the banner's own `img` rule beaten on specificity: that rule
   sizes every img in the banner as a QR square, and made the wordmark a
   200px box on its first outing. */
.lp-join-mini img.lp-join-mini-logo {
  display: block;
  width: auto;
  height: clamp(48px, 6.5vw, 92px);
  max-width: 28vw;
  object-fit: contain;
  border-radius: 0;
  image-rendering: auto;
  flex: none;
  margin-right: auto;
}
.leaderboard-page.is-compact .lp-join-mini-text span { font-size: clamp(0.85rem, 1.3vw, 1.1rem); }
.leaderboard-page.is-compact .lp-join-mini-text b { font-size: clamp(1.5rem, 3.2vw, 2.4rem); }
.leaderboard-page.is-compact .lp-card {
  gap: clamp(8px, 1.2vw, 14px);
  overflow: hidden;
}
.leaderboard-page.is-compact .lp-head h1 { font-size: clamp(1rem, 1.5vw, 1.3rem); }
.leaderboard-page.is-compact .lp-head .pill { font-size: 0.8rem; }
.leaderboard-page.is-compact .leaderboard {
  overflow: hidden;
  gap: calc(clamp(6px, 1.1vw, 14px) * var(--lp-scale, 1));
}
.leaderboard-page.is-compact .leaderboard-row { gap: calc(clamp(10px, 1.6vw, 18px) * var(--lp-scale, 1)); }
.leaderboard-page.is-compact .leaderboard-rank { font-size: calc(clamp(1rem, 1.9vw, 1.5rem) * var(--lp-scale, 1)); }
.leaderboard-page.is-compact .leaderboard-name { font-size: calc(clamp(0.95rem, 1.8vw, 1.45rem) * var(--lp-scale, 1)); }
.leaderboard-page.is-compact .leaderboard-bar { height: calc(clamp(14px, 1.9vw, 24px) * var(--lp-scale, 1)); }
.leaderboard-page.is-compact .leaderboard-score { font-size: calc(clamp(0.85rem, 1.4vw, 1.1rem) * var(--lp-scale, 1)); }

/* ---- 30-Second Talks, two modes (v4.63.0) ---------------------------- */
/* Go on the board itself — "started by the players on the board, not by the
   teacher". */
.talks-go-board {
  font-size: calc(var(--board, 600px) * 0.038);
  padding: 0.4em 1.8em;
}
/* A class-mode board can carry thirty tokens: smaller, and packed in a
   little grid on a shared square (see spotFor in js/talks-board.js). */
.talks-board.is-crowded .talks-token {
  width: calc(var(--board, 600px) * 0.032);
  height: calc(var(--board, 600px) * 0.032);
  font-size: calc(var(--board, 600px) * 0.016);
  border-width: 2px;
}
/* Groups mode's roster: one card per iPad, names moved between them. */
.talks-devices { align-items: start; }
.talks-device.is-target { border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-tint); cursor: pointer; }
.talks-device.is-over { border-color: var(--go); background: var(--accent-tint); }
.talks-device .name-chip[draggable='true'] { cursor: grab; }
/* Class mode's names, one per line. */
.talks-class-names {
  width: 100%;
  box-sizing: border-box;
  font: inherit;
  font-size: 1.1rem;
  line-height: 1.5;
  padding: 12px 14px;
  border: 2px solid var(--line);
  border-radius: var(--r-md);
  background: var(--card-raised);
  resize: vertical;
  min-height: 200px;
}
.talks-class-names:focus { outline: none; border-color: var(--brand); box-shadow: 0 0 0 4px var(--brand-tint); }
/* The class-mode board on the display: the whole card, no scrolling. */
.lp-talks-board {
  flex: 1;
  min-height: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 4px;
}
.lp-talks-card.is-talking { border-color: var(--go); }
.lp-talks-total { margin-left: auto; font-size: 0.7em; color: var(--go); }

/* =========================================================================
   IN THE PARK

   One picture and eight coloured balls. A ball's place is a fraction of the
   picture (lib/park.js), so the balls are laid over the picture in
   percentages and the picture's box is set in pixels by play.js to be
   exactly the picture. Scoped to <main>: #screen-park also exists on the
   teacher's page.
   ========================================================================= */
main#screen-park {
  position: relative;
  display: grid;
  grid-template-rows: var(--draw-status-h) minmax(0, 1fr) auto auto;
  gap: var(--game-gap);
  min-height: 0;
  touch-action: none;
}
.park-stage {
  min-height: 0;
  min-width: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--card-sunk);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-sm);
  padding: clamp(4px, 1vw, 10px);
}
.park-scene {
  --park-ball: clamp(26px, 4.6vw, 44px);
  position: relative;
  aspect-ratio: 1600 / 893;
  max-width: 100%;
  max-height: 100%;
  border-radius: var(--r-md);
  overflow: hidden;
  box-shadow: var(--shadow-sm);
  touch-action: none;
}
.park-scene.is-mini { --park-ball: clamp(14px, 2.2vw, 22px); width: 100%; }
.park-scene-img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
  -webkit-user-select: none;
  user-select: none;
  -webkit-user-drag: none;
  -webkit-touch-callout: none;
}
.park-scene.is-dropping .park-scene-img { filter: brightness(1.03); }
.park-scene.is-over { outline: 4px solid var(--go); outline-offset: -4px; }
.park-layer { position: absolute; inset: 0; }

.park-ball {
  position: absolute;
  width: var(--park-ball);
  height: var(--park-ball);
  border-radius: 50%;
  background: radial-gradient(circle at 32% 30%, #fff8 0, transparent 38%), var(--ball, #888);
  border: 2px solid rgba(0, 0, 0, 0.55);
  box-shadow: 0 3px 6px rgba(0, 0, 0, 0.35);
  transform: translate(-50%, -50%);
  touch-action: none;
  cursor: grab;
  -webkit-user-drag: none;
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
}
.park-ball.is-carried { opacity: 0.3; }
.park-ball.is-small { --park-ball: clamp(14px, 2.2vw, 22px); }
.park-ball.is-inline { position: static; transform: none; flex: none; }
.park-ball.is-match { box-shadow: 0 0 0 3px var(--go), 0 3px 6px rgba(0, 0, 0, 0.35); }
.park-ball.is-miss { box-shadow: 0 0 0 3px var(--stop), 0 3px 6px rgba(0, 0, 0, 0.35); }

/* ---- the balls still to place ----------------------------------------- */
/* One long card with the eight balls on it (v4.72.0). Adam: "Remove the
   names underneath each of the balls. Put all of the balls in a single long
   card." */
.park-tray {
  display: flex;
  gap: clamp(10px, 2.4vw, 28px);
  justify-content: center;
  align-items: center;
  flex-wrap: wrap;
  min-height: clamp(52px, 10vh, 84px);
  padding: clamp(6px, 1.2vh, 12px) clamp(12px, 2vw, 24px);
  background: var(--card);
  border: 2px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-sm);
}
.park-tray-ball {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 4px;
  border: none;
  border-radius: 50%;
  background: transparent;
  box-shadow: none;
  font: inherit;
  color: var(--ink);
  touch-action: none;
  cursor: grab;
  -webkit-user-drag: none;
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
}
.park-tray-ball::before {
  content: '';
  width: clamp(30px, 6vh, 52px);
  height: clamp(30px, 6vh, 52px);
  border-radius: 50%;
  background: radial-gradient(circle at 32% 30%, #fff8 0, transparent 38%), var(--ball, #888);
  border: 2px solid rgba(0, 0, 0, 0.55);
  box-shadow: 0 3px 6px rgba(0, 0, 0, 0.3);
}
.park-tray-ball.is-placed { opacity: 0.28; }
.park-tray-ball.is-carried { opacity: 0.3; }
.park-tray-name { font-size: clamp(0.6rem, 1.3vw, 0.78rem); font-weight: 700; text-transform: capitalize; }

/* The ball under the finger while it is being dragged. */
.park-drag {
  position: fixed;
  z-index: 60;
  pointer-events: none;
  width: clamp(34px, 6vw, 56px);
  height: clamp(34px, 6vw, 56px);
  border-radius: 50%;
  background: radial-gradient(circle at 32% 30%, #fff8 0, transparent 38%), var(--ball, #888);
  border: 3px solid #fff;
  box-shadow: var(--shadow-md);
  transform: translate(-50%, -50%);
}

/* ---- the result ------------------------------------------------------- */
/* NEVER SCROLLS (v4.72.0). Adam: "make sure that is not scrollable. At the
   moment, a nice, clean curved edge to the card is being corrupted by the
   sharp edges of a scrolling bar." The sheet is a column that fits the
   screen: the two pictures take whatever height is left after the heading,
   the score, the list and the button, and shrink rather than push. */
.park-sheet {
  max-width: 1100px;
  overflow: hidden;
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto auto auto auto;
  gap: clamp(4px, 1dvh, 10px);
  max-height: 94dvh;
}
.park-sheet h2 { margin: 0; }
.park-sheet .score-big { margin: 0 !important; }
.park-result-scenes {
  min-height: 0;
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  gap: 14px;
  align-items: start;
  justify-items: center;
}
.park-result-scene { min-height: 0; display: grid; grid-template-rows: auto auto; justify-items: center; width: 100%; }
.park-result-scene h3 { margin: 0 0 4px; font-size: 1rem; }
/* The picture is sized by the room it has — worked out in pixels by
   fitParkResult in play.js (the aspect-ratio + max-height trap, again: a
   height-bound box does not narrow through its ratio on every browser). */
.park-result-scene .park-scene.is-mini { width: var(--fit-w, 100%); }
.park-over-balls { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 4px 10px; }
@media (max-width: 720px) { .park-over-balls { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.park-over-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 2px 8px;
  border-radius: var(--r-sm);
  font-size: clamp(0.8rem, 1.8dvh, 1rem);
}
.park-over-row b { flex: 1; text-transform: capitalize; white-space: nowrap; }
.park-over-row .mark { font-weight: 800; color: var(--stop); white-space: nowrap; }
.park-over-row.agreed { background: var(--accent-tint); }
.park-over-row.agreed .mark { color: var(--go); }

/* ---- In The Park, on the teacher's side ------------------------------- */
.park-scene-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: 14px;
  margin-top: 16px;
}
.park-scene-card {
  display: grid;
  gap: 6px;
  padding: 10px;
  border: 3px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--card-raised);
  font: inherit;
  color: var(--ink);
  text-align: left;
  cursor: pointer;
}
.park-scene-card img { width: 100%; aspect-ratio: 1600 / 893; object-fit: cover; border-radius: var(--r-md); }
.park-scene-card b { font-size: 1.05rem; }
.park-scene-card:hover, .park-scene-card.is-chosen { border-color: var(--brand); box-shadow: 0 0 0 4px var(--brand-tint); }
#park-dash { grid-template-columns: repeat(auto-fit, minmax(min(100%, 620px), 1fr)); }
#park-dash .dash-seat-col + .dash-seat-col { padding-left: 0; border-left: 0; }
.dash-drawing-thumb .park-scene { width: 100%; }

/* =========================================================================
   The set builder, rebuilt (v4.66.0) — Pairs first, the same shape to follow
   on the other builders. Adam: "the primary input is a teacher creating his
   or her own cards"; the catalogue is the Picture Library pop-up.
   ========================================================================= */
.set-builder {
  display: grid;
  grid-template-columns: minmax(0, 3fr) minmax(280px, 2fr);
  gap: 18px;
  align-items: start;
}
@media (max-width: 860px) {
  .set-builder { grid-template-columns: 1fr; }
}
.set-builder-make h2 { margin: 0 0 4px; }
.set-builder-block { margin-top: 18px; }
.set-builder-block .field { display: block; margin-bottom: 6px; }
.set-builder-library {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  padding-top: 18px;
  border-top: 2px solid var(--line);
}
.set-builder-chosen .backs-chosen-list { max-height: 60dvh; overflow-y: auto; }
.set-library-sheet {
  width: min(1100px, 96vw);
  max-height: 92dvh;
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  gap: 12px;
}
.set-library-search { flex: 0 1 320px; }
.set-library-cols { min-height: 0; max-height: 78dvh; }
.set-library-cols .backs-picker-side,
.set-library-cols .backs-picker-main { min-height: 0; overflow-y: auto; }
.set-builder-block .row { flex-wrap: nowrap; }
.set-builder-block .row input { flex: 1 1 auto; min-width: 0; }
.set-builder-library .btn { white-space: nowrap; flex: none; }

/* (The site badge of v4.65.0–v4.68.0 and its .site-logo / .hero-logo rules went with the rebrand.) */

/* ---- Who's This? (v4.66.0) ---------------------------------------------
   Guess Who on two iPads. A card is Pairs' card — one element, one
   rotation, the side swapped edge-on (see .pairs-card for why there is no
   preserve-3d in it) — face UP by default, and a tap turns it DOWN onto the
   game's own logo, which is what "crossing out" looks like here. */
.wt-card {
  position: relative;
  display: block;
  width: var(--wt-card, 120px);
  aspect-ratio: 5 / 7;
  perspective: 900px;
  padding: 0;
  border: none;
  background: none;
  cursor: pointer;
  transition: transform 180ms ease;
  -webkit-tap-highlight-color: transparent;
}
.wt-card:disabled { cursor: default; }
.wt-card:not(:disabled):not(.is-teacher):hover { transform: translateY(-3px); }
.wt-card-inner { position: relative; display: block; width: 100%; height: 100%; }
.wt-card.is-turning .wt-card-inner { animation: pairs-turn 460ms cubic-bezier(0.42, 0, 0.58, 1); }
.wt-card-back,
.wt-card-face {
  position: absolute;
  inset: 0;
  border-radius: clamp(6px, 1.1vw, 12px);
  overflow: hidden;
  box-shadow: var(--shadow-md);
  border: clamp(3px, 0.5vw, 5px) solid #fff;
}
.wt-card.is-up .wt-card-back { display: none; }
.wt-card:not(.is-up) .wt-card-face { display: none; }
/* The back: the logo on a deep blue, a colour the wallpaper is not. */
.wt-card-back {
  background: #23407a;
  background-image:
    repeating-linear-gradient(45deg, rgba(255, 255, 255, 0.08) 0 2px, transparent 2px 9px),
    repeating-linear-gradient(-45deg, rgba(255, 255, 255, 0.08) 0 2px, transparent 2px 9px);
  display: grid;
  place-items: center;
  padding: 10%;
}
.wt-card-back img { width: 100%; height: auto; object-fit: contain; image-rendering: pixelated; }
.wt-card-face {
  background: var(--card-raised);
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
  justify-items: center;
  padding: 4% 4% 3%;
  gap: 2px;
}
.wt-card-face img {
  width: 100%;
  height: 100%;
  min-height: 0;
  object-fit: contain;
  border-radius: 6px;
  pointer-events: none;
}
/* The name is printed on Adam's own card art already, so the caption under
   it is for screen readers and the browser run, not for eyes. */
.wt-card-face b {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}
.wt-card-face { grid-template-rows: minmax(0, 1fr); }
/* A face-down card in a child's grid sits back a little, so the ones still
   in play stand out at a glance. */
#whosthis-grid .wt-card:not(.is-up):not(.is-turning) { opacity: 0.72; }
/* Class mode, once this child has it: the board steps back, job done. */
#whosthis-grid.is-done .wt-card { opacity: 0.85; filter: grayscale(0.8); }
/* Guessing: the cards still up invite a tap, the rest do not. */
#whosthis-grid.is-guessing .wt-card.is-up:not(:disabled) { outline: 3px solid var(--go); outline-offset: 2px; border-radius: clamp(6px, 1.1vw, 12px); }
#whosthis-grid.is-guessing .wt-card:not(.is-up) { opacity: 0.35; cursor: default; }

/* The child's screen (v4.71.0): the instruction on a cream card across the
   top, then the stage — "Your card" on a card at the left, the grid at the
   right — and the guess bar right along the bottom. Adam's own layout. */
main#screen-whosthis {
  position: relative;
  display: grid;
  grid-template-rows: minmax(0, 1fr);
  gap: var(--game-gap);
  min-height: 0;
  /* The connection pill's corner is over the grid now; keep it clear. */
  padding-top: 30px;
}
/* In the side column since v4.94.0 (the top toolbar went, the grid grew). */
.whosthis-status-card {
  background: var(--card);
  border: 2px solid var(--line);
  border-radius: var(--r-lg);
  padding: 8px 12px;
  text-align: center;
  display: grid;
  gap: 2px;
  width: 100%;
  margin: 0;
}
.whosthis-status-card .draw-status { margin: 0; height: auto; font-size: clamp(0.95rem, 2dvh, 1.2rem); line-height: 1.25; }
.whosthis-status-sub { margin: 0; font-size: clamp(0.8rem, 1.8dvh, 1rem); color: var(--ink-soft); }
/* While a guess is being chosen (v4.81.0): the card goes red, the words
   white — "Click on the person who you think your friend has." */
.whosthis-status-card.is-guessing { background: var(--stop); border-color: var(--stop); }
.whosthis-status-card.is-guessing .draw-status { color: #fff; }
.whosthis-stage {
  min-height: 0;
  min-width: 0;
  display: grid;
  grid-template-columns: minmax(150px, 22%) minmax(0, 1fr);
  gap: 16px;
  align-items: stretch;
}
.whosthis-mine {
  display: grid;
  grid-template-rows: auto auto;
  align-content: start;
  align-self: start;
  justify-items: center;
  gap: 10px;
  padding: 14px;
  background: var(--card);
}
.whosthis-mine .wt-card { width: 100%; max-width: 220px; cursor: default; }
.whosthis-mine-label { margin: 0; text-align: center; font-size: clamp(1.05rem, 2.4dvh, 1.4rem); }
/* The attempts (v4.74.0, reshaped v4.80.2): round red blobs beside the
   board, under the child's own card, one per guess left; a spent one is
   greyed and stays put so nothing reshapes. Adam: "don't have the guesses
   as long bars. Have them as round red blobs, which should be arranged to
   the side of the game board and not along the bottom." */
.whosthis-side { display: grid; align-content: start; gap: 12px; min-height: 0; min-width: 0; container-type: inline-size; }
.whosthis-guess-bar { display: flex; flex-wrap: nowrap; justify-content: center; gap: 8px; }
/* As big as three fit under the card (v4.81.0, Adam: "as big as they can
   possibly be whilst still fitting under the card on the left") — a third
   of the column less the gaps, in container units. */
.whosthis-attempt {
  --blob: calc((100cqw - 16px) / 3);
  width: var(--blob);
  height: var(--blob);
  border-radius: 50%;
  padding: 0;
  display: grid;
  place-items: center;
  background: var(--stop);
  color: #fff;
  border: none;
  font-size: clamp(0.8rem, 5cqw, 1.3rem);
  line-height: 1.05;
  text-align: center;
  font-weight: 800;
  box-shadow: 0 4px 0 rgba(0, 0, 0, 0.18), var(--shadow-sm);
}
.whosthis-attempt:hover:not(:disabled) { filter: brightness(1.06); }
.whosthis-attempt.is-spent { background: var(--card-sunk); color: var(--ink-faint); border: 2px dashed var(--line); }
.whosthis-attempt.is-guessing { background: var(--brand-dark); }
/* The verdict and the win/lose sheets. */
#whosthis-verdict-sheet.is-wrong h2 { color: var(--stop); }
.whosthis-result-sheet.is-winner { background: #dff3e4; border: 4px solid var(--go); }
.whosthis-result-sheet.is-winner h2 { color: var(--go); font-size: clamp(1.6rem, 4vw, 2.6rem); }
.whosthis-result-sheet.is-loser { background: #fbe3e3; border: 4px solid var(--stop); }
.whosthis-result-sheet.is-loser h2 { color: var(--stop); }
.whosthis-result-sheet #btn-whosthis-again { margin-top: 14px; }
.whosthis-wait-sheet { text-align: center; max-width: 420px; }
.whosthis-wait-sheet .pairs-wait-dots { justify-content: center; margin-top: 10px; }
.whosthis-grid {
  min-height: 0;
  min-width: 0;
  display: grid;
  grid-template-columns: repeat(var(--wt-cols, 6), var(--wt-card, 100px));
  gap: 8px;
  /* Top-left, not centred (v4.109.0). On a browser window — wider and
     shorter than an iPad — the deck is height-bound and used to float in
     the middle of its column while the child's own card sat at the top:
     Adam, "make sure that when playing on a browser, your card console on
     the left always aligns with the top of the card deck laid out on the
     right… this looks like it's been optimised for iPad at the expense of
     browser." */
  justify-content: start;
  align-content: start;
}
/* Portrait: the own card goes across the top and the grid under it. */
@media (max-aspect-ratio: 1 / 1) {
  .whosthis-stage { grid-template-columns: minmax(0, 1fr); grid-template-rows: auto minmax(0, 1fr); }
  /* Own card and the blobs side by side across the top. */
  .whosthis-side { grid-template-columns: minmax(0, 1fr) auto; align-items: center; }
  .whosthis-attempt { --blob: clamp(56px, 9dvh, 84px); }
  .whosthis-mine { grid-template-columns: auto 1fr; grid-template-rows: auto; align-items: center; justify-items: start; }
  .whosthis-mine-label { text-align: left; }
  .whosthis-mine .wt-card { width: 110px; }
}
.draw-status.is-wrong { color: var(--stop); animation: wt-shake 420ms ease; }
@keyframes wt-shake {
  0%, 100% { transform: translateX(0); }
  25% { transform: translateX(-6px); }
  75% { transform: translateX(6px); }
}
.whosthis-confirm-sheet { display: grid; justify-items: center; gap: 14px; --wt-card: 180px; }
.whosthis-result-faces { display: flex; justify-content: center; gap: 22px; flex-wrap: wrap; --wt-card: 150px; }
.whosthis-result-face { display: grid; justify-items: center; gap: 6px; }
.whosthis-result-face h3 { margin: 0; font-size: 1rem; }
.whosthis-result-face.is-found .wt-card-face { box-shadow: 0 0 0 4px var(--go), var(--shadow-md); }

/* The teacher's console. */
.whosthis-class-layout {
  display: grid;
  grid-template-columns: minmax(240px, 320px) minmax(0, 1fr);
  gap: 18px;
  align-items: start;
}
@media (max-width: 760px) { .whosthis-class-layout { grid-template-columns: minmax(0, 1fr); } }
.whosthis-teacher-card { display: grid; justify-items: center; --wt-card: 100%; }
.whosthis-teacher-card .wt-card { max-width: 260px; cursor: pointer; padding: 0; border: none; background: none; font: inherit; }
.whosthis-face-strip {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  gap: 5px;
  width: 100%;
}
.whosthis-face-pick {
  padding: 0;
  border: 2px solid transparent;
  border-radius: 8px;
  background: var(--card-raised);
  cursor: pointer;
  aspect-ratio: 1;
  overflow: hidden;
}
.whosthis-face-pick img { width: 100%; height: 100%; object-fit: cover; display: block; }
.whosthis-face-pick.is-on { border-color: var(--go); box-shadow: 0 0 0 3px var(--brand-tint); }
.shout-progress-row.is-got .shout-progress-fill { background: var(--go); }
.whosthis-dash-seats { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 8px; }
.whosthis-dash-seat { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 8px; align-items: center; --wt-card: 64px; }
.whosthis-dash-seat-text { display: grid; gap: 2px; min-width: 0; }
.whosthis-dash-seat.is-winner .wt-card-face { box-shadow: 0 0 0 3px var(--go), var(--shadow-md); }
.wt-card.is-small { width: var(--wt-card, 64px); }


/* ---- Set builders, the shared shape (v4.67.0) ---------------------------
   Squarez's Add Set pop-up and Pairs' "On the cards" are the same thing: a
   list of ROWS, one word each, with the picture and the ways of getting one
   to its right. Adam: "individual rows in two columns. On the left, there
   will be a box for me to write a word. On the right, there will be an option
   for me to either paste a screenshot or else upload a picture." */
.set-rows { display: grid; gap: 8px; }
.set-row {
  border: 2px solid var(--line);
  border-radius: var(--r-md);
  background: var(--card-sunk);
  padding: 6px 8px;
}
.set-row-main { display: flex; align-items: center; gap: 8px; flex-wrap: nowrap; }
.set-row-word { flex: 1 1 10rem; min-width: 6rem; }
input.set-row-word { padding: 8px 10px; font-size: 0.95rem; }
/* "Make sure the pictures are not too big." */
.set-row-pic {
  flex: none;
  width: 44px;
  height: 44px;
  border-radius: 8px;
  background: #fff;
  border: 1px solid var(--line);
  display: grid;
  place-items: center;
  overflow: hidden;
}
.set-row-pic img { width: 100%; height: 100%; object-fit: contain; display: block; }
.set-row-nopic { font-family: var(--font-display); font-weight: 900; color: var(--ink-faint); font-size: 0.95rem; }
.set-row-paste {
  flex: 1 1 9rem;
  min-width: 7rem;
  min-height: 34px;
  border: 2px dashed var(--line-strong);
  border-radius: var(--r-sm);
  padding: 5px 8px;
  font-size: 0.78rem;
  color: var(--ink-faint);
  background: var(--card);
  outline: none;
  overflow: hidden;
  white-space: nowrap;
}
.set-row-paste:empty::before { content: attr(data-placeholder); }
.set-row-paste:focus { border-style: solid; border-color: var(--brand); }
.set-row-drop { white-space: nowrap; flex: none; cursor: pointer; }
.set-row-drop.is-dropping { border-color: var(--brand); background: var(--brand-tint); }
.set-row .btn.tiny.is-on { border-color: var(--brand); background: var(--brand-tint); }
.set-row-web { margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--line); }
/* Illustrations / Photos (v4.106.0) — a small segment above the results. */
.set-row-style { margin-bottom: 6px; }
.seg.tiny button { min-height: 30px; padding: 4px 12px; font-size: 0.8rem; }
.set-row-web .pairs-web-results { margin-top: 6px; }
@media (max-width: 720px) {
  .set-row-main { flex-wrap: wrap; }
  .set-row-paste { flex-basis: 100%; }
}

/* The Add Set sheet: a fixed head and a body that scrolls INSIDE it, so the
   scrollbar sits under the rounded corners rather than cutting through them.
   Adam: "the curved edge of the window is ruined by the square or
   rectangular-shaped scrolling bar." */
.set-sheet {
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  gap: 14px;
  overflow: hidden;
  padding: 0;
}
.set-sheet-head { padding: clamp(16px, 2.5vw, 26px) clamp(20px, 3vw, 34px) 0; gap: 10px; }
.set-sheet-head h2 { margin: 0; }
.set-sheet-body {
  overflow-y: auto;
  padding: 0 clamp(20px, 3vw, 34px) clamp(20px, 3vw, 34px);
  display: grid;
  gap: 14px;
  align-content: start;
}
/* A circle, not an oblong: .btn's own min-height (56px) was stretching it (v4.82.0). */
#builder-add-row { width: 48px; height: 48px; min-height: 48px; border-radius: 50%; font-size: 1.4rem; padding: 0; }

/* The Picture Library pop-up, shared (js/picture-library.js). Adam: "expand
   the size of the column to the left so that nouns, verbs, and adjectives all
   show up together… the cards do not tab down onto a second line… Make sure
   that [Add] lines up perfectly with the search box." */
.set-library-sheet {
  width: min(1180px, 96vw);
  max-height: 92dvh;
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  gap: 12px;
  overflow: hidden;
}
.set-library-head { flex-wrap: nowrap; }
.set-library-head .btn { height: 42px; display: inline-flex; align-items: center; }
input.set-library-search { flex: 0 1 320px; min-width: 120px; height: 42px; box-sizing: border-box; margin: 0; }
.set-library-cols {
  display: grid;
  grid-template-columns: minmax(0, 15rem) minmax(0, 1fr);
  gap: 14px;
  min-height: 0;
  max-height: 78dvh;
}
.set-library-side {
  display: grid;
  gap: 5px;
  align-content: start;
  min-height: 0;
  overflow-y: auto;
  padding-right: 4px;
}
.set-library-side .backs-cat {
  width: 100%;
  justify-content: flex-start;
  text-align: left;
  padding: 4px 10px;
  font-size: 0.82rem;
  border-width: 1px;
  border-radius: var(--r-sm);
  white-space: nowrap;
}
.set-library-side .backs-cat .backs-cat-count { font-size: 0.72rem; padding: 0 5px; }
.picture-library-part {
  font-family: var(--font-display);
  font-weight: 900;
  font-size: 0.8rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-soft);
  margin: 8px 0 2px;
}
.picture-library-part:first-child { margin-top: 0; }
.set-library-main { min-height: 0; overflow-y: auto; }
.set-library-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(11.5rem, 1fr));
  gap: 8px;
  align-content: start;
}
.picture-library-chip { width: 100%; justify-content: flex-start; white-space: nowrap; overflow: hidden; }
.picture-library-chip span { overflow: hidden; text-overflow: ellipsis; }
.picture-library-chip .backs-chip-img { width: 34px; height: 34px; }
.picture-library-chip:disabled { opacity: 0.55; cursor: default; }
@media (max-width: 760px) {
  .set-library-cols { grid-template-columns: 1fr; }
  .set-library-side { max-height: 30dvh; }
}

/* The set-up screens' brand: logo hard left, the way back beneath it, at the
   ordinary topline size rather than the roster's hero size. Adam, of Squarez,
   Hot Seat and Story Blocks alike. */
.roster-brand.is-setup .dash-game-logo { height: clamp(56px, 9vh, 96px); }
.backs-setup-topline.is-left { grid-template-columns: auto 1fr auto; }
.backs-setup-topline.is-left > .roster-brand { grid-column: 1; }
.backs-setup-topline.is-left .backs-setup-logo { max-height: clamp(56px, 9vh, 96px); }
/* Hard left, both of them (v4.76.0). The logo used to keep its own
   `justify-self: center` inside the brand column, so it sat centred over
   the wider "Choose a different game" link rather than flush with it. */
.backs-setup-topline.is-left .roster-brand .backs-setup-logo { justify-self: start; }
/* Pairs' builder: the two cards stacked, and clear of the bar above them.
   Adam: "the spacing between the 'Create your new word set' card and the
   header card needs to be opened. They are crushing into each other." */
.set-builder { grid-template-columns: 1fr; margin-top: 18px; }

/* Hot Seat / What's That Word's builder in the same shape (v4.76.0), with
   the word library folded away in a third card under the list — Adam: "the
   option to collapse or open the word library… hidden until they require
   it." Two columns in there now (categories, cards): the set itself is the
   card above, not a column beside them. */
.backs-picker-screen .set-builder {
  max-width: min(96vw, 86rem);
  margin-left: auto;
  margin-right: auto;
}
.backs-picker-cols.is-library { grid-template-columns: minmax(0, 11rem) minmax(0, 1fr); }
@media (max-width: 820px) {
  .backs-picker-cols.is-library { grid-template-columns: 1fr; }
}
.backs-library .backs-picker-main { max-height: 70dvh; overflow-y: auto; }
.backs-library .backs-picker-side { max-height: 70dvh; overflow-y: auto; }
#btn-backs-picker-save:disabled { opacity: 0.55; }

/* Story Blocks' own sets (v4.67.0). Adam: "Make the green button for 'New
   set' a little smaller. At the moment, there's a lot of weird green space
   below it. The text to the right that says 'No sets of your own yet' should
   be centralised in the middle of the green box to the left. Also, make that
   into italics… the name of the set should be on the top row in bold, and the
   number of words in it should be beneath it in non-bold. They should not
   appear in little squares but should be consistent with the new set box." */
#storyblocks-sets-card .backs-set-strip { --backs-tile-h: 50px; align-items: center; }
#storyblocks-sets-card .backs-new-set { padding: 0 16px; align-content: center; }
#storyblocks-sets-card .backs-new-set .tiny { display: none; }
#storyblocks-sets-card .backs-set-row > p { margin: 0; font-style: italic; align-self: center; }
#storyblocks-saved-sets .backs-set-card {
  position: relative;
  height: var(--backs-tile-h);
  padding: 0 30px 0 16px;
  gap: 0;
  align-content: center;
  justify-items: start;
  text-align: left;
}
#storyblocks-saved-sets .backs-set-card.on { border-color: var(--go); box-shadow: 0 0 0 3px var(--go-tint, rgba(47, 145, 89, 0.2)); }
#storyblocks-saved-sets .backs-set-main {
  display: grid;
  gap: 1px;
  padding: 0;
  border: none;
  background: none;
  font: inherit;
  color: inherit;
  cursor: pointer;
  text-align: left;
  line-height: 1.15;
}
#storyblocks-saved-sets .backs-set-main b { font-weight: 800; }
#storyblocks-saved-sets .backs-set-main .tiny { font-weight: 400; color: var(--ink-soft); }
#storyblocks-saved-sets .backs-set-x {
  position: absolute;
  top: 4px;
  right: 6px;
  border: none;
  background: none;
  font: inherit;
  font-size: 1rem;
  line-height: 1;
  color: var(--ink-faint);
  cursor: pointer;
  padding: 2px 4px;
}
#storyblocks-saved-sets .backs-set-x:hover { color: var(--sq-red); }
/* Any chosen-list picture without its own size class stays small too. */
.backs-chosen-row > img { width: 26px; height: 26px; object-fit: contain; background: #fff; border-radius: 4px; flex: none; }

/* Hot Seat's teams panel (v4.67.0). Adam: "make sure that the reset scores
   and add a team bubbles do not protrude outside the card." A pill's own
   padding was wider than a third-of-the-screen column on a laptop; both are
   kept inside the panel's box now, whatever its width. */
.backs-panel .btn { max-width: 100%; box-sizing: border-box; white-space: nowrap; }
#btn-backs-reset-scores { padding: 6px 12px; min-height: 34px; font-size: 0.82rem; flex: none; }
#btn-backs-add-team { width: 100%; justify-self: stretch; padding-left: 12px; padding-right: 12px; }
/* …and still protruding in Safari (v4.76.0: "The Reset Scores and Add a
   Team buttons are still protruding outside the Teams card"). Chromium was
   fine, WebKit was not: the panel is a grid with one implicit `auto` track,
   and WebKit sizes that track from each row's min-content — where a team
   name box's `flex: 1` (a 0% basis) resolves against an indefinite width to
   the input's own intrinsic 20-character size, about 20px more than the
   panel has. So the track grew past the panel and every full-width thing in
   it hung out of the right-hand edge. A pixel basis, a zero width and a
   track that can never exceed the panel between them stop it in both. */
.backs-panel { grid-template-columns: minmax(0, 1fr); min-width: 0; }
.backs-panel > * { min-width: 0; }
.backs-team-list .backs-team input { flex: 1 1 0px; width: 0; }
.set-row .btn.tiny { padding: 7px 13px; min-height: 36px; font-size: 0.85rem; }

/* ---- The site header (the rebrand, v4.120.0) ----------------------------
   An opaque cream bar across the top of every inner page: the mark and the
   name as one link (`.site-brand.home-badge` — the console's leave-the-
   lesson confirm still hangs off .home-badge), the page title, the nav, and
   the account. It replaces the fixed badge that floated over the corner of
   every page and the padding every bar carried to stay clear of it. */
.site-header {
  position: relative; z-index: 2;
  display: flex; align-items: center; gap: 18px; flex-wrap: wrap;
  padding: 12px clamp(14px, 3vw, 28px);
  background: rgba(255, 255, 255, 0.7); -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--line);
}
.site-brand {
  display: inline-flex; align-items: center; gap: 10px; text-decoration: none;
  color: var(--ink); font-size: 1.6rem;
}
/* The console's header is a slim strip: the game screens beneath were
   built to fit one screen and it comes out of their room. */
body:has(> .wrap) > .site-header { padding-top: 6px; padding-bottom: 6px; }
body:has(> .wrap) > .site-header .site-brand { font-size: 1.35rem; }
body:has(> .wrap) > .wrap { padding-top: 12px; }
/* The set-up screens built to fit one screen give up the page's bottom
   gutter, which the header's height now needs. */
body:has(> .wrap) > .wrap:has(> #screen-backs-setup:not(.hidden)), body:has(> .wrap) > .wrap:has(> #screen-storyblocks:not(.hidden)) { padding-bottom: 12px; }
body:has(> .wrap) > .site-header { gap: 12px; }
body:has(> .wrap) > .site-header .site-nav a { padding: 6px 10px; font-size: 0.9rem; }
#btn-test-student { padding: 5px 12px; min-height: 0; font-size: 0.85rem; }
.site-brand:hover { opacity: 0.85; }
.site-title { font-family: var(--font-display); font-weight: 600; font-size: 1.15rem; color: var(--ink); padding-left: 14px; border-left: 2px solid var(--line); line-height: 1.2; }
.site-title:empty { display: none; }
.site-nav { display: flex; gap: 4px; margin-left: auto; flex-wrap: wrap; }
.site-nav a {
  padding: 8px 12px; border-radius: 999px; text-decoration: none;
  color: var(--ink); font-weight: 500; font-size: 0.95rem;
}
.site-nav a:hover { color: var(--brand-dark); }
.site-nav a[aria-current] { color: var(--brand); }
.site-account { display: flex; align-items: center; gap: 14px; }
.site-account a:not(.btn) { color: var(--ink); font-weight: 600; text-decoration: underline; text-underline-offset: 3px; }
.site-account .muted { color: var(--ink-soft); }
body.is-dash-window .site-header, body.is-card-window .site-header { display: none; }
/* Signed out, the header is the brand alone. */
body.is-signed-out .site-nav, body.is-signed-out .site-account { display: none; }
@media (max-width: 720px) {
  .site-header { gap: 10px; }
  .site-title { display: none; }
  .site-nav { margin-left: 0; width: 100%; order: 3; }
  .site-nav a { padding: 6px 10px; font-size: 0.9rem; }
  .site-account { margin-left: auto; }
}
/* The pages without a header bar (play, the projected screen, homework)
   carry the brand small in the corner, as the badge was; hidden on a
   child's live game screens, whose every pixel is the board. */
.site-brand.is-floating {
  position: fixed; top: 10px; left: 12px; z-index: 35;
  padding: 6px 14px; border-radius: 999px; background: var(--card); box-shadow: var(--shadow-md);
  font-size: 1.1rem;
}
body.is-live .site-brand.is-floating { display: none; }
/* (The account bubble of v4.74.0 is the site header now.) */

/* The site badge on the child's code screen (v4.69.0). */
.join-card .join-badge { --side: 72px; justify-self: center; margin-bottom: 4px; }
.join-card .join-badge.is-small { --side: 56px; }

/* ---- Treasure Hunt: chest, events, and the gold down the side (v4.69.0) --- */
.talks-chest, .talks-event {
  display: grid;
  justify-items: center;
  gap: calc(var(--board, 600px) * 0.008);
  width: 100%;
}
.talks-chest-img, .talks-event-img {
  width: calc(var(--board, 600px) * 0.2);
  height: calc(var(--board, 600px) * 0.2);
  object-fit: contain;
}
.talks-chest.is-open .talks-chest-img { animation: talks-chest-pop 0.6s cubic-bezier(0.2, 0.9, 0.3, 1.3); }
@keyframes talks-chest-pop {
  0% { transform: scale(0.6) rotate(-8deg); }
  60% { transform: scale(1.12) rotate(4deg); }
  100% { transform: scale(1) rotate(0); }
}
.talks-chest-text, .talks-event-text {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: calc(var(--board, 600px) * 0.036);
  line-height: 1.2;
  color: var(--ink);
  max-width: 100%;
}
.talks-chest-text { color: #8a5a12; }
.talks-continue { font-size: calc(var(--board, 600px) * 0.03); padding: 0.5em 1.4em; }
.talks-scores {
  flex: none;
  align-self: stretch;
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  gap: 6px;
  min-height: 0;
  background: #fbf0d6;
  border: 4px solid #7a5230;
  border-radius: var(--r-lg);
  padding: 8px;
  box-shadow: var(--shadow-md), inset 0 0 0 2px #e6c28f;
}
.talks-stage-inner.is-stacked .talks-scores { align-self: center; max-height: 110px; }
.talks-scores-title {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 900;
  font-size: 1rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: #7a5230;
  text-align: center;
}
.talks-score-list { display: grid; gap: 5px; align-content: start; min-height: 0; overflow-y: auto; }
.talks-stage-inner.is-stacked .talks-score-list { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
.talks-score-row {
  display: grid;
  /* Name | gold | +/− stacked (v4.77.0). The name column takes what is
     left and WRAPS — a name is never cut to "L…". */
  grid-template-columns: minmax(0, 1fr) auto auto;
  align-items: center;
  gap: 6px;
  padding: 4px 6px;
  background: var(--card);
  border: 2px solid transparent;
  border-radius: var(--r-sm);
}
.talks-score-row.is-current { border-color: var(--go); }
.talks-score-row.is-skipping { opacity: 0.6; }
.talks-score-who { display: flex; align-items: center; gap: 6px; min-width: 0; }
.talks-score-dot { width: 14px; height: 14px; border-radius: 50%; background: var(--token, #999); flex: none; }
.talks-score-name { font-weight: 700; font-size: 0.92rem; min-width: 0; overflow-wrap: anywhere; line-height: 1.15; }
.talks-score-buttons { flex-direction: column; }
.talks-score-gold { display: inline-flex; align-items: center; gap: 3px; }
.talks-score-coin { width: 18px; height: 18px; }
.talks-score-coins { font-family: var(--font-display); font-size: 1.05rem; min-width: 1.6ch; text-align: right; }
.talks-score-buttons { display: inline-flex; gap: 4px; }
/* The rows sized to their room (v4.76.0) — `--talks-row` is set by
   fitScores in js/talks-board.js: the column's height shared between the
   seats, so two names come out big and eight still fit. Adam: "make the
   scoreboard rows bigger — they can get more compact as teams are added."
   Everything in the row is a fraction of it. */
.talks-scores[style*='--talks-row'] .talks-score-row {
  min-height: var(--talks-row);
  padding: calc(var(--talks-row) * 0.08) calc(var(--talks-row) * 0.12);
  gap: calc(var(--talks-row) * 0.12);
}
.talks-scores[style*='--talks-row'] .talks-score-dot { width: clamp(14px, calc(var(--talks-row) * 0.2), 24px); height: clamp(14px, calc(var(--talks-row) * 0.2), 24px); }
.talks-scores[style*='--talks-row'] .talks-score-name { font-size: clamp(0.92rem, calc(var(--talks-row) * 0.26), 1.3rem); }
.talks-scores[style*='--talks-row'] .talks-score-coin { width: clamp(18px, calc(var(--talks-row) * 0.34), 34px); height: clamp(18px, calc(var(--talks-row) * 0.34), 34px); }
.talks-scores[style*='--talks-row'] .talks-score-coins { font-size: clamp(1.05rem, calc(var(--talks-row) * 0.42), 1.9rem); }
.talks-scores[style*='--talks-row'] .talks-score-btn { width: clamp(26px, calc(var(--talks-row) * 0.44), 46px); height: clamp(26px, calc(var(--talks-row) * 0.44), 46px); }
.talks-scores[style*='--talks-row'] .talks-score-btn::before,
.talks-scores[style*='--talks-row'] .talks-score-btn::after { width: clamp(12px, calc(var(--talks-row) * 0.2), 22px); height: clamp(2.5px, calc(var(--talks-row) * 0.04), 4px); }
/* A pocket that just took a coin gives a little bounce. */
.talks-score-row.is-bumping .talks-score-gold { animation: talks-bump 220ms ease-out; }
@keyframes talks-bump {
  0% { transform: scale(1); }
  50% { transform: scale(1.22); }
  100% { transform: scale(1); }
}

/* Coins in flight (v4.76.0): from the chest in the lagoon to the pocket on
   the scores column, one after another, with the count climbing as each
   lands (flyCoins in js/talks-board.js). */
.talks-coin-layer { position: absolute; inset: 0; pointer-events: none; overflow: visible; z-index: 6; }
.talks-flying-coin { position: absolute; opacity: 0; will-change: transform; filter: drop-shadow(0 2px 3px rgba(0, 0, 0, 0.3)); }

/* The chest's three topics (v4.76.0): a pop-up over the whole board. Adam:
   "a popup with three topic choices worth 3, 4 and 5 coins — the learner
   chooses, then talks for 30 seconds." */
.talks-choose {
  position: absolute;
  inset: 0;
  z-index: 5;
  display: grid;
  place-items: center;
  padding: 12px;
  background: rgba(20, 40, 40, 0.42);
  border-radius: var(--r-lg);
}
.talks-choose-sheet {
  width: min(100%, 720px);
  max-height: 100%;
  overflow-y: auto;
  display: grid;
  justify-items: center;
  gap: 12px;
  background: #fbf0d6;
  border: 4px solid #7a5230;
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-lg), inset 0 0 0 2px #e6c28f;
  padding: clamp(12px, 3vmin, 28px);
}
.talks-choose-chest { width: clamp(56px, 12vmin, 110px); height: auto; }
.talks-choose-title {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 900;
  font-size: clamp(1.1rem, 3.4vmin, 1.8rem);
  color: #7a5230;
  text-align: center;
}
.talks-choices { display: grid; gap: 10px; width: 100%; }
.talks-choice {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  gap: 14px;
  width: 100%;
  text-align: left;
  padding: clamp(10px, 2vmin, 16px) clamp(12px, 2.4vmin, 20px);
  background: var(--card);
  border: 3px solid #e6c28f;
  border-radius: var(--r-md);
  font: inherit;
  cursor: pointer;
  color: var(--ink);
}
.talks-choice:hover:not(:disabled) { border-color: var(--go); background: #f3fbf5; }
.talks-choice:active:not(:disabled) { transform: scale(0.99); }
.talks-choice:disabled { cursor: default; opacity: 0.85; }
.talks-choice-worth {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-family: var(--font-display);
  font-weight: 900;
  font-size: clamp(1.1rem, 3.6vmin, 2rem);
  color: #8a5a12;
}
/* The coins as shapes (v4.84.1). Adam: "The three coins should be in a
   triangle: one on top, two on the bottom. The four coins should be in a
   square: two on the top, two on the bottom. Leave the five coins at the
   bottom as they are" (three over two). One fixed-width box for all three
   so the questions line up, a six-column grid inside it: a coin spans two
   columns, so a row of three fills it, a row of two sits at columns 2–5,
   and a single coin at columns 3–4. */
.talks-choice-worth {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  justify-items: center;
  align-content: center;
  gap: 2px 0;
  width: clamp(64px, 13.5vmin, 116px);
  flex: none;
}
.talks-choice-worth img { width: clamp(18px, 4vmin, 34px); height: clamp(18px, 4vmin, 34px); grid-column: span 2; }
.talks-choice-worth[data-coins='3'] img:nth-child(1) { grid-column: 3 / span 2; }
.talks-choice-worth[data-coins='3'] img:nth-child(2) { grid-column: 2 / span 2; }
.talks-choice-worth[data-coins='3'] img:nth-child(3) { grid-column: 4 / span 2; }
.talks-choice-worth[data-coins='4'] img:nth-child(odd) { grid-column: 2 / span 2; }
.talks-choice-worth[data-coins='4'] img:nth-child(even) { grid-column: 4 / span 2; }
.talks-choice-worth[data-coins='5'] img:nth-child(4) { grid-column: 2 / span 2; }
.talks-choice-worth[data-coins='5'] img:nth-child(5) { grid-column: 4 / span 2; }

/* The open chest at the foot of the panel, coins peeking (v4.77.0). The
   coins sit behind the chest's rim in a box that shows only their top
   half; the chest picture is drawn over their bottoms. */
.talks-chest-peek {
  position: absolute;
  right: 2.5%;
  bottom: 2.5%;
  width: calc(var(--board, 600px) * 0.15);
  z-index: 2;
  pointer-events: none;
}
.talks-peek-chest { width: 100%; height: auto; display: block; }
/* The coins stand in the open chest: a box over its interior (the dark
   band is 41–59% of the picture's height), clipped at the front rim, so
   only their top halves show. */
.talks-peek-coins {
  position: absolute;
  left: 14%;
  right: 14%;
  bottom: 41%;
  height: 13%;
  display: flex;
  justify-content: center;
  align-items: flex-start;
  gap: 1%;
  overflow: hidden;
}
.talks-peek-coin { flex: 0 0 auto; width: 17%; }
.talks-peek-coin img { width: 100%; height: auto; display: block; }
.talks-chest-peek.is-emptied .talks-peek-coins { visibility: hidden; }
.talks-choice-text { font-weight: 700; font-size: clamp(0.95rem, 2.8vmin, 1.4rem); line-height: 1.25; }
.talks-choose-hint { margin: 0; color: var(--ink-soft); font-size: clamp(0.85rem, 2.2vmin, 1.05rem); text-align: center; }
/* The same three on the console, small. */
.talks-console-choices { display: grid; gap: 6px; margin-top: 6px; }
.talks-console-choice {
  display: grid;
  grid-template-columns: 5.5em minmax(0, 1fr);
  gap: 8px;
  align-items: center;
  text-align: left;
  padding: 8px 10px;
  background: var(--card);
  border: 2px solid var(--line);
  border-radius: var(--r-sm);
  font: inherit;
  font-size: 0.9rem;
  cursor: pointer;
  color: var(--ink);
}
.talks-console-choice:hover { border-color: var(--go); }
.talks-console-choice b { color: #8a5a12; }

/* Class set-up (v4.76.0): players and teams in two boxes. */
.talks-class-boxes { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; }
@media (max-width: 760px) { .talks-class-boxes { grid-template-columns: 1fr; } }
.talks-class-box {
  background: var(--card-sunk);
  border: 3px solid transparent;
  border-radius: var(--r-md);
  padding: 12px 14px;
  display: grid;
  gap: 6px;
  align-content: start;
}
.talks-class-box.is-live { border-color: var(--go); }
.talks-class-box h3 { margin: 0; }
.talks-class-box .btn.tiny { padding: 6px 12px; min-height: 32px; font-size: 0.82rem; }
/* One box per name (v4.77.0), stacked. */
.talks-name-list { display: grid; gap: 8px; }
/* One grid of names (v4.112.0): boxes across the card, not a column. */
.talks-name-grid { grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); }
/* Mode and level toggles on the rosters (v4.112.0). */
.talks-options { display: grid; gap: 10px; margin-bottom: 14px; }
.talks-options-row { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 16px; }
.talks-options-row .field { margin: 0; min-width: 10rem; }
.talks-name-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 6px; align-items: center; }
.talks-name-input { width: 100%; box-sizing: border-box; font-size: 1.05rem; font-weight: 700; padding: 10px 14px; }
.talks-name-remove { width: 30px; height: 30px; }
.talks-class-box > .btn { justify-self: start; }

/* Late arrivals on the console (v4.76.0). */
.talks-late { display: grid; gap: 8px; }
.talks-late-device {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 6px 8px 6px 12px;
  background: var(--card);
  border: 2px solid var(--line);
  border-radius: 999px;
}
.talks-late-names { font-weight: 700; }
.talks-console-gold { display: inline-flex; align-items: center; gap: 4px; }
.talks-console-gold .talks-score-coin { width: 18px; height: 18px; }
.talks-score-btn {
  width: 26px;
  height: 26px;
  border-radius: 50%;
  border: 2px solid;
  font: inherit;
  font-weight: 900;
  line-height: 1;
  display: grid;
  place-items: center;
  cursor: pointer;
  padding: 0;
}
.talks-score-btn.is-plus { border-color: var(--go); background: #e2f3e8; color: var(--go); }
.talks-score-btn.is-minus { border-color: var(--stop); background: #fbe3e3; color: var(--stop); }
/* The + and − as two bars, dead centre (v4.72.0) — no font can put a glyph
   exactly in the middle of a circle, so the glyph is not a glyph. */
.talks-score-btn { position: relative; }
.talks-score-btn::before,
.talks-score-btn::after {
  content: '';
  position: absolute;
  left: 50%;
  top: 50%;
  width: 12px;
  height: 2.5px;
  border-radius: 2px;
  background: currentColor;
  transform: translate(-50%, -50%);
}
.talks-score-btn.is-plus::after { transform: translate(-50%, -50%) rotate(90deg); }
.talks-score-btn.is-minus::after { display: none; }

/* The verdict (v4.72.0): a green tick and a red cross, big, side by side. */
.talks-judge { display: flex; gap: calc(var(--board, 600px) * 0.05); justify-content: center; }
.talks-judge-btn {
  width: calc(var(--board, 600px) * 0.14);
  height: calc(var(--board, 600px) * 0.14);
  border-radius: 50%;
  border: none;
  display: grid;
  place-items: center;
  color: #fff;
  cursor: pointer;
  box-shadow: var(--shadow-md);
  padding: 0;
}
.talks-judge-btn.is-tick { background: var(--sq-green); }
.talks-judge-btn.is-cross { background: var(--stop); }
.talks-judge-btn:hover { filter: brightness(1.06); }
.talks-judge-glyph { width: 52%; height: 52%; display: block; }
/* The ring is a button while a talk is on, where the screen may finish it. */
.talks-timer.is-tappable { cursor: pointer; }
.talks-timer.is-tappable:hover .talks-ring-fill { stroke: #34ac5b; }


/* ---- In The Park, teacher-led and into groups (v4.70.0) ---------------- */
/* Start Game with Teacher-led beneath it, the same size — Adam: "Beneath
   that, a button of the same size saying 'Teacher-led'." */
.roster-go-row.is-stacked { flex-direction: column; align-items: stretch; gap: 8px; }
/* Uniform in size (v4.101.0, Adam's template): every button in the stack
   takes the width of the widest, and a pill above them is centred. */
.roster-go-row.is-stacked .btn { min-width: 190px; }
.roster-go-row.is-stacked .btn:not(.hidden) { display: inline-flex; justify-content: center; }
.roster-go-row.is-stacked > .pill, .roster-go-row.is-stacked > .switch { align-self: center; }

/* The Teacher card. */
.park-teacher-card { padding: 14px 18px; border: 3px solid var(--brand); }
.park-teacher-badge {
  display: inline-grid;
  place-items: center;
  padding: 8px 16px;
  border-radius: 999px;
  background: var(--brand);
  color: #fff;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 1.05rem;
  letter-spacing: 0.02em;
}
.park-teacher-who { display: flex; flex-direction: column; gap: 2px; }
.park-teacher-who b { font-size: 1.15rem; }

/* The class leaderboard: one row per child, tappable to make that iPad the
   teacher's. */
.park-class-board { display: flex; flex-direction: column; gap: 8px; }
.park-class-row {
  display: grid;
  grid-template-columns: 28px 12px minmax(120px, 1fr) minmax(90px, 2fr) auto auto;
  align-items: center;
  gap: 12px;
  width: 100%;
  padding: 10px 14px;
  border: 2px solid var(--line);
  border-radius: var(--r-md);
  background: var(--card-raised);
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
}
.park-class-row:hover { border-color: var(--brand); }
.park-class-row.is-marked { border-color: var(--go); }
.park-class-rank { font-family: var(--font-display); font-weight: 800; color: var(--ink-soft); text-align: center; }
.park-class-name { font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.park-class-progress { position: relative; height: 12px; border-radius: 999px; background: var(--line); overflow: hidden; }
.park-class-progress-fill { position: absolute; inset: 0 auto 0 0; background: var(--go); border-radius: 999px; }
.park-class-points { font-family: var(--font-display); font-weight: 800; white-space: nowrap; min-width: 4.5em; text-align: right; }
.park-class-best { white-space: nowrap; min-width: 4em; text-align: right; }

/* "Get into your groups": the big green circle, the groups down one side. */
.park-groups-card {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(220px, 30%);
  gap: 24px;
  align-items: stretch;
  min-height: min(62dvh, 560px);
}
.park-groups-stage { display: grid; place-items: center; }
.park-groups-go {
  aspect-ratio: 1;
  width: clamp(200px, 42dvh, 380px);
  border-radius: 50%;
  border: none;
  background: var(--sq-green);
  color: #fff;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(1.3rem, 3.4dvh, 2.2rem);
  line-height: 1.15;
  padding: 1em;
  box-shadow: var(--shadow-lg);
  cursor: pointer;
}
.park-groups-go:hover:not(:disabled) { filter: brightness(1.05); transform: scale(1.02); }
.park-groups-go:disabled { opacity: 0.5; cursor: default; }
.park-groups-side { border-left: 2px solid var(--line); padding-left: 24px; }
.park-groups-list { display: flex; flex-direction: column; gap: 8px; }
.park-groups-item { display: flex; flex-direction: column; padding: 8px 12px; border-radius: var(--r-md); background: var(--brand-tint); }
.park-groups-item b { font-size: 0.8rem; color: var(--ink-soft); text-transform: uppercase; letter-spacing: 0.04em; }
.park-groups-item span { font-weight: 700; font-size: 1.05rem; }
@media (max-width: 760px) {
  .park-groups-card { grid-template-columns: 1fr; }
  .park-groups-side { border-left: none; padding-left: 0; border-top: 2px solid var(--line); padding-top: 16px; }
}

/* The live board: a group's best, and a late joiner dragged (or tapped) in. */
.park-best { font-size: 0.85rem; font-weight: 700; color: var(--brand-dark); background: var(--brand-tint); padding: 2px 10px; border-radius: 999px; }
.park-waiting-chip { cursor: grab; font: inherit; }
.park-waiting-chip.picked { outline: 3px solid var(--brand); }
.park-card.is-target { border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-tint); cursor: pointer; }
.park-card.is-over { border-color: var(--go); background: var(--accent-tint); }
.name-chip.is-teacher { cursor: default; border-style: dashed; }
.name-chip.is-teacher .pill { margin-left: 6px; }

/* Who's This? live scoreboard (v4.71.0): a big logo and the join details on
   the top row, Play again beneath; pair cards with names and counts only. */
.whosthis-topline { display: grid; gap: 10px; }
.whosthis-topline-row { align-items: center; }
.topline .dash-game-logo.is-big { height: clamp(96px, 16vh, 170px); width: auto; max-width: 42%; }
.whosthis-dash-seat.is-faceless { grid-template-columns: minmax(0, 1fr); }

/* The join code at the very top right of the whole screen on the name step
   (v4.72.0), off the card. */
.name-code-corner { position: fixed; top: 12px; right: 12px; z-index: 5; margin: 0; }

/* In The Park's live board (v4.72.0): the join details top right, the three
   buttons smaller, under the QR code on that side. */
.park-topline-right { display: grid; justify-items: end; gap: 8px; margin-left: auto; }
.park-topline-buttons { justify-content: flex-end; }
.park-topline-buttons .btn { padding: 7px 13px; min-height: 36px; font-size: 0.85rem; }

/* Story Blocks (v4.72.0): the console's card at the very top with the logo
   hard left — Adam: "the Story Blocks icon is clearly shown, aligned left…
   right at the top of the screen, with nothing above it and no white space
   above it." The logo is bigger and starts the bar; under 1320px the badge
   in the corner would sit on it, so the bar makes room at its left instead
   of the page making room above. */
#screen-storyblocks .backs-setup-topline.is-left .backs-setup-logo { justify-self: start; max-height: clamp(76px, 12vh, 124px); height: clamp(76px, 12vh, 124px); }
/* (Story Blocks' bar is a .backs-setup-topline, covered by the badge rule
   beside .home-badge.) */
/* The board's way back, beside Roll. */
.sb-actions { display: flex; align-items: center; justify-content: center; gap: 14px; position: relative; }
.sb-back { position: absolute; left: 0; }
.talks-judge-btn.is-small { width: 40px; height: 40px; box-shadow: none; }
.talks-judge-btn.is-small .talks-judge-glyph { width: 22px; height: 22px; }

/* Shouting Dictation's first screen (v4.75.0): documents left, previous
   games right; the documents scroll inside their card. */
/* Level and the same height (v4.77.3) — Adam: "the 'Choose Your Document'
   and 'Previous Games' tiles should be level with one another… always at
   least the same height, with scrolling where this is not possible." The
   two cards stretch to one row height, capped at the screen, and each
   card's list scrolls inside it. */
.shouting-docs-layout { display: grid; grid-template-columns: minmax(0, 3fr) minmax(0, 2fr); gap: 18px; align-items: stretch; }
@media (max-width: 900px) { .shouting-docs-layout { grid-template-columns: minmax(0, 1fr); } }
.shouting-docs-layout > .card { display: flex; flex-direction: column; min-height: 0; max-height: calc(100dvh - 150px); margin-top: 0; }
.shouting-docs-layout > .card > h2, .shouting-docs-layout > .card > .card-head { flex: none; margin-top: 0; }
.shouting-doc-scroll { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding-right: 4px; }
.shouting-games-list { display: grid; gap: 8px; align-content: start; }
/* The name over each page on the reveal (v4.77.3): big and bold. */
.shout-result-name { margin: 0 0 6px; text-align: center; font-family: var(--font-display); font-weight: 800; font-size: clamp(1.2rem, 3dvh, 1.8rem); color: var(--brand-dark); }
.shouting-game-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  border: 2px solid var(--line);
  border-radius: var(--r-md);
  background: var(--card-raised);
}
.shouting-game-row b { display: block; }
.shouting-game-row span.tiny { color: var(--ink-soft); }
/* The Download PDF button's own text stays white (v4.77.3) — the muted
   rule above was catching the button's `tiny` class too: "At the moment,
   it's black, and I can't read it." */
.shouting-game-row .btn.tiny { color: #fff; }
/* The delete cross (v4.88.0): a red circle that is there all the time, not
   a quiet button the white-text rule above had made invisible. */
.shouting-game-delete {
  appearance: none; border: none; cursor: pointer; padding: 0; flex: none;
  width: 36px; height: 36px; border-radius: 50%;
  background: var(--danger); color: #fff;
  display: inline-grid; place-items: center;
  box-shadow: var(--shadow-sm);
}
.shouting-game-delete:hover { filter: brightness(0.92); }
/* The live bar's logo top-aligned with the QR code (v4.77.3): "make sure
   that the Shouting Dictation logo is higher up towards the top, so the top
   edge of it is aligned with the top edge of the QR code." The join card
   pads its picture by 8px, so the logo gets the same. */
.wrap > #screen-shouting > .topline { align-items: flex-start; }
.wrap > #screen-shouting > .topline .dash-game-logo.is-big { margin-top: 8px; }
/* The late arrivals' chips are buttons now: tap two to pair them. */
.waiting-chip.is-pickable { cursor: pointer; font: inherit; border: 2px solid var(--line); background: var(--card-raised); }
.waiting-chip.is-picked { border-color: var(--go); background: var(--accent-tint); box-shadow: 0 0 0 3px var(--accent-tint); }

/* In The Park (v4.76.0): "Waiting for … to finish" on green while it applies. */
#park-status.is-waiting { background: var(--go); color: #fff; border-radius: 999px; padding: 4px 18px; width: fit-content; margin: 0 auto; }

/* Squarez typing mode (v4.80.0). Adam: "instead of placing pictures on the
   game board, the children have to type words… during the game, the
   children click on a square and then start typing into it." */
/* The "How they play" card beside Your Own Sets on the set-up screen. */
/* Both cards the same height (v4.84.1, Adam: "make sure that the 'Choose
   Your Pictures' option is as tall as the cards that show just the picture
   and the word on the right of it") — stretched, so the shorter one grows
   to the taller. */
/* (The rebrand: the side panel sizes to its own content rather than
   stretching to the pictures panel.) */
.vocab-top-row { display: grid; grid-template-columns: minmax(0, 1fr) minmax(240px, 300px); gap: 16px; align-items: start; }
/* A set-up bar's page title, beside Start (the rebrand). */
.setup-title { font-family: var(--font-display); font-weight: 600; color: var(--ink-soft); margin-right: 6px; }
.topline.is-sticky { position: sticky; top: 0; z-index: 20; box-shadow: var(--shadow-md); }
.vocab-top-row > .card { margin: 0; }
.vocab-mode-card h2 { margin-bottom: 10px; }
.seg.is-stacked { display: grid; grid-template-columns: 1fr; border-radius: 16px; width: 100%; }
.seg.is-stacked button { border-radius: 12px; text-align: left; }
@media (max-width: 860px) { .vocab-top-row { grid-template-columns: minmax(0, 1fr); } }

/* A typing square is a box with a text field across it — a <div>, since a
   field cannot live inside a <button>. The field is as wide as the square
   and sized to it, so a long word shrinks rather than scrolls. */
.square.is-typing { cursor: text; padding: 6%; display: flex; align-items: center; justify-content: center; }
/* A textarea that wraps at spaces and grows line by line, shrunk by
   fitTypedInput only when it still will not fit (v4.85.0). Semi-transparent
   over the square's colour, the words in a medium blue — Adam's own asks. */
.square.is-typing .square-input {
  width: 100%; min-width: 0; box-sizing: border-box; display: block;
  font: inherit; font-weight: 800; text-align: center; line-height: 1.15;
  font-size: clamp(1rem, 3.8vmin, 1.9rem);
  padding: 0.3em 0.4em; border-radius: 12px;
  border: 3px solid rgba(31, 39, 51, 0.22); background: rgba(255, 255, 255, 0.55); color: #2b6cb0;
  min-height: 0; resize: none; overflow: hidden; white-space: pre-wrap; overflow-wrap: break-word;
}
.square.is-typing .square-input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.5); }
.square.is-typing.locked .square-input { background: rgba(255, 255, 255, 0.4); }
/* Free typing has no word bank, so the tray row is just the finish bar:
   the board keeps its size and sits centred in the room the bank used to
   take, and the bar is a short strip the width of the board. */
#screen-game.is-typing.no-bank { grid-template-rows: var(--turnprompt-h) 1fr auto var(--foot-h); }
#screen-game.is-typing.no-bank .board { height: var(--board-h); align-self: center; }
#screen-game.is-typing.no-bank .tray-wrap { height: auto; }
#screen-game.is-typing.no-bank .tray { justify-content: center; }
#screen-game.is-typing.no-bank .tray.is-bank .typing-finish { aspect-ratio: auto; height: auto; min-height: 44px; padding: 8px 16px; flex: none; }
/* The tray in typing mode is a word bank: pictures to type from, nothing to drag. */
.tray.is-bank .tile { cursor: default; }
/* The teacher's and the reveal's small boards: the word as it was typed.
   Right is the picture with the word under it; wrong is the word alone. */
.mini .typed {
  position: absolute; left: 3px; right: 3px; bottom: 3px; font-size: 0.62rem; font-weight: 700; line-height: 1.15;
  background: rgba(255, 255, 255, 0.9); border-radius: 6px; padding: 2px 3px; text-align: center;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ink);
}
.mini .typed.is-wrong { color: var(--danger); text-decoration: line-through; }
.mini .typed.is-alone { position: static; font-size: 0.8rem; white-space: normal; overflow-wrap: anywhere; }
/* Typed boards in the dashboard window (v4.101.0). Adam: "what comes up is
   not legible because it's too small, and all the text that has been typed
   onto the boards is garbled. Change this so that the boards are much
   bigger and legible." A typing card takes the whole row, each child's
   board a third of it, and the word is the square's content — big, wrapped,
   never clipped — with the picture (when there is one) small above it. */
/* The dashboard window's boards fill their card (v4.109.0, Adam: "a lot of
   dead space to the right of every game. Expand the size of each board into
   this space so it fills each card from left to right. If there is a group
   of three… make their card bigger to accommodate the extra board"): a
   pair's card has two tracks, a three's card takes the whole row and has
   three. v4.18.0's "always three tracks so every board is the same size"
   gives way to this — the three's wider card is what keeps its boards the
   pair's size. */
/* The grid is four tracks: a pair's card spans two (half the row), a
   three's spans three (three quarters) — so a three's card is exactly a
   board wider than a pair's and every board on the screen is the same
   size. A wide window gets six tracks (three pairs a row, a three across
   half of it); a narrow one is one card a row. */
body.is-dash-window #dash { container-type: inline-size; grid-template-columns: repeat(4, minmax(0, 1fr)); }
body.is-dash-window #dash .dash-card { grid-column: span 2; }
body.is-dash-window #dash .dash-card.is-three { grid-column: span 3; }
@container (min-width: 1500px) {
  body.is-dash-window #dash { grid-template-columns: repeat(6, minmax(0, 1fr)); }
}
@container (max-width: 760px) {
  body.is-dash-window #dash { grid-template-columns: minmax(0, 1fr); }
  body.is-dash-window #dash .dash-card, body.is-dash-window #dash .dash-card.is-three { grid-column: 1 / -1; }
}
body.is-dash-window #dash .dash-card .dash-seat-col + .dash-seat-col { border-left: 0; padding-left: 0; } /* every board the same width */
body.is-dash-window #dash .dash-card[data-seats='2'] .dash-seats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
body.is-dash-window #dash .dash-card[data-seats='1'] .dash-seats { grid-template-columns: minmax(0, 1fr); }
body.is-dash-window #dash .dash-card.is-three .dash-seats { grid-template-columns: repeat(3, minmax(0, 1fr)); }
/* A waiting child dragged (or tapped) onto a card in the dashboard window. */
#dash.is-dragging .dash-card.is-droppable, #dash.is-placing .dash-card.is-droppable { border-style: dashed; border-color: var(--go); cursor: copy; }
#dash .dash-card.is-over, #dash.is-placing .dash-card.is-droppable:hover { background: var(--accent-tint); box-shadow: 0 0 0 3px var(--accent-tint); border-style: solid; }
body.is-dash-window #dash .dash-card.is-typing { grid-column: 1 / -1; }
body.is-dash-window #dash .dash-card.is-typing .dash-seats { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 24px; }
body.is-dash-window #dash .dash-card.is-typing .dash-seat-col + .dash-seat-col { border-left: 0; padding-left: 0; } /* every board the same width */
body.is-dash-window #dash .dash-card.is-typing .dash-seat-col .dash-seat { font-size: 1.05rem; }
body.is-dash-window #dash .dash-card.is-typing .mini-board { gap: 8px; }
body.is-dash-window #dash .dash-card.is-typing .mini-board { min-width: 0; }
/* min-width: 0 — a grid item's min-content width is the longest word, and
   a board of six such squares was wider than its column. */
body.is-dash-window #dash .dash-card.is-typing .mini { min-width: 0; overflow: hidden; border-radius: 12px; border-width: 2px; grid-template-rows: auto auto; align-content: center; gap: 4px; padding: 6px; }
body.is-dash-window #dash .dash-card.is-typing .mini .pic svg, body.is-dash-window #dash .dash-card.is-typing .mini .pic img { width: clamp(28px, 3.2vw, 48px); height: clamp(28px, 3.2vw, 48px); }
body.is-dash-window #dash .dash-card.is-typing .mini .typed {
  position: static; max-width: 100%; box-sizing: border-box;
  font-size: clamp(1rem, 1.7vw, 1.5rem); line-height: 1.2; padding: 4px 8px; border-radius: 8px;
  white-space: normal; overflow: visible; text-overflow: clip; overflow-wrap: anywhere;
}
body.is-dash-window #dash .dash-card.is-typing .mini .typed.is-alone { font-size: clamp(1.1rem, 2vw, 1.7rem); }
/* The Finished button heads the word bank once every square has a word
   (no auto-submit while a child may still be typing). It is a tile-sized
   green button, so it takes a tile's slot rather than a row of its own. */
.tray.is-bank .typing-finish { cursor: pointer; font-weight: 800; font-size: clamp(0.9rem, 2.4vmin, 1.2rem); padding: 0 8px; background: var(--accent); color: #fff; border-color: var(--accent); }

/* Who's This? — the teacher's card in a window of its own (v4.80.1): the
   class screen with everything but the card hidden, and no page furniture. */
body.is-card-window .card-window-hidden { display: none !important; }
body.is-card-window .wrap > #screen-whosthis-class > .topline { display: none; }
body.is-card-window .whosthis-class-layout { grid-template-columns: minmax(0, 1fr); margin-top: 0 !important; }
body.is-card-window .whosthis-teacher-card { padding: 12px; }
body.is-card-window .whosthis-teacher-card .wt-card { max-width: 320px; }
body.is-card-window .wrap { padding: 8px !important; }
body.is-card-window .home-badge { display: none; }

/* The cross beside a name on a live board (v4.81.0) — see removeX in
   teacher.js. Quiet until hovered; a round target big enough for a finger. */
.dash-remove {
  appearance: none; border: none; background: transparent; color: var(--ink-faint);
  width: 26px; height: 26px; border-radius: 50%; display: inline-grid; place-items: center;
  cursor: pointer; padding: 0; flex: none; margin-left: 2px;
}
.dash-remove:hover { background: var(--danger); color: #fff; }
.dash-seat .dash-remove { margin-left: auto; }

/* The projected card fades in rather than popping (v4.81.0) — one more
   thing between "the window opened" and "the board is there" that read as
   a stagger on Story Blocks. Opacity only: a transform here would become
   the containing block for anything fixed inside. */
@keyframes lp-card-in { from { opacity: 0; } to { opacity: 1; } }
.leaderboard-page .lp-card:not(.hidden) { animation: lp-card-in 0.3s ease; }

/* "How will students play the game?" (v4.82.0): two tiles, the chosen one
   lit — the same tile the mode picker uses. Side by side since v4.106.0
   (Adam: "Put these cards side by side to save some space. Remove the
   subheading in each card"), and the same shape carries "Pairs or Groups /
   Play as a Class" on Squarez's set-up screen and Drawing Dictation's
   roster now that the separate mode screen is gone. */
.play-how { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.play-how .mode-card { padding: 16px 18px; }
.play-how .mode-card h3 { margin: 0; }
@media (max-width: 640px) { .play-how { grid-template-columns: minmax(0, 1fr); } }
.play-how .mode-card[aria-pressed='true'] { border-color: var(--brand); background: var(--brand-tint); }

/* ======================================================================== */
/* Mapz (v4.90.0) — the town, the walker, the side panel                     */
/* ======================================================================== */

main#screen-mapz {
  position: relative;
  display: grid;
  grid-template-rows: minmax(0, 1fr);
  gap: var(--game-gap);
  min-height: 0;
  padding-top: 34px; /* under the connection pill */
}
.mapz-body {
  /* The map on the left, the console right up against it (v4.103.0):
     the pair is centred as one, the map sized by layoutMapz to the room
     the console leaves. */
  display: flex;
  justify-content: center;
  align-items: stretch;
  gap: 12px;
  min-height: 0;
}
.mapz-stage { min-height: 0; min-width: 0; flex: 0 0 auto; display: flex; align-items: center; justify-content: center; }
.mapz-side { flex: 0 0 clamp(230px, 30vw, 380px); }
/* The map's box is square and never turns; the rotor inside it does. */
.mapz-map {
  position: relative;
  flex: none;
  border-radius: var(--r-md);
  overflow: hidden;
  box-shadow: var(--shadow-md);
  background: #b7d98c;
  -webkit-user-select: none;
  user-select: none;
  touch-action: none;
}
.mapz-board { position: relative; }
.mapz-rotor {
  position: absolute;
  inset: 0;
  transform-origin: 50% 50%;
  transition: transform 0.45s cubic-bezier(0.4, 0, 0.2, 1);
  will-change: transform;
}
@media (prefers-reduced-motion: reduce) { .mapz-rotor { transition-duration: 0.15s; } }
.mapz-map-img, .mapz-mini img, .mapz-console-map img { display: block; width: 100%; height: 100%; object-fit: fill; -webkit-user-drag: none; pointer-events: none; }
.mapz-layer { position: absolute; inset: 0; }
/* A block: invisible until a building is in the air, then a dashed ring
   on every empty one and a solid one under the finger. */
.mapz-plot {
  position: absolute;
  border: 3px dashed transparent;
  border-radius: 10px;
  pointer-events: none;
  transition: border-color 0.15s, background 0.15s;
}
.mapz-plot.is-droppable { border-color: rgba(255, 255, 255, 0.9); background: rgba(255, 255, 255, 0.18); }
.mapz-plot.is-hover { border-style: solid; border-color: #f7c948; background: rgba(247, 201, 72, 0.35); }
.mapz-buildings { position: absolute; inset: 0; pointer-events: none; }
/* A building: a picture at the centre of its block, its name under it,
   turned back upright however the map is turned. */
.mapz-building {
  position: absolute;
  width: 12.5%;
  transform: translate(-50%, -58%) rotate(var(--mapz-counter, 0deg));
  transition: transform 0.45s cubic-bezier(0.4, 0, 0.2, 1);
  display: grid;
  justify-items: center;
  gap: 0;
  pointer-events: auto;
  touch-action: none;
  z-index: 2;
}
.mapz-building img { display: block; width: 100%; height: auto; -webkit-user-drag: none; pointer-events: none; filter: drop-shadow(0 3px 4px rgba(0, 0, 0, 0.3)); }
.mapz-building-name {
  margin-top: -6%;
  background: rgba(255, 255, 255, 0.95);
  border: 2px solid var(--ink);
  border-radius: 999px;
  padding: 1px 7px;
  font-family: var(--font-display);
  font-size: clamp(0.5rem, 1.15vw, 0.85rem);
  font-weight: 800;
  white-space: nowrap;
  max-width: 160%;
  overflow: hidden;
  text-overflow: ellipsis;
  box-shadow: var(--shadow-sm);
  pointer-events: none;
}
.mapz-building.is-mine { cursor: grab; }
.mapz-building.is-mine:active { cursor: grabbing; }
.mapz-building.is-fixed .mapz-building-name { background: var(--ink); color: #fff; border-color: var(--ink); }
.mapz-building.is-truth { opacity: 0.55; pointer-events: none; z-index: 1; }
.mapz-building.is-truth .mapz-building-name { border-color: var(--go); color: #0b6d5d; border-style: dashed; max-width: 220%; white-space: normal; text-align: center; line-height: 1.1; }
.mapz-building-mark {
  position: absolute;
  top: -6px;
  right: -6px;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  display: none;
  place-items: center;
  color: #fff;
  font-weight: 900;
  font-size: 16px;
  border: 2px solid #fff;
}
.mapz-building.is-right .mapz-building-mark { display: grid; background: var(--go); }
.mapz-building.is-wrong .mapz-building-mark { display: grid; background: var(--danger); }
.mapz-building.is-wrong img { filter: drop-shadow(0 0 0 transparent) saturate(0.5) opacity(0.75); }
.mapz-map.is-dragging .mapz-building.is-mine { opacity: 0.85; }
/* The building in the air, following the finger. */
.mapz-drag-ghost {
  position: fixed;
  z-index: 60;
  width: clamp(72px, 9vw, 120px);
  transform: translate(-50%, -70%);
  pointer-events: none;
  display: grid;
  justify-items: center;
  filter: drop-shadow(0 8px 10px rgba(0, 0, 0, 0.35));
}
.mapz-drag-ghost img { width: 100%; height: auto; }
.mapz-drag-ghost span { background: #fff; border: 2px solid var(--ink); border-radius: 999px; padding: 1px 8px; font-family: var(--font-display); font-weight: 800; font-size: 0.8rem; margin-top: -8px; }
/* The start marker: a small flag on the junction. */
.mapz-start {
  position: absolute;
  transform: translate(-50%, -50%) rotate(var(--mapz-counter, 0deg));
  transition: transform 0.45s cubic-bezier(0.4, 0, 0.2, 1);
  background: var(--ink);
  color: #fff;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(0.5rem, 1vw, 0.75rem);
  letter-spacing: 0.06em;
  padding: 2px 7px;
  border-radius: 999px;
  pointer-events: none;
  z-index: 2;
}
/* The walker: a round token with an arrow that turns with its heading —
   and the map turns the other way, so on the iPad the arrow always points
   up the screen (v4.95.0). */
.mapz-walker {
  position: absolute;
  width: clamp(28px, 4.2vw, 54px);
  height: clamp(28px, 4.2vw, 54px);
  transform: translate(-50%, -50%) rotate(var(--mapz-heading, 0deg));
  transition: left 0.35s ease, top 0.35s ease, transform 0.45s cubic-bezier(0.4, 0, 0.2, 1);
  border-radius: 50%;
  background: var(--danger);
  border: 3px solid #fff;
  box-shadow: 0 3px 8px rgba(0, 0, 0, 0.35);
  display: grid;
  place-items: center;
  pointer-events: none;
  z-index: 3;
}
.mapz-walker-arrow {
  width: 0;
  height: 0;
  border-left: clamp(7px, 1vw, 12px) solid transparent;
  border-right: clamp(7px, 1vw, 12px) solid transparent;
  border-bottom: clamp(12px, 1.8vw, 22px) solid #fff;
  transform: translateY(-15%);
}
@media (prefers-reduced-motion: reduce) { .mapz-walker { transition-duration: 0.12s; } }

/* The side column: the tray, "I'm finished", the pad at the bottom. */
.mapz-side {
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto auto;
  gap: 10px;
  min-height: 0;
  background: var(--card);
  border-radius: var(--r-lg);
  padding: 12px;
  box-shadow: var(--shadow-sm);
}
.mapz-side-head { margin: 0 0 4px; font-family: var(--font-display); font-size: clamp(0.95rem, 2.1dvh, 1.1rem); line-height: 1.25; padding: 6px 8px; border-radius: var(--r-md); border: 2px solid var(--line); background: var(--card-raised); }
.mapz-side-head.is-turn { border-color: var(--go); background: #e6f4ea; }
.mapz-side-head.is-wait { border-color: var(--brand); background: var(--brand-tint); }
/* The logo across the top of the console (Adam, after v4.114.0). */
.mapz-side-logo { justify-self: center; height: clamp(44px, 9dvh, 88px); width: auto; max-width: 100%; }
/* The buildings three to a row, the last row centred — Adam: "three on the
   top and two on the bottom, with the two on the bottom centralised. It
   will just lay them out a little bit more attractively." A flex row that
   wraps and centres does exactly that for five (and 3-3-3-1 for ten). */
.mapz-tray { min-height: 0; overflow: auto; display: flex; flex-wrap: wrap; justify-content: center; align-content: flex-start; gap: 6px; }
.mapz-tray .mapz-side-head { flex: 1 1 100%; }
.mapz-tray-item {
  flex: 0 0 calc((100% - 12px) / 3);
  display: grid;
  justify-items: center;
  gap: 2px;
  padding: 6px 4px;
  border-radius: var(--r-md);
  border: 2px solid var(--line);
  background: var(--card-raised);
  touch-action: none;
  -webkit-user-select: none;
  user-select: none;
}
.mapz-tray-item.is-live { cursor: grab; }
.mapz-tray-item.is-dragging { opacity: 0.35; }
.mapz-tray-item img { width: 100%; max-width: 96px; height: auto; -webkit-user-drag: none; pointer-events: none; }
.mapz-tray-name { font-family: var(--font-display); font-weight: 800; font-size: clamp(0.72rem, 1.6dvh, 0.9rem); text-align: center; line-height: 1.1; }
.mapz-finished.is-invisible { visibility: hidden; }
/* The pad: left, forward and right across, turn around beneath — green
   arrows, no down. */
.mapz-pad { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px; }
.mapz-pad-btn {
  min-height: clamp(52px, 8dvh, 72px);
  border: 0;
  border-radius: var(--r-md);
  background: var(--go);
  color: #fff;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(1.4rem, 3.4dvh, 2rem);
  line-height: 1;
  cursor: pointer;
  box-shadow: 0 3px 0 rgba(0, 0, 0, 0.18);
  touch-action: manipulation;
}
.mapz-pad-btn:active { transform: translateY(2px); box-shadow: 0 1px 0 rgba(0, 0, 0, 0.18); }
.mapz-pad-btn:disabled { opacity: 0.45; cursor: default; }
.mapz-pad-btn.is-around, .mapz-pad-btn.is-start { grid-column: 1 / -1; background: var(--card-raised); color: var(--ink); border: 2px solid var(--line); box-shadow: none; font-size: clamp(0.95rem, 2.2dvh, 1.15rem); min-height: 44px; }
/* "Go back to the start" (v4.100.0): the map turns back to the start,
   the arrow back on it. */
.mapz-pad-btn.is-start { background: #fff; }
.mapz-pad.is-off { opacity: 0.6; }

/* The reveal. */
.mapz-result-sheet { width: min(1280px, 100%); display: grid; gap: 8px; }
.mapz-result-sheet h2 { margin: 0; }
.mapz-result-maps { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.mapz-result-col h3 { margin: 0 0 6px; font-size: 1rem; }
/* Two square maps side by side have to fit the height of an iPad in
   landscape: each is as wide as the sheet's height allows. */
.mapz-mini { position: relative; width: min(100%, calc(92dvh - 236px)); margin: 0 auto; aspect-ratio: 1; border-radius: var(--r-md); overflow: hidden; box-shadow: var(--shadow-sm); background: #b7d98c; }
.mapz-result-sheet { padding: clamp(14px, 2vw, 24px); }
.mapz-mini .mapz-building-mark { width: 20px; height: 20px; font-size: 12px; }
.mapz-mini .mapz-building-name { font-size: clamp(0.45rem, 0.85vw, 0.7rem); }
@media (max-width: 760px) {
  .mapz-body { flex-direction: column; }
  .mapz-side { flex: 0 0 auto; }
  .mapz-side { grid-template-rows: auto; grid-template-columns: minmax(0, 1fr) auto auto; align-items: center; }
  .mapz-tray { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .mapz-result-maps { grid-template-columns: 1fr; }
}

/* Mapz on the console (v4.90.0, v2 in v4.95.0). */
.pill.bad { background: #fbe9e7; color: #a12a20; }
.mapz-class-layout { display: grid; grid-template-columns: minmax(0, 3fr) minmax(280px, 2fr); gap: 16px; align-items: start; }
.mapz-class-layout > .card + .card { margin: 0; }
@media (max-width: 1000px) { .mapz-class-layout { grid-template-columns: 1fr; } }
.mapz-console-map { position: relative; width: 100%; aspect-ratio: 1; border-radius: var(--r-md); overflow: hidden; box-shadow: var(--shadow-sm); -webkit-user-select: none; user-select: none; background: #b7d98c; }
.mapz-console-map .mapz-building-name { font-size: clamp(0.5rem, 0.85vw, 0.8rem); }
/* The town face down until clicked (v4.103.0): the back covers the map,
   the logo on it and "Click here to reveal"; `.is-shown` lifts it. */
.mapz-town-flip { position: relative; }
.mapz-town-back {
  position: absolute; inset: 0; z-index: 2;
  display: grid; place-items: center; align-content: center; gap: 10px;
  border-radius: var(--r-md); border: 0; cursor: pointer;
  background: var(--brand) repeating-linear-gradient(45deg, rgba(255, 255, 255, 0.08) 0 14px, transparent 14px 28px);
  color: #fff; font: inherit; box-shadow: var(--shadow-sm);
}
.mapz-town-back-logo { width: min(45%, 260px); height: auto; filter: drop-shadow(0 4px 6px rgba(0, 0, 0, 0.25)); }
.mapz-town-back-eye { font-size: clamp(1.6rem, 4vw, 2.6rem); line-height: 1; }
.mapz-town-back-text { font-family: var(--font-display); font-weight: 800; font-size: clamp(1.1rem, 2.4vw, 1.6rem); }
.mapz-town-back:hover { filter: brightness(1.05); }
.mapz-town-flip.is-shown .mapz-town-back { display: none; }
/* The town in a window of its own (v4.103.0). */
body.is-town-window .wrap > #screen-mapz-class > .topline { display: none; }
body.is-town-window .mapz-class-layout { grid-template-columns: minmax(0, 1fr); }
body.is-town-window .mapz-town-card { padding: 8px; }

/* Which town (v4.114.0): three thumbnails, the one in play outlined. On the
   roster it is a card of its own; on the live screens a strip with the
   pictures small, so it costs the boards little. */
.mapz-map-pick { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; margin-top: 10px; max-width: 640px; }
.mapz-map-option {
  font: inherit; cursor: pointer; padding: 8px; display: grid; gap: 6px; justify-items: center;
  border: 3px solid var(--line); border-radius: var(--r-lg); background: var(--card); color: inherit;
}
.mapz-map-option img { width: 100%; aspect-ratio: 1; object-fit: cover; border-radius: 8px; display: block; }
.mapz-map-option span { font-weight: 700; font-size: 0.95rem; }
.mapz-map-option:hover { border-color: var(--brand); box-shadow: 0 0 0 4px var(--brand-tint); }
.mapz-map-option[aria-pressed='true'] { border-color: var(--go); box-shadow: 0 0 0 4px var(--brand-tint); }
.mapz-map-card.is-strip { display: flex; align-items: center; gap: 16px; padding: 10px 16px; margin: 0 0 16px; }
.mapz-map-card.is-strip .mapz-map-pick { margin: 0; gap: 10px; max-width: 600px; flex: 1; }
.mapz-map-card.is-strip .mapz-map-option { grid-template-columns: 56px minmax(0, 1fr); padding: 4px 8px 4px 4px; gap: 8px; justify-items: start; text-align: left; }
.mapz-map-card.is-strip .mapz-map-option img { width: 56px; }
.mapz-map-card.is-strip .mapz-map-option span { font-size: 0.85rem; line-height: 1.15; min-width: 0; overflow-wrap: anywhere; }
@media (max-width: 760px) { .mapz-map-card.is-strip { flex-direction: column; align-items: stretch; } .mapz-map-card.is-strip .mapz-map-pick { max-width: none; } }
.shout-progress-row.is-got .dash-seat { color: #0b6d5d; }
.shout-progress-row.is-missed .dash-seat { color: #a12a20; }
.mapz-class-score { font-family: var(--font-display); }
.mapz-card-seats { display: grid; gap: 6px; margin: 8px 0; }
/* Reset top right, the names straight under it (v4.100.0). The button is
   pinned to the card's corner so a result pill beside the pair's name can
   wrap without pushing it down. */
.mapz-card { position: relative; }
.mapz-card-head { align-items: flex-start; flex-wrap: wrap; padding-right: 84px; min-height: 36px; }
.mapz-card-head .btn.go.tiny { position: absolute; top: 10px; right: 10px; }
.mapz-card .shout-progress-list { margin-top: 4px; }
.mapz-card-status { margin: 0; }

/* ======================================================================== */
/* Printables (v4.91.0)                                                      */
/* ======================================================================== */
.printables-wrap { max-width: 72rem; margin: 0 auto; padding-top: 24px; }
.printables-logo img { width: min(50vw, 220px); }
/* The intro is one plain sentence, the width of the grid's card (the
   rebrand). The cartoon and the italic paragraph it used to carry are on
   /about now. */
.printables-bubble { max-width: 72rem; margin: 0 auto 18px; font-size: 1.05rem; line-height: 1.5; color: var(--ink); }
.printables-bubble p { margin: 0; }
.printables-card { max-width: 72rem; margin: 0 auto; }
.printables-grid .game-card { cursor: pointer; }
.printable-panel { max-width: 60rem; margin: 18px auto 0; }
.printable-panel-logo { height: 64px; width: auto; object-fit: contain; }
.printable-options { margin: 10px 0; }
.printable-actions { display: flex; align-items: center; gap: 12px; margin-top: 16px; }
.printable-select, .printable-input {
  font: inherit;
  padding: 10px 12px;
  border-radius: var(--r-md);
  border: 2px solid var(--line);
  background: var(--card-raised);
  color: var(--ink);
  min-height: 44px;
}
.printable-select { max-width: 100%; }
.printable-pictures { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 8px; max-height: 420px; overflow: auto; padding: 4px; }
.printable-pictures .picture-card { border: 2px solid var(--line); border-radius: var(--r-md); background: var(--card-raised); cursor: pointer; }
.park-print-scene { display: inline-flex; margin: 4px 8px 4px 0; }
.park-print-scene img { width: 96px; aspect-ratio: 1600 / 893; object-fit: cover; border-radius: var(--r-sm); }
/* The word picker: what is chosen, then the library. */
.word-picker { display: grid; gap: 10px; margin-top: 10px; }
.word-picker-chosen { display: flex; flex-wrap: wrap; gap: 6px; min-height: 40px; padding: 8px; border-radius: var(--r-md); background: var(--card-sunk); }
.word-chip { display: inline-flex; align-items: center; gap: 6px; padding: 4px 8px 4px 4px; border-radius: 999px; border: 2px solid var(--go); background: var(--card); font: inherit; font-weight: 700; cursor: pointer; }
.word-chip img { width: 28px; height: 28px; object-fit: cover; border-radius: 50%; }
.word-chip-x { color: var(--ink-faint); font-size: 0.8rem; }
.word-picker-bar { display: flex; gap: 8px; flex-wrap: wrap; }
.word-picker-bar .printable-input { flex: 1 1 200px; }
.word-picker-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(104px, 1fr)); gap: 6px; max-height: 300px; overflow: auto; padding: 4px; }
.word-card { display: grid; gap: 4px; justify-items: center; padding: 6px; border-radius: var(--r-md); border: 2px solid var(--line); background: var(--card-raised); font: inherit; cursor: pointer; color: var(--ink); }
.word-card img, .word-card-blank { width: 100%; aspect-ratio: 4 / 3; object-fit: contain; }
.word-card-word { font-size: 0.85rem; font-weight: 700; }
.word-card.is-on { border-color: var(--brand); background: var(--brand-tint); box-shadow: 0 0 0 3px var(--brand-tint); }
/* The button on the game picker, beside the email. */
.account-bubble .printables-link { font-weight: 700; color: var(--brand-dark); }

/* =========================================================================
   SPOT THE DIFFERENCE

   One picture — this child's own, A or B — and marks laid over it in
   percentages, the picture's box set in pixels by play.js to be exactly the
   picture (lib/spot.js keeps a mark as a fraction of it). Scoped to <main>:
   #screen-spot also exists on the teacher's page.
   ========================================================================= */
main#screen-spot {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-rows: auto minmax(0, 1fr) auto;
  gap: var(--game-gap);
  min-height: 0;
  touch-action: manipulation;
}
/* The logo top-left, the instruction beside it (v4.94.0). */
.spot-top {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  gap: 14px;
  padding-right: 96px; /* the connection pill's corner */
}
.spot-logo { height: clamp(48px, 9dvh, 84px); width: auto; display: block; }
main#screen-spot .spot-stage { grid-column: 1; grid-row: 2; }
main#screen-spot #btn-spot-finished { grid-column: 1 / -1; }
.spot-status-card {
  background: var(--card);
  border: 2px solid var(--line);
  border-radius: var(--r-lg);
  padding: 8px 16px;
  text-align: center;
  display: grid;
  gap: 2px;
  width: 100%;
  margin: 0;
  transition: background 0.2s, color 0.2s;
}
/* The thermometer, to the right of the picture: 0 to 8 up its side. */
.spot-meter {
  grid-column: 2;
  grid-row: 2;
  height: 100%;
  min-height: 0;
  width: clamp(120px, 15vw, 200px);
  display: grid;
  place-items: center;
}
.spot-meter-svg { height: 100%; width: 100%; overflow: visible; }
.spot-meter-tube { fill: #fff; stroke: var(--ink); stroke-width: 5; }
.spot-meter-bulb { fill: var(--stop); stroke: var(--ink); stroke-width: 5; }
.spot-meter-bulb-inner { fill: var(--stop); }
.spot-meter-fill {
  fill: var(--stop);
  transform-box: view-box;
  transform-origin: 60px 368px;
  transform: scaleY(0.11);
  transition: transform 0.35s cubic-bezier(0.2, 0.9, 0.3, 1.2);
}
.spot-meter.is-full .spot-meter-fill, .spot-meter.is-full .spot-meter-bulb, .spot-meter.is-full .spot-meter-bulb-inner { fill: var(--go); }
.spot-meter-tick { stroke: var(--ink); stroke-width: 4; stroke-linecap: round; }
.spot-meter-num { font-family: var(--font-display); font-weight: 800; font-size: 30px; fill: var(--ink-soft); }
.spot-meter-num.is-now { fill: var(--stop); font-size: 40px; }
.spot-meter.is-full .spot-meter-num.is-now { fill: var(--go); }
@media (max-width: 640px) {
  main#screen-spot { grid-template-columns: minmax(0, 1fr); }
  .spot-meter { display: none; }
  main#screen-spot .spot-stage { grid-column: 1; }
}
.spot-status-card .draw-status { margin: 0; height: auto; font-size: clamp(1rem, 2.4dvh, 1.4rem); }
.spot-status-sub { margin: 0; font-size: clamp(0.8rem, 1.8dvh, 1rem); color: var(--ink-soft); }
.spot-status-card.is-waiting { border-color: var(--go); background: #e6f4ea; }
/* Eight marks used and a ninth tap: a red flash says why nothing happened. */
.spot-status-card.is-full { border-color: var(--stop); background: #fbe3e3; }

.spot-stage {
  min-height: 0;
  min-width: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--card-sunk);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-sm);
  padding: clamp(4px, 1vw, 10px);
}
.spot-picture {
  --spot-mark: clamp(30px, 4.8vw, 50px);
  position: relative;
  aspect-ratio: 2752 / 1536;
  max-width: 100%;
  max-height: 100%;
  border-radius: var(--r-md);
  overflow: hidden;
  box-shadow: var(--shadow-sm);
  cursor: crosshair;
  touch-action: manipulation;
}
.spot-picture.is-locked { cursor: default; }
.spot-picture.is-mini { --spot-mark: clamp(14px, 2vw, 20px); width: var(--fit-w, 100%); cursor: default; }
.spot-picture-img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
  -webkit-user-select: none;
  user-select: none;
  -webkit-user-drag: none;
  -webkit-touch-callout: none;
}
.spot-layer { position: absolute; inset: 0; }

/* A mark: a numbered ring, big enough for a finger, centred on the tap. */
.spot-mark {
  position: absolute;
  width: var(--spot-mark);
  height: var(--spot-mark);
  transform: translate(-50%, -50%);
  border-radius: 50%;
  border: 4px solid var(--stop);
  background: rgba(255, 255, 255, 0.55);
  box-shadow: 0 0 0 2px #fff, 0 2px 8px rgba(0, 0, 0, 0.35);
  display: grid;
  place-items: center;
  padding: 0;
  font: inherit;
  cursor: pointer;
  animation: spot-mark-in 0.18s ease-out;
}
@keyframes spot-mark-in { from { transform: translate(-50%, -50%) scale(0.4); } to { transform: translate(-50%, -50%) scale(1); } }
.spot-mark-number {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: calc(var(--spot-mark) * 0.5);
  line-height: 1;
  color: var(--stop);
}
.spot-mark.is-small { border-width: 3px; animation: none; }
.spot-mark.is-hit { border-color: var(--go); }
.spot-mark.is-hit .spot-mark-number { color: var(--go); }
.spot-mark.is-miss { border-color: var(--stop); border-style: dashed; }

/* A child's own marks on the reveal are dots, not numbers — the numbers
   belong to the rings now (v4.94.0). */
.spot-mark.is-dot { --spot-mark: clamp(12px, 1.4vw, 16px); border-width: 3px; background: rgba(255, 255, 255, 0.7); }
.spot-mark.is-dot .spot-mark-number { display: none; }

/* The reveal's rings, one per difference spot: green both, amber one, grey none. */
.spot-ring {
  position: absolute;
  aspect-ratio: 1 / 1;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  border: 4px solid #9a9a9a;
  box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.8);
  pointer-events: none;
}
/* Tappable on the child's reveal: a tap says what the difference was. */
.spot-ring.is-tappable { pointer-events: auto; cursor: pointer; padding: 0; background: transparent; font: inherit; }
.spot-ring.is-tappable .spot-ring-n { background: #9a9a9a; font-family: var(--font-display); font-weight: 800; font-size: clamp(0.8rem, 1.6vw, 1.1rem); padding: 3px 8px; pointer-events: none; }
.spot-ring.is-tappable.is-both .spot-ring-n { background: var(--go); }
.spot-ring.is-tappable.is-one .spot-ring-n { background: #e0a020; }
.spot-popover {
  position: absolute;
  z-index: 3;
  transform: translate(-50%, calc(-100% - 8px));
  min-width: 180px;
  max-width: min(320px, 80%);
  background: var(--card);
  border: 3px solid var(--line);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-md);
  padding: 8px 12px;
  display: grid;
  gap: 4px;
  text-align: left;
  font-size: clamp(0.85rem, 1.6vw, 1.05rem);
  line-height: 1.25;
  animation: spot-mark-in 0.16s ease-out;
}
.spot-popover.is-below { transform: translate(-50%, 8px); }
.spot-popover.is-both { border-color: var(--go); }
.spot-popover.is-one { border-color: #e0a020; }
.spot-popover-row { display: flex; gap: 8px; align-items: baseline; }
.spot-popover-n {
  flex: none;
  min-width: 1.6em;
  text-align: center;
  border-radius: 999px;
  background: var(--stop);
  color: #fff;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 0.85em;
  padding: 1px 6px;
}
.spot-popover-head { display: flex; align-items: center; gap: 8px; padding-bottom: 4px; border-bottom: 1px solid var(--line); }
.spot-popover-side {
  flex: none;
  min-width: 1.6em;
  text-align: center;
  border-radius: 6px;
  background: var(--card-sunk);
  color: var(--ink-soft);
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 0.8em;
  padding: 1px 5px;
}
.spot-popover-found { color: var(--ink-soft); font-size: 0.85em; }
.spot-popover.is-both .spot-popover-found { color: var(--go); font-weight: 700; }
.spot-ring.is-both { border-color: var(--go); box-shadow: 0 0 0 2px #fff, 0 0 14px rgba(46, 158, 82, 0.6); }
.spot-ring.is-one { border-color: #e0a020; box-shadow: 0 0 0 2px #fff; }
.spot-ring.is-none { border-color: #9a9a9a; border-style: dashed; }

/* ---- the result ------------------------------------------------------- */
/* The pictures are the point (v4.94.0, Adam: "make the two pictures much
   bigger"): the sheet takes the whole width, its padding is small, and the
   pictures get every pixel of height the chrome leaves. */
#result-spot.overlay { padding: 8px; }
.spot-result-sheet {
  max-width: none;
  width: 100%;
  padding: 12px 14px;
  overflow: hidden;
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto auto;
  gap: clamp(4px, 1dvh, 8px);
  max-height: 96dvh;
}
.spot-result-pictures { gap: 10px; }
.spot-result-sheet #btn-spot-again { min-height: 0; padding: 10px 16px; }
.spot-mark.is-dot { pointer-events: none; }
.spot-result-sheet h2 { margin: 0; }
.spot-result-sheet .score-big { margin: 0 !important; font-size: clamp(1.8rem, 5dvh, 3rem); line-height: 1; }
/* The heading and the score on one line, so the pictures get the height. */
.spot-result-head { display: flex; align-items: center; justify-content: center; gap: 24px; flex-wrap: wrap; }
.spot-picture.is-mini { position: relative; overflow: visible; }
.spot-picture.is-mini .spot-picture-img { border-radius: var(--r-md); }
.spot-result-pictures {
  min-height: 0;
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  gap: 14px;
  align-items: start;
  justify-items: center;
}
.spot-result-picture { min-height: 0; display: grid; grid-template-rows: auto auto; justify-items: center; width: 100%; }
.spot-result-picture h3 { margin: 0 0 4px; font-size: 1rem; }
.spot-over-list { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 3px 10px; }
@media (max-width: 640px) { .spot-over-list { grid-template-columns: minmax(0, 1fr); } }
/* .spot-over-list is unused on the child's reveal since v4.94.0 (the list
   went; the rings say it on a tap), kept for the console's key. */
.spot-over-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 2px 8px;
  border-radius: var(--r-sm);
  font-size: clamp(0.78rem, 1.7dvh, 0.95rem);
  min-width: 0;
}
.spot-over-n { flex: none; width: 1.4em; font-weight: 800; color: var(--ink-soft); }
.spot-over-text { flex: 1; min-width: 0; line-height: 1.2; }
.spot-over-row .mark { flex: none; font-weight: 800; color: #9a9a9a; }
.spot-over-row.is-both { background: var(--accent-tint); }
.spot-over-row.is-both .mark { color: var(--go); }
.spot-over-row.is-one .mark { color: #e0a020; }
.spot-over-row.is-none .mark { color: var(--stop); }

/* ---- Spot the Difference, on the teacher's side ------------------------ */
.spot-scene-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 12px;
  margin-top: 16px;
}
.spot-scene-card img { aspect-ratio: 2752 / 1536; }
.spot-picture.is-console { width: 100%; cursor: default; --spot-mark: 28px; }
/* The key's rings: numbered, so the list under the picture reads across. */
.spot-ring.is-key { border-color: var(--stop); border-style: solid; box-shadow: 0 0 0 2px #fff; }
.spot-ring-n {
  position: absolute;
  left: 50%;
  top: 0;
  transform: translate(-50%, -60%);
  background: var(--stop);
  color: #fff;
  font-size: 0.8rem;
  line-height: 1;
  padding: 2px 6px;
  border-radius: 999px;
  border: 2px solid #fff;
}
.spot-key { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 2px 14px; margin-top: 10px; }
.spot-key-row { font-size: 0.9rem; line-height: 1.3; padding-left: 1.8em; text-indent: -1.8em; }
.spot-key-row b { display: inline-block; width: 1.8em; text-indent: 0; }
.spot-class-layout {
  display: grid;
  grid-template-columns: minmax(0, 3fr) minmax(280px, 2fr);
  gap: 18px;
  align-items: start;
}
@media (max-width: 900px) { .spot-class-layout { grid-template-columns: minmax(0, 1fr); } }
.spot-class-picture-card .card-head { margin-bottom: 8px; }
.spot-class-board { display: grid; gap: 6px; }
/* Two lines, not one (v4.118.4 — Adam: the bars were "all over the place,
   protruding outside the designated card"): the class card is the narrow
   column of the layout, and rank + dot + name + cross + bar + "0 of 8
   marked" + best did not fit across it, so the row's tracks ran past the
   card. The bar takes the whole second line. */
.spot-class-row { cursor: default; grid-template-columns: 28px 12px minmax(0, 1fr) auto auto auto; grid-template-rows: auto auto; row-gap: 6px; }
.spot-class-row .park-class-progress { grid-column: 1 / -1; grid-row: 2; }
.spot-class-row .park-class-name { min-width: 0; }
.spot-class-row:hover { border-color: var(--line); }
.spot-class-row .park-class-points { min-width: 0; }
.spot-class-row .park-class-best:empty { display: none; }
.spot-key-card { margin-bottom: 16px; }
.spot-key-pictures { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
@media (max-width: 720px) { .spot-key-pictures { grid-template-columns: minmax(0, 1fr); } }
#spot-dash { grid-template-columns: repeat(auto-fit, minmax(min(100%, 420px), 1fr)); }
.spot-side-pill { margin-left: 4px; }

/* =========================================================================
   BOARD GAME GENERATOR

   A winding grid of square tiles (js/boardgame-board.js sizes them in
   pixels: --bg-tile), counters laid over it, and the Treasure Hunt die
   beside it. Four colour themes on the host's data-theme.
   ========================================================================= */
.bg-host {
  --bg-paper: #dff0d0;
  --bg-tile-bg: #fffdf7;
  --bg-line: #2b3a2e;
  --bg-special: #fff1c9;
  --bg-start: #bfe8c6;
  --bg-finish: #ffd0d0;
  --bg-road: #f0dfae;
  --bg-road-edge: #c9a86a;
  --bg-road-dash: #ffffff;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 14px;
  min-height: 0;
  min-width: 0;
}
/* The four colours (v4.111.0): the ground, the road, its edge and its
   dashes — a sandy path on grass, a jetty over the sea, a light-trail in
   space, a liquorice lace on candy. */
.bg-host[data-theme='sea'] { --bg-paper: #cfe9f7; --bg-line: #1f3b57; --bg-road: #f6e7c3; --bg-road-edge: #b98b52; --bg-road-dash: #ffffff; }
.bg-host[data-theme='space'] { --bg-paper: #1f2340; --bg-line: #9aa7ff; --bg-tile-bg: #fbfbff; --bg-road: #4a4f8c; --bg-road-edge: #8f96e6; --bg-road-dash: #ffe27a; }
.bg-host[data-theme='candy'] { --bg-paper: #fbe0ec; --bg-line: #6b2d4a; --bg-road: #ffffff; --bg-road-edge: #e28bb2; --bg-road-dash: #ff7fb0; }
.bg-host.is-preview { grid-template-columns: minmax(0, 1fr); }
.bg-stage {
  min-height: 0;
  min-width: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: clamp(8px, 1.5vw, 18px);
  background: var(--bg-paper);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-sm);
  position: relative;
}
.bg-grid {
  position: relative;
  flex: none;
}
.bg-road { position: absolute; inset: 0; z-index: 0; display: block; }
.bg-road-edge { stroke: var(--bg-road-edge); }
.bg-road-fill { stroke: var(--bg-road); }
.bg-road-dash { stroke: var(--bg-road-dash); opacity: 0.9; }
.bg-tokens { position: absolute; inset: 0; pointer-events: none; z-index: 2; }
/* A square is a rounded stepping stone on the road (v4.111.0): placed by
   the module (left/top), a soft shadow, a thick outline in the theme's
   ink, a number badge in the corner. */
.bg-tile {
  position: absolute;
  z-index: 1;
  width: var(--bg-tile, 60px);
  height: var(--bg-tile, 60px);
  border: max(2px, calc(var(--bg-tile, 60px) * 0.04)) solid var(--bg-line);
  border-radius: calc(var(--bg-tile, 60px) * 0.22);
  background: var(--bg-tile-bg);
  box-shadow: 0 calc(var(--bg-tile, 60px) * 0.05) 0 rgba(0, 0, 0, 0.18), 0 calc(var(--bg-tile, 60px) * 0.08) calc(var(--bg-tile, 60px) * 0.16) rgba(0, 0, 0, 0.12);
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
  justify-items: center;
  align-items: center;
  overflow: hidden;
  padding: calc(var(--bg-tile, 60px) * 0.07);
  box-sizing: border-box;
}
.bg-tile.is-start { background: var(--bg-start); border-radius: 50%; }
.bg-tile.is-finish { background: var(--bg-finish); border-radius: 50%; }
.bg-tile.is-start .bg-tile-word, .bg-tile.is-finish .bg-tile-word { font-size: clamp(9px, calc(var(--bg-tile, 60px) * 0.17), 22px); }
.bg-tile.is-miss, .bg-tile.is-back3, .bg-tile.is-forward3, .bg-tile.is-mystery { background: var(--bg-special); }
.bg-tile-pic { max-width: 100%; max-height: 100%; object-fit: contain; min-height: 0; }
.bg-tile-nopic { width: 60%; height: 60%; border-radius: 50%; background: var(--card-sunk); }
.bg-tile-word {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(8px, calc(var(--bg-tile, 60px) * 0.14), 18px);
  line-height: 1.1;
  text-align: center;
  color: var(--bg-line);
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.bg-tile-icon { font-size: calc(var(--bg-tile, 60px) * 0.36); line-height: 1; }
.bg-tile.is-mystery .bg-tile-icon { font-family: var(--font-display); font-weight: 900; color: #8e44ad; }
.bg-tile-n {
  position: absolute;
  left: 3%;
  top: 3%;
  min-width: calc(var(--bg-tile, 60px) * 0.22);
  height: calc(var(--bg-tile, 60px) * 0.22);
  padding: 0 calc(var(--bg-tile, 60px) * 0.04);
  border-radius: 999px;
  background: var(--bg-line);
  color: #fff;
  font-size: clamp(7px, calc(var(--bg-tile, 60px) * 0.12), 13px);
  line-height: calc(var(--bg-tile, 60px) * 0.22);
  text-align: center;
  font-weight: 800;
}
.bg-tile.is-start .bg-tile-n, .bg-tile.is-finish .bg-tile-n { display: none; }
.bg-tile.is-miss .bg-tile-word, .bg-tile.is-back3 .bg-tile-word, .bg-tile.is-forward3 .bg-tile-word, .bg-tile.is-mystery .bg-tile-word { white-space: normal; line-height: 1.05; }
.bg-token {
  position: absolute;
  width: calc(var(--bg-tile, 60px) * 0.42);
  height: calc(var(--bg-tile, 60px) * 0.42);
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background: var(--token, #d64545);
  border: 3px solid #fff;
  box-shadow: 0 3px 8px rgba(0, 0, 0, 0.35);
  display: grid;
  place-items: center;
  color: #fff;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: calc(var(--bg-tile, 60px) * 0.16);
  transition: left 0.25s ease, top 0.25s ease;
  pointer-events: none;
}
.bg-token.is-turn { box-shadow: 0 0 0 3px #fff, 0 0 0 6px var(--token), 0 3px 8px rgba(0, 0, 0, 0.35); }
.bg-token.is-hopping { transform: translate(-50%, -70%) scale(1.15); z-index: 2; }
.bg-side {
  width: clamp(180px, 22vw, 280px);
  display: grid;
  align-content: start;
  gap: 10px;
  --board: 600px;
}
.bg-side .talks-die { --die: clamp(48px, 5vw, 72px); justify-self: center; }
.bg-seats { display: grid; gap: 4px; }
.bg-seat {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 10px;
  border-radius: var(--r-md);
  border: 2px solid transparent;
  background: var(--card);
}
.bg-seat.is-turn { border-color: var(--go); background: #e6f4ea; }
.bg-seat.is-winner { border-color: #e0a020; background: #fff3d1; }
.bg-seat-dot { width: 16px; height: 16px; border-radius: 50%; background: var(--token); border: 2px solid #fff; box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.2); flex: none; }
.bg-seat-name { font-weight: 700; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bg-status {
  min-height: 2.6em;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(0.95rem, 1.6vw, 1.2rem);
  line-height: 1.25;
  text-align: center;
  padding: 8px 10px;
  border-radius: var(--r-md);
  background: var(--card);
  border: 2px solid var(--line);
}
.bg-status.is-over { background: #fff3d1; border-color: #e0a020; }
.bg-roll.is-pressed, .bg-next.is-pressed, .bg-again.is-pressed { opacity: 0.55; }
/* The one button (v4.111.0): the next player's name over "Click here to
   roll". */
.bg-roll { display: grid; justify-items: center; gap: 2px; line-height: 1.15; }
.bg-roll-name { font-size: 1.25em; }
.bg-roll-text { font-size: 0.85em; font-weight: 600; opacity: 0.9; }
.bg-seg { flex-wrap: wrap; }
.bg-sets { grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); }

/* ---- the generator (console) ------------------------------------------- */
.bg-layouts { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; }
.bg-layout-card {
  display: grid;
  gap: 6px;
  padding: 10px;
  border: 3px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--card-raised);
  font: inherit;
  color: var(--ink);
  text-align: left;
  cursor: pointer;
}
.bg-layout-card.is-chosen, .bg-layout-card:hover { border-color: var(--brand); box-shadow: 0 0 0 4px var(--brand-tint); }
.bg-layout-mini { display: grid; gap: 2px; width: 100%; aspect-ratio: 7 / 5; }
.bg-layout-mini i { display: block; background: var(--line); border-radius: 2px; opacity: 0.55; }
.bg-layout-mini i.is-end { background: var(--go); opacity: 1; }
.bg-specials { display: flex; flex-wrap: wrap; gap: 8px 18px; }
.bg-themes { display: flex; gap: 8px; flex-wrap: wrap; }
.bg-theme {
  width: 34px;
  height: 34px;
  border-radius: 50%;
  border: 3px solid var(--line);
  cursor: pointer;
  padding: 0;
}
.bg-theme.is-chosen { box-shadow: 0 0 0 4px var(--brand-tint); border-color: var(--brand); }
.bg-preview { min-height: 320px; height: min(62dvh, 640px); }
.bg-saved { display: flex; flex-wrap: wrap; gap: 8px; }
.bg-saved-chip { display: inline-flex; align-items: center; gap: 6px; }
.bg-class-names { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 8px; }
.bg-live { height: min(78dvh, 820px); min-height: 420px; }
#bg-dash { grid-template-columns: repeat(auto-fit, minmax(min(100%, 380px), 1fr)); }

/* ---- the iPad ----------------------------------------------------------- */
main#screen-boardgame {
  position: relative;
  display: grid;
  grid-template-rows: minmax(0, 1fr);
  min-height: 0;
}
main#screen-boardgame .bg-host { min-height: 0; height: 100%; }
main#screen-boardgame .bg-side { width: clamp(170px, 24vw, 260px); }
/* The connection pill sits in the top-right corner, which on this screen is
   the first seat's row — so the side panel starts below it. */
main#screen-boardgame .bg-side { padding-top: 34px; }

/* =========================================================================
   MY CLASSES AND HOMEWORK (v4.96.0)
   ========================================================================= */
/* The two buttons top-right of the picker (Printables, My Classes). */
.account-bubble .printables-link { padding: 4px 10px; border: 1px solid var(--line); border-radius: 999px; background: var(--card); text-decoration: none; }
.account-bubble .printables-link:hover { border-color: var(--brand); }
.classes-layout { display: grid; grid-template-columns: minmax(280px, 2fr) minmax(320px, 3fr); gap: 16px; align-items: start; }
.classes-layout > .card + .card { margin: 0; }
@media (max-width: 900px) { .classes-layout { grid-template-columns: 1fr; } }
.classes-add { gap: 8px; margin: 6px 0 12px; }
.classes-add input { flex: 1 1 0px; width: 0; min-width: 0; }
.classes-list { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 14px; }
/* A class is a chip (Soft pastel): white and lifted, ink when chosen. */
.class-chip {
  display: grid;
  gap: 0;
  padding: 7px 16px;
  border-radius: 999px;
  border: 0;
  background: var(--card);
  box-shadow: var(--shadow-sm);
  text-align: left;
  cursor: pointer;
  color: var(--ink);
  font: inherit;
  font-weight: 600;
}
.class-chip.is-on { background: var(--ink); color: #fff; }
.class-delete-row { display: flex; justify-content: flex-end; margin-top: 14px; }
.class-detail { border-top: 1px solid var(--line); padding-top: 12px; }
.class-detail .card-head { margin-bottom: 6px; }
.students-list { display: grid; gap: 4px; }
.student-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto;
  align-items: center;
  gap: 10px;
  padding: 8px 12px;
  border-radius: var(--r-md);
  background: var(--card);
  border: 2px solid var(--line);
}
.student-row .nm { font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* A home code on a peach chip, in a monospace so the letters are told apart. */
.student-code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-weight: 700; letter-spacing: 0.15em; color: var(--ink); background: var(--peach); border: 0; border-radius: 10px; padding: 4px 10px; }
.student-x { position: static; }
.tasks-list { display: grid; gap: 12px; margin-top: 12px; }
.task-card { border: 2px solid var(--line); border-radius: var(--r-md); padding: 10px 12px; background: var(--card); }
.task-card.is-off { opacity: 0.7; }
.task-card > .row { gap: 10px; flex-wrap: wrap; }
.task-rows { display: grid; gap: 2px; margin-top: 8px; }
.task-row { display: grid; grid-template-columns: minmax(0, 1fr) 70px 70px 110px; gap: 8px; padding: 3px 8px; border-radius: 6px; align-items: center; }
.task-row.is-head { color: var(--ink-soft); font-size: 0.8rem; border-bottom: 1px solid var(--line); }
.task-row.is-played { background: var(--accent-tint); }
.task-row .nm { font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#task-set { width: 100%; max-width: 100%; margin: 4px 0 4px; }
/* The student's homework page. */
.homework-wrap { max-width: 720px; margin: 0 auto; padding-top: 24px; }
.homework-wrap:has(> #hw-login:not(.hidden)) { display: grid; align-content: center; }
.homework-login { text-align: center; padding: 28px 24px; }
.homework-title { margin: 0 0 4px; font-family: var(--font-display); font-size: clamp(1.6rem, 5vw, 2.4rem); }
.homework-form { display: grid; gap: 12px; margin-top: 14px; }
.homework-form input { text-align: center; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 1.9rem; font-weight: 700; letter-spacing: 0.3em; text-indent: 0.3em; text-transform: uppercase; height: 72px; background: var(--paper); border-radius: 18px; }
.homework-form input::placeholder { text-transform: none; letter-spacing: 0.05em; text-indent: 0; font-family: var(--font); font-weight: 400; font-size: 1.1rem; color: var(--ink-faint); }
.homework-form .btn { width: 100%; }
.homework-wordmark { display: block; width: fit-content; margin: 4px auto 14px; font-size: 2.2rem; }
.homework-topline { margin-bottom: 14px; align-items: flex-start; }
.homework-topline p { margin: 0; }
.homework-list { display: grid; gap: 12px; }
.homework-task {
  display: grid;
  grid-template-columns: 96px minmax(0, 1fr) auto;
  gap: 14px;
  align-items: center;
  padding: 14px;
  background: var(--card);
  border: 2px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-sm);
}
.homework-task-logo { width: 96px; height: auto; }
.homework-task-text { display: grid; gap: 4px; min-width: 0; }
.homework-task-text b { font-family: var(--font-display); font-size: 1.15rem; }
.homework-task-played { color: var(--go); font-weight: 700; }
.homework-play { min-width: 120px; }
@media (max-width: 560px) { .homework-task { grid-template-columns: 64px minmax(0, 1fr); } .homework-task-logo { width: 64px; } .homework-play { grid-column: 1 / -1; } }
/* On the play page during homework: a way back to the list. */
.hw-back { position: absolute; bottom: max(10px, env(safe-area-inset-bottom)); left: max(12px, env(safe-area-inset-left)); z-index: 5; background: rgba(255, 252, 244, 0.9); }
body.is-homework #btn-leave-pairs { display: none; }

/* ---- Learning Journals (v4.97.0): the student's page ------------------ */
.journal-card { margin-bottom: 18px; }
.journal-heading { margin: 0; font-family: var(--font-display); }
.homework-heading { margin: 22px 0 10px; font-family: var(--font-display); }
.journal-prompt { margin: 0 0 12px; font-weight: 600; }
.journal-write { display: grid; gap: 14px; }
.journal-step h3 { margin: 0 0 6px; font-size: 1rem; display: flex; align-items: center; gap: 8px; }
.journal-n { display: inline-grid; place-items: center; width: 26px; height: 26px; border-radius: 50%; background: var(--brand); color: #fff; font-family: var(--font-display); font-weight: 800; font-size: 0.9rem; }
.journal-subjects { display: flex; flex-wrap: wrap; gap: 8px; }
.journal-subject { padding: 8px 14px; border-radius: 999px; border: 2px solid var(--line); background: var(--card-raised); font: inherit; font-weight: 700; cursor: pointer; color: var(--ink); }
.journal-subject.is-on { border-color: var(--brand); background: var(--brand-tint); box-shadow: 0 0 0 3px var(--brand-tint); }
.journal-words { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; min-height: 8px; }
.journal-word { display: inline-flex; align-items: center; gap: 6px; padding: 4px 6px 4px 12px; border-radius: 999px; background: #fff3c4; border: 1px solid #e0c060; font-weight: 700; font-size: 0.95rem; }
.journal-word.is-small { padding: 2px 10px; font-size: 0.8rem; }
.journal-word-x { border: 0; background: transparent; font-size: 1rem; line-height: 1; cursor: pointer; color: var(--ink-soft); padding: 0 4px; }
.journal-help { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 8px; }
.journal-help-btn.is-on { background: var(--brand); color: #fff; border-color: var(--brand); }
.journal-panel { border: 2px solid var(--line); border-radius: var(--r-md); padding: 10px 12px; margin-bottom: 8px; background: var(--card-raised); display: grid; gap: 8px; }
.journal-panel > p { margin: 0; }
.journal-starter-group b { display: block; margin-bottom: 4px; color: var(--ink-soft); }
.journal-starter-lines { display: flex; flex-wrap: wrap; gap: 6px; }
.journal-starter { text-align: left; padding: 6px 10px; border-radius: 8px; border: 1px solid var(--line); background: #fff; font: inherit; font-size: 0.9rem; cursor: pointer; color: var(--ink); }
.journal-starter:hover { border-color: var(--brand); }
.journal-builder-tabs { display: flex; flex-wrap: wrap; gap: 6px; }
.journal-builder { display: grid; gap: 8px; }
.journal-builder > p { margin: 0; color: var(--ink-soft); }
.journal-builder-columns { display: flex; gap: 8px; overflow-x: auto; padding-bottom: 4px; }
.journal-builder-col { display: grid; gap: 4px; align-content: start; min-width: 120px; flex: 1 0 120px; }
.journal-builder-colname { font-size: 0.72rem; color: var(--ink-soft); text-transform: uppercase; letter-spacing: 0.04em; }
.journal-cell { text-align: left; padding: 6px 8px; border-radius: 8px; border: 1px solid var(--line); background: #fff; font: inherit; font-size: 0.85rem; cursor: pointer; color: var(--ink); line-height: 1.2; }
.journal-cell.is-on { border-color: var(--brand); background: var(--brand-tint); font-weight: 700; }
.journal-cell.is-gap { border-style: dashed; color: var(--ink-soft); font-style: italic; }
.journal-builder-out { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; padding: 8px 10px; background: #fff; border-radius: 8px; border: 1px solid var(--line); }
.journal-builder-sentence { flex: 1 1 200px; font-weight: 700; }
.journal-check { display: flex; gap: 10px; align-items: flex-start; padding: 6px 8px; border-radius: 8px; cursor: pointer; }
.journal-check.is-on { background: var(--accent-tint); }
.journal-check input { margin-top: 3px; }
#journal-text { width: 100%; font-size: 1.05rem; line-height: 1.5; padding: 12px; border: 2px solid var(--line); border-radius: var(--r-md); resize: vertical; font-family: inherit; }
.journal-count-row { margin-top: 8px; gap: 10px; }
#journal-count.is-enough { color: var(--go); font-weight: 700; }
.journal-entries { margin-top: 18px; display: grid; gap: 10px; }
.journal-entries-head { margin: 0; font-size: 1.05rem; }
.journal-entry { border: 2px solid var(--line); border-radius: var(--r-md); padding: 10px 12px; background: var(--card-raised); display: grid; gap: 6px; }
.journal-entry.has-feedback { border-color: var(--go); }
.journal-entry-head { gap: 10px; }
.journal-entry-words { display: flex; flex-wrap: wrap; gap: 4px; }
.journal-entry-text { margin: 0; white-space: pre-wrap; line-height: 1.5; }
.journal-stars { color: #e0a020; font-size: 1.1rem; letter-spacing: 0.05em; }
.journal-feedback { background: #e6f4ea; border-radius: 8px; padding: 8px 10px; }
body.is-journal .homework-heading, body.is-journal .homework-list { display: none; }
/* The teacher's journal feed. */
#journal-card { margin-top: 16px; }
.journal-subject-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.journal-feed { display: grid; gap: 10px; }
.journal-feed-entry { border: 2px solid var(--line); border-radius: var(--r-md); padding: 10px 12px; background: var(--card-raised); display: grid; gap: 6px; }
.journal-feed-entry.is-new { border-color: var(--brand); }
.journal-feed-entry.has-feedback { border-color: var(--go); }
.journal-feedback-form { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.journal-feedback-form input { flex: 1 1 240px; width: 0; min-width: 0; }
.journal-star-row { display: inline-flex; gap: 2px; }
.journal-star { border: 0; background: transparent; font-size: 1.5rem; line-height: 1; color: #cfcfcf; cursor: pointer; padding: 0 2px; }
.journal-star.is-on { color: #e0a020; }
#journal-word { flex: 1 1 0px; width: 0; min-width: 0; }
/* A gap under the page's top bar (Adam: "the classes and homework cards
   are currently bumping into the My Classes cards"). */
.wrap > #screen-classes > .topline:first-child { margin-bottom: 16px; }
/* The faint copyright line at the foot of the front door (v4.98.0). */

/* =========================================================================
   AT THE MARKET (v4.98.0; Adam's redesign v4.99.0)
   A shopper: a shopping list torn off a notepad, and a wallet. A seller: a
   market stall with the goods on its counter, and a till. Deals happen in
   a pop-up. Scoped to <main>: #screen-market also exists on the teacher's
   page.
   ========================================================================= */
main#screen-market {
  position: relative;
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  gap: var(--game-gap);
  min-height: 0;
}
/* The holding page (v4.102.0): a green circle, nothing else. */
.market-hold { grid-row: 1 / -1; display: grid; place-items: center; min-height: 0; }
.market-hold-circle {
  aspect-ratio: 1;
  width: clamp(240px, 52dvh, 480px);
  border-radius: 50%;
  background: var(--sq-green, #2e9e52);
  color: #fff;
  display: grid;
  place-items: center;
  text-align: center;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(1.3rem, 3.6dvh, 2.3rem);
  line-height: 1.2;
  padding: 1.2em;
  box-shadow: var(--shadow-lg);
}
/* The bar: the logo twice the size it was (v4.102.0) and one line of
   words, "Tap on something to buy it." */
.market-top { display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: center; gap: 18px; padding-right: 96px; }
.market-top-right { display: grid; gap: 8px; min-width: 0; }
/* A little shorter since v4.110.0 (was 18dvh) — the stall gets the height. */
.market-logo { height: clamp(96px, 14dvh, 168px); width: auto; display: block; }
.market-role-card {
  background: var(--card);
  border: 2px solid var(--line);
  border-radius: var(--r-lg);
  padding: 14px 20px;
  display: grid;
  align-items: center;
}
.market-role-card b { font-family: var(--font-display); font-size: clamp(1.15rem, 3dvh, 1.7rem); }
.market-short { background: #fff3c4; border: 2px solid #e0c060; border-radius: var(--r-md); padding: 6px 14px; font-size: 0.95rem; }
/* The list and the wallet (or the stall and the till) side by side in the
   middle with no dead space between them (v4.102.0). */
.market-body { display: flex; justify-content: center; align-items: stretch; gap: 24px; min-height: 0; }

/* ---- the shopping list ------------------------------------------------- */
/* Adam: "a piece of paper which has been ripped off a notepad with lines
   and text written in Bradley Hand Script font… At the top, there is a big
   underlined heading reading 'Shopping List'." Bradley Hand is on every
   iPad and Mac; the fallbacks are the nearest hand-written faces. Every
   row is exactly one ruled line tall (--rule) and the rules are painted
   from the content box, so the writing sits on the lines. */
.market-notepad { min-height: 0; max-height: 100%; overflow: auto; display: grid; justify-items: center; align-items: start; padding: 6px 4px 12px; flex: 0 1 760px; }
.market-paper {
  --rule: clamp(40px, 6.4dvh, 54px);
  --paper-ink: #1d3f7a;
  position: relative;
  width: 100%;
  box-sizing: border-box;
  padding: 44px 26px 30px 70px;
  font-family: 'Bradley Hand', 'Bradley Hand ITC', 'Segoe Print', 'Chalkboard SE', 'Comic Sans MS', cursive;
  color: var(--paper-ink);
  background-color: #fffdf6;
  background-image:
    linear-gradient(to right, transparent 54px, #f0a0a0 54px, #f0a0a0 56px, transparent 56px),
    repeating-linear-gradient(to bottom, transparent 0, transparent calc(var(--rule) - 2px), #b9d3ea calc(var(--rule) - 2px), #b9d3ea var(--rule));
  background-origin: border-box, content-box;
  background-clip: border-box, border-box;
  box-shadow: 0 10px 24px rgba(0, 0, 0, 0.14), 0 2px 4px rgba(0, 0, 0, 0.08);
  transform: rotate(-0.6deg);
  /* the torn top edge */
  clip-path: polygon(0 14px, 3% 6px, 7% 15px, 11% 4px, 15% 12px, 19% 2px, 23% 13px, 28% 5px, 33% 14px, 38% 3px, 43% 11px, 48% 1px, 53% 12px, 58% 4px, 63% 13px, 68% 2px, 73% 11px, 78% 5px, 83% 14px, 88% 3px, 93% 12px, 97% 5px, 100% 14px, 100% 100%, 0 100%);
}
.market-paper-holes {
  position: absolute;
  top: 20px;
  left: 0;
  right: 0;
  height: 14px;
  background-image: radial-gradient(circle at 7px 7px, #d8d2c4 0 5px, transparent 6px);
  background-size: 40px 14px;
  background-position: 18px 0;
  background-repeat: repeat-x;
}
.market-paper-head {
  margin: 0;
  /* Two whole rules — a rule and a half shifted every line under it by
     half a rule, so the ruled lines ran through the words (Adam's
     screenshot, v4.102.0). */
  height: calc(var(--rule) * 2);
  line-height: calc(var(--rule) * 2);
  font-family: inherit;
  font-size: clamp(1.8rem, 4.4dvh, 2.6rem);
  font-weight: 700;
  text-decoration: underline;
  text-decoration-thickness: 3px;
  text-underline-offset: 6px;
  letter-spacing: 0.01em;
}
.market-paper-intro { margin: 0; box-sizing: border-box; height: var(--rule); line-height: var(--rule); font-size: clamp(1.15rem, 2.8dvh, 1.55rem); }
.market-paper-lines { display: grid; grid-auto-rows: var(--rule); align-content: start; min-height: calc(var(--rule) * 6); }
.market-paper-line {
  display: flex;
  align-items: flex-end;
  gap: 10px;
  box-sizing: border-box;
  height: var(--rule);
  line-height: 1;
  padding-bottom: 6px;
  width: 100%;
  padding: 0 6px 0 0;
  background: none;
  border: 0;
  border-radius: 8px;
  text-align: left;
  font: inherit;
  font-size: clamp(1.2rem, 3dvh, 1.65rem);
  color: var(--paper-ink);
  cursor: pointer;
}
.market-paper-line:disabled, .market-paper-line.is-note { cursor: default; }
.market-paper-line.is-note { font-size: clamp(1rem, 2.4dvh, 1.3rem); color: #6b7a99; }
.market-paper-line:not(:disabled):hover, .market-paper-line.is-picked { background: rgba(255, 224, 120, 0.55); }
.market-paper-line img { width: calc(var(--rule) - 8px); height: calc(var(--rule) - 8px); object-fit: contain; flex: none; }
.market-paper-nopic { width: calc(var(--rule) - 8px); flex: none; }
.market-paper-text { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Bought: struck through, "very thick and obvious" (Adam, v4.102.0); part
   bought: the whole line struck and what is left written beside it. */
.market-paper-text.is-struck { text-decoration: line-through; text-decoration-thickness: 0.22em; text-decoration-color: #c0392b; }
.market-paper-text.is-left { margin-left: 14px; color: #c0392b; }
.market-paper-line.is-done img { opacity: 0.55; }
.market-paper-line.is-done .market-paper-text { opacity: 0.75; }

/* ---- the wallet and the till ------------------------------------------- */
.market-money-card {
  display: grid;
  justify-items: center;
  align-content: start;
  gap: 4px;
  flex: 0 0 clamp(200px, 22vw, 300px);
  align-self: start;
  padding: 14px 12px 16px;
  background: var(--card);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-sm);
  text-align: center;
}
.market-money-pic { width: 100%; max-width: 260px; height: auto; display: block; }
.market-money { font-family: var(--font-display); font-weight: 800; font-size: clamp(2rem, 6.5dvh, 3.6rem); line-height: 1; color: #0b6d5d; margin-top: 4px; }
.market-money-label { color: var(--ink-soft); font-size: clamp(0.85rem, 1.9dvh, 1rem); }

/* ---- the stall ---------------------------------------------------------- */
/* The stall picture is the whole box (sized in JS to keep its shape —
   layoutMarketStall) and has THREE SHELVES (v4.102.0): the goods are laid
   on them by percentages of the picture, measured on stall.png (the white
   between the posts is 18%–82% of the width; the shelf spaces are 30–54%,
   52–64% and 70–83% of the height, each shelf's board just under). Each
   item is its picture, its name and its count. */
.market-stall { min-height: 0; flex: 0 1 auto; display: grid; place-items: center; overflow: hidden; }
.market-stall-box { position: relative; }
.market-stall-pic { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; pointer-events: none; }
.market-stall-goods { position: absolute; inset: 0; }
.market-shelf {
  position: absolute;
  left: 19%;
  right: 19%;
  display: flex;
  justify-content: space-evenly;
  align-items: flex-end;
  gap: 4px;
}
.market-shelf[data-shelf='0'] { top: 31%; height: 15.5%; }
.market-shelf[data-shelf='1'] { top: 50%; height: 15%; }
.market-shelf[data-shelf='2'] { top: 68.5%; height: 15%; }
.market-stall-note { position: absolute; left: 20%; right: 20%; top: 45%; margin: 0; text-align: center; font-weight: 700; color: var(--ink-soft); background: rgba(255, 255, 255, 0.85); padding: 8px 12px; border-radius: var(--r-md); }
.market-good {
  position: relative;
  display: grid;
  justify-items: center;
  gap: 0;
  padding: 2px 6px 0;
  background: none;
  border: 0;
  border-radius: 12px;
  cursor: pointer;
  font: inherit;
  color: var(--ink);
  min-width: 0;
  height: 100%;
  align-content: end;
}
.market-good img, .market-good-word { width: clamp(34px, 7.4dvh, 70px); height: clamp(34px, 7.4dvh, 70px); object-fit: contain; filter: drop-shadow(0 3px 3px rgba(0, 0, 0, 0.25)); }
.market-good-word { display: grid; place-items: center; background: #fff; border-radius: 10px; font-weight: 700; font-size: 0.8rem; text-align: center; overflow: hidden; }
.market-good-name { font-family: var(--font-display); font-weight: 800; font-size: clamp(0.7rem, 1.7dvh, 0.95rem); line-height: 1.1; text-align: center; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; background: rgba(255, 255, 255, 0.85); border-radius: 6px; padding: 1px 6px; margin-top: 2px; }
/* The count sits on the picture's top-right corner so it never covers a
   name or the next shelf. */
.market-good-qty { position: absolute; top: 0; right: 0; min-width: 26px; padding: 0 6px; border-radius: 999px; background: #fff; border: 2px solid var(--ink); font-family: var(--font-display); font-weight: 800; font-size: clamp(0.85rem, 1.9dvh, 1.05rem); line-height: 1.4; box-shadow: 0 2px 0 rgba(0, 0, 0, 0.15); }
.market-good:not(:disabled):hover, .market-good.is-picked { background: rgba(255, 224, 120, 0.7); }
.market-good.is-gone { opacity: 0.45; cursor: default; }
.market-good.is-gone .market-good-qty { border-color: var(--danger); color: var(--danger); }
.market-good.is-short:not(.is-gone) .market-good-qty { border-color: #e0a020; background: #fff3c4; }

/* ---- the deal pop-up ---------------------------------------------------- */
#market-deal .market-deal-sheet { position: relative; width: min(560px, 100%); display: grid; gap: 0; overflow: hidden; padding: 18px 20px 20px; }
.market-confetti { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; z-index: 1; }
.market-deal-body { position: relative; display: grid; gap: 12px; z-index: 2; }
.market-deal-head { display: flex; align-items: center; gap: 12px; }
.market-deal-head img { width: 56px; height: 56px; object-fit: contain; flex: none; }
.market-deal-head h2 { margin: 0; flex: 1; min-width: 0; font-size: clamp(1.3rem, 3dvh, 1.7rem); }
.market-deal-x { width: 40px; height: 40px; border-radius: 50%; border: 2px solid var(--line); background: #fff; font-size: 1.5rem; line-height: 1; cursor: pointer; color: var(--ink); flex: none; }
.market-deal-q { margin: 0; font-family: var(--font-display); font-weight: 700; font-size: clamp(1.15rem, 2.8dvh, 1.5rem); line-height: 1.3; }
.market-deal-q.is-deal { color: #0b6d5d; }
.market-deal-q.is-nomatch { color: #a12a20; }
.market-deal-sub { margin: 0; color: var(--ink-soft); }
.market-deal-sheet[data-step='deal'] { background: #f2fbf4; }
.market-deal-sheet[data-step='deal'] .market-deal-head h2 { color: #0b6d5d; font-size: clamp(1.6rem, 4dvh, 2.2rem); }
.market-deal-sheet[data-step='nomatch'], .market-deal-sheet[data-step='refused'] { background: #fff5f4; }
.market-deal-go { width: 100%; font-size: 1.2rem; }
.market-choices { display: grid; grid-template-columns: repeat(auto-fill, minmax(64px, 1fr)); gap: 8px; }
/* The names centred (Adam, v4.102.0) and a LIGHTER green when pressed —
   "the shade of green they highlight in is a little bit too dark". */
.market-choices.is-names { display: flex; flex-wrap: wrap; justify-content: center; }
.market-choices.is-names .market-choice { flex: 0 1 160px; }
.market-choice { min-height: 60px; font-family: var(--font-display); font-weight: 800; font-size: 1.25rem; background: var(--card-raised); border: 2px solid var(--line); color: var(--ink); }
.market-choice:hover, .market-choice:active, .market-choice:focus-visible,
.market-key:not(.is-ok):hover, .market-key:not(.is-ok):active { border-color: var(--go); background: #d7f0dc; color: var(--ink); }
.market-deal-sheet .market-deal-q { text-align: center; }
.market-deal-sheet .market-deal-sub { text-align: center; }
.market-deal-sheet .market-deal-head h2 { text-align: center; }
.market-price { display: flex; align-items: baseline; justify-content: center; gap: 4px; padding: 8px; background: #fff; border: 2px solid var(--line); border-radius: var(--r-md); }
.market-price-value { font-family: var(--font-display); font-weight: 800; font-size: clamp(1.8rem, 5dvh, 2.6rem); min-width: 2ch; text-align: left; }
.market-price-unit { font-family: var(--font-display); font-weight: 800; font-size: clamp(1.4rem, 4dvh, 2rem); color: var(--ink-soft); }
.market-keypad { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; max-width: 320px; justify-self: center; width: 100%; }
.market-key { min-height: 52px; font-family: var(--font-display); font-weight: 800; font-size: 1.3rem; background: var(--card-raised); border: 2px solid var(--line); color: var(--ink); }
.market-key.is-ok { background: var(--go); color: #fff; border-color: var(--go); }
.market-key.is-back { background: #fff; }
.market-pending { display: grid; justify-items: center; gap: 10px; padding: 14px; text-align: center; background: var(--brand-tint); border-radius: var(--r-md); }
.market-pending p { margin: 0; }
.market-pending-spin { width: 32px; height: 32px; border-radius: 50%; border: 4px solid var(--brand); border-top-color: transparent; animation: market-spin 0.9s linear infinite; }
@keyframes market-spin { to { transform: rotate(360deg); } }

/* ---- the result ---------------------------------------------------------- */
.market-line {
  display: grid;
  grid-template-columns: 44px minmax(0, 1fr) auto;
  align-items: center;
  gap: 10px;
  padding: 6px 10px;
  border-radius: var(--r-md);
  border: 2px solid var(--line);
  background: var(--card-raised);
  text-align: left;
}
.market-line.is-done { opacity: 0.55; }
.market-line img { width: 44px; height: 44px; object-fit: contain; }
.market-line-text { display: grid; gap: 0; min-width: 0; }
.market-line-text b { font-size: clamp(0.95rem, 2dvh, 1.1rem); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.market-result-sheet { width: min(720px, 100%); display: grid; gap: 10px; }
.market-result-sheet h2, .market-result-sheet .score-big, .market-result-sheet p { margin: 0; }
.market-over-lines { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 6px; }
@media (max-width: 640px) {
  .market-body { flex-direction: column; }
  .market-money-card { grid-template-columns: auto auto; align-items: center; justify-content: center; gap: 12px; flex-basis: auto; }
  .market-money-pic { max-width: 110px; }
}

/* ---- the console ------------------------------------------------------- */
.market-setup-layout { display: grid; grid-template-columns: minmax(0, 3fr) minmax(280px, 2fr); gap: 16px; align-items: start; }
.market-setup-layout > .card, .market-setup-left > .card { margin: 0; }
.market-setup-left { display: grid; gap: 16px; }
@media (max-width: 900px) { .market-setup-layout { grid-template-columns: 1fr; } }
.market-group-head { margin: 10px 0 6px; font-size: 0.95rem; color: var(--ink-soft); }
.market-items { display: flex; flex-wrap: wrap; gap: 6px; }
.market-item-chip { display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px 4px 6px; border-radius: 999px; border: 2px solid var(--line); background: var(--card-raised); font: inherit; font-weight: 700; color: var(--ink); cursor: pointer; }
.market-item-chip img, .market-item-nopic { width: 28px; height: 28px; object-fit: contain; }
.market-item-nopic { display: grid; place-items: center; font-size: 0.7rem; background: #fff; border-radius: 6px; color: var(--ink-soft); }
.market-item-chip.is-on { border-color: var(--go); background: #e6f4ea; }
.market-item-chip.is-custom { border-style: dashed; }
.market-item-chip.is-custom.is-on { border-style: solid; }
/* A row of the teacher's own items: the shared set-row plus its grammar. */
.market-custom-row { display: grid; gap: 4px; padding-bottom: 8px; border-bottom: 1px dashed var(--line); }
.market-custom-row:last-child { border-bottom: 0; }
.market-grammar { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; padding-left: 8px; }
.market-grammar-kind, .market-grammar-word { font: inherit; font-size: 0.9rem; padding: 4px 8px; border-radius: 8px; border: 2px solid var(--line); background: #fff; color: var(--ink); min-height: 0; }
input.market-grammar-word[type='text'] { width: 130px; min-height: 0; padding: 4px 8px; }
select.market-grammar-kind { width: auto; flex: 0 0 auto; }
.market-mode { padding: 12px 14px; display: flex; gap: 10px; align-items: flex-start; }
.market-mode input { margin-top: 4px; }
.market-mode:has(input:checked) { border-color: var(--brand); background: var(--brand-tint); }
.market-roles-layout { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; align-items: start; }
.market-roles-layout > .card + .card { margin: 0; }
.market-names { display: flex; flex-wrap: wrap; gap: 8px; min-height: 48px; }
#market-waiting-card { margin-bottom: 16px; }
.market-drop.is-over { border-color: var(--go); background: #e6f4ea; }
#screen-market-roles.is-dragging .market-drop, #screen-market-roles.is-placing .market-drop { border-style: dashed; cursor: copy; }
#screen-market-roles.is-placing .market-drop:hover { border-color: var(--go); background: #e6f4ea; }
.market-name.is-away { opacity: 0.6; }
.market-short-note { background: #fff3c4; border: 2px solid #e0c060; border-radius: var(--r-md); padding: 8px 14px; margin-bottom: 14px; }
.market-live-layout { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; align-items: start; }
.market-live-layout > .card + .card { margin: 0; }
@media (max-width: 900px) { .market-live-layout, .market-roles-layout { grid-template-columns: 1fr; } }
/* One row a child on the live screen: money and a count, never a list. */
.market-rows { display: grid; gap: 6px; }
.market-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; padding: 8px 12px; border-radius: var(--r-md); background: var(--card-raised); }
.market-row.is-done { background: #e6f4ea; }
.market-stat { display: grid; justify-items: center; min-width: 74px; line-height: 1.1; }
.market-stat b { font-family: var(--font-display); font-size: 1.25rem; }
.market-stat.is-done b { color: var(--go); }
.market-waiting { background: var(--brand-tint); color: var(--brand-dark); font-weight: 700; }
.market-deals { display: grid; gap: 4px; }
/* My Picture Sets (v4.104.0). */
.picture-sets-title { margin: 0; font-size: clamp(1.4rem, 3vw, 2rem); }
.set-card.is-library { cursor: pointer; }
.market-deal { padding: 4px 8px; border-radius: 6px; background: var(--card-raised); }

/* At The Market's set-up steppers (v4.110.0): label, −, value, + in four
   columns, so the list length and the purse line up under each other. */
.market-steppers { display: grid; grid-template-columns: auto auto minmax(3.5ch, auto) auto; gap: 8px 10px; align-items: center; justify-content: start; }
.market-steppers b { text-align: center; }

/* ======================================================================
   Spelling Race (v4.115.0) — the iPad
   ====================================================================== */

main#screen-spelling {
  position: relative;
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  gap: var(--game-gap);
  min-height: 0;
}
.spelling-top { display: flex; align-items: stretch; gap: 14px; }
.spelling-logo { height: clamp(64px, 13dvh, 120px); width: auto; flex: none; }
.spelling-status-card {
  flex: 1; min-width: 0; display: grid; gap: 8px; align-content: center;
  background: var(--card); border: 3px solid var(--line); border-radius: var(--r-lg); padding: 10px 16px;
}
.spelling-status-row { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; font-size: clamp(1.1rem, 2.6vmin, 1.5rem); }
.spelling-score { color: var(--accent); font-weight: 800; }
/* The countdown: a bar draining right to left, the seconds inside it. */
.spelling-clock { position: relative; height: 22px; border-radius: 999px; background: var(--card-raised, #eee6d8); overflow: hidden; border: 2px solid var(--line); }
.spelling-clock i { position: absolute; inset: 0; width: 100%; background: var(--go, #2e9e52); }
.spelling-clock.is-low i { background: var(--danger, #d64545); }
.spelling-clock span { position: relative; display: block; text-align: center; font-weight: 800; font-size: 0.85rem; line-height: 18px; color: #fff; text-shadow: 0 1px 2px rgba(0,0,0,0.4); }

.spelling-body { position: relative; display: grid; min-height: 0; }
/* One card: the picture on top, the spelling underneath (Adam's second
   note). The picture takes what the answer leaves. */
.spelling-card {
  min-height: 0; display: grid; grid-template-rows: minmax(0, 1fr) auto; gap: 12px;
  background: var(--card); border: 3px solid var(--line); border-radius: var(--r-lg); padding: 14px 16px; overflow: hidden;
}
.spelling-picture-box { min-height: 0; display: grid; place-items: center; overflow: hidden; }
.spelling-picture { max-width: 100%; max-height: 100%; width: auto; height: auto; object-fit: contain; border-radius: 10px; -webkit-user-drag: none; user-select: none; }
.spelling-no-picture { font-size: clamp(3rem, 14vmin, 8rem); font-weight: 800; color: var(--muted); }
.spelling-answer { display: grid; grid-auto-rows: auto; justify-items: center; align-content: center; gap: 14px; }
.spelling-answer.is-locked { opacity: 0.85; }
/* "Get ready" — the green circle over the card before every word, then
   3, 2, 1, Go. */
/* The circle is sized in pixels by play.js (`--circle`, from the room it
   has) with the text sized from it — Adam saw an oblong: a circle sized
   by the viewport and free to grow round its own words is not one. Both
   width and height are set and the text can never widen it. */
.spelling-ready { position: absolute; inset: 0; display: grid; place-items: center; background: var(--card); border: 3px solid var(--line); border-radius: var(--r-lg); }
.spelling-ready-circle {
  --circle: 300px;
  width: var(--circle); height: var(--circle); flex: none; border-radius: 50%; background: var(--go, #2e9e52); color: #fff;
  display: flex; align-items: center; justify-content: center; text-align: center; padding: 0; overflow: hidden; box-sizing: border-box;
  font-family: var(--font-display); font-weight: 800; font-size: calc(var(--circle) * 0.15); line-height: 1.05; box-shadow: var(--shadow-md);
}
.spelling-ready-circle span { display: block; width: 100%; padding: 0 6%; box-sizing: border-box; }
.spelling-ready-circle.is-count { font-size: calc(var(--circle) * 0.5); line-height: 1; }
.spelling-ready-circle.is-go { background: var(--accent); font-size: calc(var(--circle) * 0.4); }

/* The boxes the word goes in — one a letter, a gap between words. */
.spelling-boxes { display: flex; flex-wrap: wrap; justify-content: center; align-items: center; gap: 8px 26px; max-width: 100%; }
.spelling-word { display: inline-flex; gap: 6px; }
.spelling-box {
  --tile: clamp(44px, 8vmin, 72px);
  width: var(--tile); height: var(--tile); padding: 0; display: grid; place-items: center;
  border: 3px dashed var(--line); border-radius: 10px; background: var(--card-raised, #f4efe4); cursor: pointer; font: inherit;
}
.spelling-box.is-filled { border-style: solid; border-color: var(--brand); background: var(--card); }
.spelling-box.is-over { border-color: var(--go); box-shadow: 0 0 0 4px var(--brand-tint); }
.spelling-box:disabled { cursor: default; }

/* Scrabble-like tiles: ivory, a shadow under, the letter in capitals. */
.spelling-tile {
  --tile: clamp(44px, 8vmin, 72px);
  width: var(--tile); height: var(--tile); padding: 0; display: grid; place-items: center;
  border: 0; border-radius: 8px; background: linear-gradient(180deg, #fbf3df, #efdfbb); color: #3a2c15;
  box-shadow: 0 4px 0 #b99a63, 0 6px 8px rgba(0,0,0,0.18);
  font-family: var(--font-display); font-weight: 800; font-size: calc(var(--tile) * 0.58); text-transform: uppercase; line-height: 1;
  cursor: grab; touch-action: none; -webkit-user-select: none; user-select: none; -webkit-touch-callout: none;
  transition: transform 0.12s ease;
}
.spelling-tile:active, .spelling-tile.is-lifted { transform: translateY(2px); box-shadow: 0 2px 0 #b99a63, 0 3px 4px rgba(0,0,0,0.18); }
.spelling-tile.is-in-box { width: calc(var(--tile) - 8px); height: calc(var(--tile) - 8px); font-size: calc((var(--tile) - 8px) * 0.58); cursor: pointer; }
.spelling-tile.is-away { visibility: hidden; }
.spelling-tile:disabled { cursor: default; }
.spelling-box.is-landed .spelling-tile { animation: spelling-land 0.22s ease-out; }
@keyframes spelling-land { from { transform: scale(1.25); } to { transform: scale(1); } }
.spelling-tile-ghost { position: fixed; z-index: 60; pointer-events: none; transform: translate(-50%, -50%) scale(1.1); opacity: 0.95; }
.spelling-tile-ghost:not(.is-moving) { opacity: 0; }
/* The rack: a wooden ledge the tiles sit on. */
.spelling-rack {
  display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; max-width: 100%;
  padding: 10px 14px 12px; border-radius: 12px; background: linear-gradient(180deg, #a5713d, #8a5a2b); box-shadow: inset 0 3px 6px rgba(0,0,0,0.25);
}
.spelling-input {
  width: min(100%, 520px); font-family: var(--font-display); font-weight: 800; font-size: clamp(1.6rem, 6vmin, 2.8rem);
  text-align: center; text-transform: uppercase; letter-spacing: 0.08em; padding: 14px 18px;
  border: 3px solid var(--line); border-radius: var(--r-lg); background: var(--card-raised, #f4efe4);
}
.spelling-input:focus { outline: none; border-color: var(--brand); box-shadow: 0 0 0 4px var(--brand-tint); }
/* The green tick — the moment the timer stops. */
.spelling-tick {
  width: clamp(64px, 11vmin, 92px); height: clamp(64px, 11vmin, 92px); border-radius: 50%; border: 0; padding: 0;
  background: var(--go, #2e9e52); color: #fff; font-size: clamp(1.8rem, 5.5vmin, 3rem); font-weight: 900; cursor: pointer;
  box-shadow: 0 6px 0 rgba(0,0,0,0.18), var(--shadow-md); display: grid; place-items: center; line-height: 1;
}
.spelling-tick:disabled { background: #b8c1bb; box-shadow: none; cursor: default; opacity: 0.7; }
.spelling-tick:not(:disabled):active { transform: translateY(3px); box-shadow: 0 3px 0 rgba(0,0,0,0.18); }

/* After a word: the sheet, green for right, red-edged for wrong. */
.spelling-result-sheet { display: grid; gap: 12px; justify-items: center; text-align: center; width: min(560px, 100%); border: 4px solid var(--line); }
.spelling-result-sheet.is-right { border-color: var(--go); }
.spelling-result-sheet.is-right h2 { color: var(--go); }
.spelling-result-sheet.is-wrong { border-color: var(--danger, #d64545); }
.spelling-result-sheet.is-wrong h2 { color: var(--danger, #d64545); }
.spelling-result-sheet.is-over { border-color: var(--accent); }
.spelling-compare { display: grid; gap: 10px; width: 100%; }
.spelling-compare-row { display: grid; gap: 2px; padding: 10px 14px; border-radius: 12px; background: var(--card-raised, #f4efe4); }
.spelling-compare-row b { font-family: var(--font-display); font-size: clamp(1.4rem, 5vmin, 2.2rem); letter-spacing: 0.08em; text-transform: uppercase; }
.spelling-compare-row.is-right { background: #e3f5e8; }
.spelling-compare-row.is-right b { color: var(--go); }
/* "Try again" (Adam's fourth note): a greyed box that counts 3, 2, 1,
   then lights up. */
.spelling-retry {
  width: min(100%, 360px); padding: 16px 20px; border-radius: 16px; border: 3px solid var(--line);
  background: #d9d9d9; color: #7a7a7a; font-family: var(--font-display); font-weight: 800; font-size: clamp(1.4rem, 5vmin, 2.2rem); cursor: default;
  transition: background 0.25s ease, color 0.25s ease, transform 0.12s ease;
}
.spelling-retry.is-ready { background: var(--go, #2e9e52); color: #fff; border-color: var(--go, #2e9e52); cursor: pointer; box-shadow: 0 6px 0 rgba(0,0,0,0.18), var(--shadow-md); }
.spelling-retry.is-ready:active { transform: translateY(3px); box-shadow: 0 3px 0 rgba(0,0,0,0.18); }
.spelling-podium-mini { display: grid; gap: 8px; width: 100%; }
.spelling-podium-row { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 10px; align-items: center; padding: 8px 14px; border-radius: 12px; background: var(--card-raised, #f4efe4); text-align: left; }
.spelling-podium-row.is-1 { background: #fff3c4; }
.spelling-podium-row.is-2 { background: #eeeeee; }
.spelling-podium-row.is-3 { background: #f5e1cf; }
.spelling-medal { font-size: 1.6rem; }

/* ---- Spelling Race, the console ----------------------------------------- */
.spelling-setup-layout { display: grid; grid-template-columns: minmax(0, 3fr) minmax(280px, 2fr); gap: 16px; align-items: start; }
.spelling-setup-layout > .card { margin: 0; }
.spelling-setup-side { display: grid; gap: 16px; }
@media (max-width: 900px) { .spelling-setup-layout { grid-template-columns: 1fr; } }
.spelling-now { padding: 12px 18px; }
.spelling-now-text { display: grid; gap: 8px; min-width: 0; }
.spelling-now .spelling-clock { height: 26px; }
.spelling-now .spelling-clock span { line-height: 22px; font-size: 0.95rem; }
.spelling-now.is-over .spelling-clock i { background: var(--brand); }
.spelling-rows { display: grid; gap: 6px; margin-top: 8px; }
.spelling-row { display: flex; align-items: center; gap: 10px; padding: 8px 12px; border-radius: 12px; background: var(--card-raised, #f4efe4); }
.spelling-row.is-right { background: #e3f5e8; }
.spelling-row.is-wrong { background: #fbe4e4; }
.spelling-row-rank { min-width: 1.6em; font-weight: 800; color: var(--muted); }
.spelling-row .nm { font-weight: 700; }
.spelling-row-mark { font-weight: 700; font-size: 0.95rem; }
.spelling-row-mark.is-right { color: var(--go); }
.spelling-row-mark.is-wrong { color: var(--danger, #d64545); }
.spelling-row-score { font-size: 1.2rem; min-width: 3ch; text-align: right; color: var(--accent); }
/* The solo flow: how far each child is through their own words. */
.spelling-row-progress { font-size: 0.95rem; color: var(--muted); font-weight: 700; }
.spelling-row-progress.is-done { color: var(--go); }
#btn-spelling-word-next.is-ready { box-shadow: 0 0 0 4px var(--brand-tint); }
/* The podium pop-up. */
.spelling-podium-sheet { display: grid; gap: 14px; width: min(560px, 100%); border: 4px solid var(--accent); }
.spelling-podium { display: grid; gap: 10px; }
.spelling-podium-place { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 14px; align-items: center; padding: 12px 18px; border-radius: 14px; background: var(--card-raised, #f4efe4); }
.spelling-podium-place.is-1 { background: #fff3c4; font-size: 1.25rem; }
.spelling-podium-place.is-2 { background: #eeeeee; }
.spelling-podium-place.is-3 { background: #f5e1cf; }
.spelling-podium-medal { font-size: 2.2rem; line-height: 1; }
.spelling-podium-name { font-family: var(--font-display); font-size: 1.3em; }
.spelling-podium-score { font-weight: 800; color: var(--accent); }
/* The review matrix. */
.spelling-legend { display: inline-flex; align-items: center; gap: 6px 10px; font-size: 0.8rem; color: var(--muted); flex-wrap: wrap; }
.spelling-legend i, .spelling-blob { display: inline-block; width: 16px; height: 16px; border-radius: 50%; }
.is-green.spelling-blob, .spelling-legend i.is-green { background: #2e9e52; }
.is-red.spelling-blob, .spelling-legend i.is-red { background: #d64545; }
.spelling-legend i.is-orange { background: #f0932b; }
.spelling-matrix-wrap { overflow-x: auto; margin-top: 8px; }
.spelling-matrix { border-collapse: separate; border-spacing: 4px; min-width: 100%; }
.spelling-matrix th, .spelling-matrix td { padding: 6px 8px; text-align: center; border-radius: 10px; }
.spelling-matrix td { background: var(--card-raised, #f4efe4); }
.spelling-matrix-name { text-align: left !important; white-space: nowrap; font-weight: 700; position: sticky; left: 0; background: var(--card); }
.spelling-matrix-word { display: table-cell; vertical-align: bottom; min-width: 72px; color: #fff; font-weight: 800; }
.spelling-matrix-word img { display: block; width: 48px; height: 48px; object-fit: contain; margin: 0 auto 4px; background: #fff; border-radius: 8px; }
.spelling-matrix-word span { display: block; font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.04em; }
.spelling-matrix-word small { display: block; font-weight: 600; opacity: 0.9; }
.spelling-matrix-word.is-green { background: #2e9e52; }
.spelling-matrix-word.is-orange { background: #f0932b; }
.spelling-matrix-word.is-red { background: #d64545; }
.spelling-matrix-word.is-none { background: var(--muted); }
.spelling-blob { width: 22px; height: 22px; }
