/* ============================================================================
   AKAM curriculum — universal stylesheet.

   Built for the data-driven renderer. Independent of the legacy styles.css
   so the two systems can coexist during the port.

   The only layout primitives:
     .ak-lineup-fit  — centering wrapper (measured by ResizeObserver)
     .ak-lineup      — flex row of cells; never wraps
     .ak-cell        — { .ak-image-slot, .ak-word-slot } — both always
                       in DOM, visibility toggles per stage
     .ak-content-grid — 2-row × N-col grid for multi-row cards (negation,
                       AND, questions, quiz); replaced the old .ak-row flow

   Sizing is viewport-relative via clamp(). The fit-to-container helper
   applies transform:scale when natural width exceeds the container.
   ========================================================================== */

:root {
  /* Trimmed ~8% from --ak-img and --ak-pill-font (previously 56-132 / 15-28)
     so single-emoji cells and word-pill text don't compete visually with
     M9's wider plural-row cells. Subtle reduction; preserves card layout
     overall. */
  --ak-img:       clamp(52px, 7.4vw, 122px);
  --ak-gap:       clamp(14px, 2.6vw, 48px);
  --ak-pill-font: clamp(14px, 1.95vw, 26px);
  --ak-pill-pad:  8px 16px;
  --ak-row-gap:   clamp(16px, 2.5vw, 36px);

  --ak-pill-bg:     #fff5d6;
  --ak-pill-ink:    #2d2a26;
  --ak-pill-border: #e8c95a;
  --ak-card-bg:     #ffffff;
  --ak-ink:         #2d2a26;
  --ak-muted:       #8a8378;
}

/* ---------- Card surface ---------- */
.ak-card {
  background: var(--ak-card-bg);
  border-radius: 20px;
  box-shadow: 0 10px 30px rgba(0,0,0,0.08);
  padding: clamp(28px, 4vw, 64px);
  display: flex;
  flex-direction: column;
  gap: var(--ak-row-gap);
  color: var(--ak-ink);
}
/* Exercise cards (present/quiz/match/connect/letter-fill/letter-tracing) strip
   their title in v2, leaving the inherited top padding as a big empty gap —
   tighten it for ALL of them. Splash/vocab keep their title + full padding. */
.ak-card.ak-card-notitle { padding-top: clamp(8px, 1.2vw, 18px); }

/* Card title — bigger, ink-coloured, with a subtle gold accent underline
   that echoes the AK pill aesthetic. The underline is implemented as a
   thin pseudo-element so it doesn't change the line-box height. Smaller /
   muted styling is kept for present / quiz / match (which strip this
   element entirely in v2 — the rule below is what splash + vocab use). */
.ak-card-title {
  position: relative;
  font-size: clamp(26px, 3vw, 42px);
  font-weight: 800;
  letter-spacing: 0.04em;
  color: var(--ak-ink);
  text-transform: uppercase;
  text-align: center;
  /* (2026-05-30) The gold ::after underline + its reserved padding-bottom were
     removed to free ~14-18px of vertical space on splash/vocab cards, so tall
     emoji (cucumber/pear) no longer overflow the card's bottom edge. Applies at
     ALL breakpoints (incl. full desktop + fullscreen). The underline was purely
     decorative; the padding only existed to make room for it. */
}

/* "MODULE N · ACTIVITY M" locator — pill chip that sits above the title
   on splash / vocab cards, echoing the AK word-slot pill aesthetic for
   visual cohesion. Inserted by `addCardLocator()` in v2/boot.js. */
.ak-card-locator {
  display: inline-block;
  align-self: center;
  padding: clamp(4px, 0.6vh, 8px) clamp(12px, 1.6vw, 22px);
  font-size: clamp(11px, 1.1vw, 14px);
  font-weight: 700;
  letter-spacing: 0.14em;
  color: var(--ak-pill-ink);
  text-transform: uppercase;
  text-align: center;
  background: var(--ak-pill-bg);
  border: 2px solid var(--ak-pill-border);
  border-radius: 999px;
  box-shadow: 0 3px 0 rgba(0, 0, 0, 0.06);
}
.ak-card-locator .ak-loc-sep {
  display: inline-block;
  margin: 0 clamp(4px, 0.6vw, 8px);
  color: var(--ak-pill-border);
  font-weight: 900;
}

/* ---------- Lineup (fit-to-container) ---------- */
.ak-lineup-fit {
  width: 100%;
  display: flex;
  justify-content: center;
  overflow: hidden;
}

.ak-lineup {
  display: inline-flex;
  flex-direction: row;
  align-items: flex-start;
  justify-content: center;
  gap: var(--ak-gap);
  flex-wrap: nowrap;
  transform-origin: top center;
  will-change: transform;
}

/* ---------- Cell ---------- */
.ak-cell {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  position: relative;
}

/* HE / SHE pronoun cells carry a small right-pointing triangle sitting just
   to the left of the cell — a deictic "that person" pointer used across the
   3rd-person modules (M7 HAS, M8 DOESN'T HAVE, M10 EAT/DRINK, M11 IS …).
   The wrapper sits in ONE grid column so the column count stays the same;
   the triangle adds a small chunk of width to that single column.
   Legacy reference: design-feedback-patterns.md "Triangle pointer + paired
   pronoun (M4 HE/SHE)". */
/* Triangle pointer for HE / SHE pronouns.
   One CSS-only rule covers every container kind: vocab cells, splash cells,
   present/quiz image cells, match tray pills + filled slots, letter-fill
   pictogram row, connect-quiz right column. The trigger is an attribute
   match on the boy.svg / girl.svg <img> via :has(), so any future render
   path that holds the asset gets the pointer automatically.

   Asset: /curriculum/assets/triangle.svg, rotated 90° to point right.
   Size: 0.5 × --ak-img — scales 1:1 with the emoji size in every card kind.
   Position: vertically centred on the image, with the tip sitting one
             triangle-width to the left so it sits close (~6% gap). */

:where(.ak-image-slot, .ak-grid-img, .ak-pill-drag, .ak-match-slot,
       .ak-letter-emoji, .ak-connect-emoji)
       :is(:has(> img[src$="/boy.svg"]), :has(> img[src$="/girl.svg"])),
:where(.ak-image-slot, .ak-grid-img, .ak-pill-drag, .ak-match-slot,
       .ak-letter-emoji, .ak-connect-emoji):has(> img[src$="/boy.svg"]),
:where(.ak-image-slot, .ak-grid-img, .ak-pill-drag, .ak-match-slot,
       .ak-letter-emoji, .ak-connect-emoji):has(> img[src$="/girl.svg"]) {
  position: relative;
  overflow: visible;   /* let the triangle extend past the container edge */
}
:where(.ak-image-slot, .ak-grid-img, .ak-pill-drag, .ak-match-slot,
       .ak-letter-emoji, .ak-connect-emoji):has(> img[src$="/boy.svg"])::before,
:where(.ak-image-slot, .ak-grid-img, .ak-pill-drag, .ak-match-slot,
       .ak-letter-emoji, .ak-connect-emoji):has(> img[src$="/girl.svg"])::before {
  content: '';
  position: absolute;
  /* Size tracks the parent container directly so the triangle always matches
     the rendered emoji size — works across vocab (column-constrained, ~90px),
     present (180px), match (160-180px), connect (larger), and any future
     context. Tight inline-SVG triangle (no padding) means the rendered shape
     fills 100% of these dimensions — so 80% width = a triangle 80% the size
     of the emoji next to it. */
  /* Triangle box ≈ 36% of the emoji's container — small enough to read as
     a pointer (not as a sibling figure) but proportional everywhere.
     (2026-05-31 user req: 45% → 36%, −20%, all viewports.) */
  width:  36%;
  height: 36%;
  /* Sit close to the emoji: with the 36% box, -33% from the parent's left edge
     puts the triangle's right edge ~3% INTO the image — the same closeness as
     before the size cut. (2026-05-31 user req: brought closer; was -42%, which
     after the 45%→36% shrink had opened a ~9% gap.) */
  left: -33%;
  top: 50%;
  transform: translateY(-50%);
  background-color: #6b7a8a;
  /* clip-path renders a precise right-pointing triangle filling the entire
     box edge-to-edge. polygon: top-left → right-middle (apex) → bottom-left. */
  -webkit-clip-path: polygon(0 0, 100% 50%, 0 100%);
          clip-path: polygon(0 0, 100% 50%, 0 100%);
  pointer-events: none;
  z-index: 2;
}

/* (Stacked cells above are used by SPLASH and VOCAB cards only — both
   slots visible together, image on top, word below.) */

/* ============================================================================
   EXERCISE + QUIZ — 2-row content grid.

   Both exercise and quiz cards use the same N-column × 2-row CSS Grid. Each
   column has TWO grid cells — one in the top row, one in the bottom row —
   and each grid cell contains EITHER an image OR a text pill, never both.

   The grid columns are sized once to the WIDEST possible content in each
   column (image-width vs text-pill-width), so a column's width doesn't change
   when content swaps between top and bottom across stages. The grid as a
   whole has the same size and layout at every stage; only what sits in each
   grid cell changes.

   Stages 1–3 keep images at full size in either row. The FINAL stage
   (all columns flipped — text-on-top, image-on-bottom) and the QUIZ use
   small images as visual support. The size choice is per-cell via
   .is-small.
   ========================================================================== */

.ak-content-grid {
  display: grid;
  grid-template-columns: repeat(var(--cols, 3), auto);
  column-gap: var(--ak-gap);
  row-gap: clamp(16px, 2vw, 32px);
  justify-content: center;
  align-items: center;
  justify-items: center;
  transform-origin: top center;
  will-change: transform;
}

.ak-grid-img {
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--ak-img);
  height: var(--ak-img);
  flex-shrink: 0;
}
.ak-grid-img > img { width: 100%; height: 100%; object-fit: contain; }

/* Fallback shown when a noun SVG fails to load (404 / network) — a visible
   "?" so a missing picture reads as an obvious gap, not an empty square.
   Swapped in by nounImg()'s onerror handler in renderer.js. */
.ak-img-missing {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  min-width: 1em;
  min-height: 1em;
  font-size: clamp(24px, 3vw, 44px);
  font-weight: 800;
  color: var(--ak-muted, #8a8378);
  border: 2px dashed var(--ak-muted, #8a8378);
  border-radius: 12px;
  opacity: 0.6;
}

/* Smaller image variant — used in the bottom row of the FINAL exercise
   stage (all-text-on-top) and in the quiz's anchor row at the top. */
.ak-grid-img.is-small {
  width: calc(var(--ak-img) * 0.55);
  height: calc(var(--ak-img) * 0.55);
}

.ak-grid-txt {
  display: flex;
  align-items: center;
  justify-content: center;
}

/* The text pill in a grid cell is the same .ak-word-slot used in splash /
   vocab — it inherits all its styling from the .ak-word-slot rule above,
   so no grid-specific override is needed here. */

/* Empty slot used by quiz drop targets. Renders the expected word as
   transparent so the slot auto-sizes to filled-pill width — no reflow. */
.ak-word-slot.is-slot {
  border-style: dashed;
  background: transparent;
  color: transparent;
}
.ak-word-slot.is-slot.is-correct {
  border-style: solid;
  background: var(--ak-pill-bg);
  color: var(--ak-pill-ink);
  animation: ak-pop 360ms cubic-bezier(.34,1.56,.64,1);
}

/* ---------- Match quiz (word → picture matching) ----------
   Empty image-slot-sized placeholder, dashed. On correct drop, the visual
   from the tray is moved in and the border solidifies. */
.ak-match-slot {
  border: 2px dashed var(--ak-pill-border);
  border-radius: 16%;
  background: transparent;
}
.ak-match-slot.is-filled {
  border-style: solid;
  background: rgba(255, 245, 214, 0.4);
  animation: ak-pop 360ms cubic-bezier(.34,1.56,.64,1);
}
/* Tray pill for the match quiz — wraps a visual at the SAME size as the
   placeholders above (full image-slot), so dragging a pill into its
   placeholder is visually a 1:1 transfer with no scale change. */
.ak-match-pill {
  width:  var(--ak-img);
  height: var(--ak-img);
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--ak-pill-bg);
  border: 2px solid var(--ak-pill-border);
  border-radius: 16%;
  padding: 8px;
  cursor: grab;
  -webkit-user-select: none;
          user-select: none;
  flex-shrink: 0;
  /* Pointer-events drag (see wirePointerDrag in renderer.js) needs the
     browser to surrender touch panning so the gesture goes to JS. */
  touch-action: none;
}
.ak-match-pill:active { cursor: grabbing; }
.ak-match-pill.is-used { opacity: 0; pointer-events: none; }
/* `transform` on .dragging already creates a stacking context, so z-index
   takes effect without needing position: relative — which would have
   nudged surrounding layout. */
.ak-match-pill.dragging { z-index: 100; cursor: grabbing; }
.ak-match-pill > img { width: 100%; height: 100%; object-fit: contain; }
.ak-match-pill > .ak-cshape,
.ak-match-pill > .ak-splatter { width: 100%; height: 100%; }

/* Title-only splash (used by activities without a frame — review/match). */
.ak-card-titleonly {
  align-items: center;
  justify-content: center;
  min-height: clamp(180px, 28vh, 320px);
  gap: clamp(20px, 3vh, 36px);
}
.ak-card-titleonly .ak-card-title {
  font-size: clamp(22px, 3vw, 36px);
  color: var(--ak-ink);
}
/* Emoji hint on the title-only splash — large, decorative, hints at activity. */
.ak-splash-emoji {
  font-size: clamp(72px, 12vw, 140px);
  line-height: 1;
  text-align: center;
  filter: drop-shadow(0 3px 6px rgba(0,0,0,0.18));
  animation: ak-splash-bob 2.6s ease-in-out infinite;
}
@keyframes ak-splash-bob {
  0%, 100% { transform: translateY(0) rotate(-2deg); }
  50%      { transform: translateY(-8px) rotate(2deg); }
}

/* ---------- Image slot ----------
   The slot's outer dimensions never change. Whatever content kind fills it
   (single image, group of N, digit keycap, color splatter) adapts to fit. */
.ak-image-slot {
  width: var(--ak-img);
  height: var(--ak-img);
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
.ak-image-slot img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}

/* Group cells: N copies in a horizontal row inside the same slot — M6 plural
   pattern, M9 plurals, and the splash/vocab equivalents. Both .ak-image-slot
   (splash/vocab) and .ak-grid-img (present/quiz) get identical layout so the
   plural group looks the same wherever it appears. The EXPLICIT per-count
   width matters: the dashed shaded brackets anchor at -26px from these widths,
   so the bracketed cells flank the full row. (A plain `width: auto` doesn't
   expand inside the grid track — the cell stays at min-content and the flex
   row visually overflows.) Per-image size shrinks as N grows so 5 fit without
   overflowing the lineup-fit container. */
.ak-image-slot.is-group,
.ak-grid-img.is-group {
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: center;
  gap: clamp(3px, 0.4vw, 8px);
  height: var(--ak-img);
  padding: 0;
}
.ak-image-slot.is-group[data-count="2"], .ak-grid-img.is-group[data-count="2"] { width: calc(var(--ak-img) * 1.55); }
.ak-image-slot.is-group[data-count="3"], .ak-grid-img.is-group[data-count="3"] { width: calc(var(--ak-img) * 2.05); }
.ak-image-slot.is-group[data-count="4"], .ak-grid-img.is-group[data-count="4"] { width: calc(var(--ak-img) * 2.4);  }
.ak-image-slot.is-group[data-count="5"], .ak-grid-img.is-group[data-count="5"] { width: calc(var(--ak-img) * 2.7);  }
.ak-image-slot.is-group > img,
.ak-grid-img.is-group > img {
  /* Default per-image size — overridden per data-count below. */
  width:  calc(var(--ak-img) * 0.6);
  height: calc(var(--ak-img) * 0.6);
  object-fit: contain;
  flex-shrink: 0;
}
.ak-image-slot.is-group[data-count="2"] > img, .ak-grid-img.is-group[data-count="2"] > img { width: calc(var(--ak-img) * 0.7);  height: calc(var(--ak-img) * 0.7);  }
.ak-image-slot.is-group[data-count="3"] > img, .ak-grid-img.is-group[data-count="3"] > img { width: calc(var(--ak-img) * 0.62); height: calc(var(--ak-img) * 0.62); }
.ak-image-slot.is-group[data-count="4"] > img, .ak-grid-img.is-group[data-count="4"] > img { width: calc(var(--ak-img) * 0.55); height: calc(var(--ak-img) * 0.55); }
.ak-image-slot.is-group[data-count="5"] > img, .ak-grid-img.is-group[data-count="5"] > img { width: calc(var(--ak-img) * 0.5);  height: calc(var(--ak-img) * 0.5);  }

/* ---------------------------------------------------------------------------
   Edge-to-edge SVG normalization (v2 mirror of legacy styles.css ~L1103).
   These specific SVGs fill their viewBox edge-to-edge, so at the same
   render dimensions they look LARGER than padded Twemoji-style siblings
   (which sit in the centre of their viewBox with transparent margin).
   Scaling them down by 0.85 brings the visible glyph in line. Applies to
   any v2 image cell — present, quiz, match, group. */
/* cake/guitar/hat/drum/robot dropped from the normalize list 2026-05-29 —
   the source SVGs were orphans (no catalog referenced them) and were deleted;
   their scale-down rules went with them. */
.ak-grid-img > img[src$="/peach.svg"],
.ak-grid-img > img[src$="/car.svg"],
.ak-grid-img > img[src$="/book.svg"],
.ak-grid-img > img[src$="/rocket.svg"],
.ak-grid-img > img[src$="/flower.svg"],
.ak-grid-img > img[src$="/coconut.svg"],
.ak-grid-img > img[src$="/watermelon.svg"],
.ak-grid-img > img[src$="/drink.svg"],
.ak-grid-img > img[src$="/snail.svg"],
.ak-grid-img > img[src$="/aeroplane.svg"],
.ak-image-slot > img[src$="/peach.svg"],
.ak-image-slot > img[src$="/car.svg"],
.ak-image-slot > img[src$="/book.svg"],
.ak-image-slot > img[src$="/rocket.svg"],
.ak-image-slot > img[src$="/flower.svg"],
.ak-image-slot > img[src$="/coconut.svg"],
.ak-image-slot > img[src$="/watermelon.svg"],
.ak-image-slot > img[src$="/drink.svg"],
.ak-image-slot > img[src$="/snail.svg"],
.ak-image-slot > img[src$="/aeroplane.svg"] {
  /* snail.svg + aeroplane.svg added 2026-05-30: their art fills the viewBox
     edge-to-edge, so at the same box size they paint larger than padded
     Twemoji siblings and overflowed the splash card's bottom edge. */
  transform: scale(0.85);
  transform-origin: center;
}

/* Number keycaps (one–five.svg) — the "N in a thick rounded rectangle" number
   tiles read ~20% larger than the noun emojis beside them (their art fills the
   viewBox edge-to-edge). Scale down 20% so they sit proportionally next to the
   nouns. All viewports; transform doesn't change layout → no overflow.
   (2026-05-31 user req.) Covers the sentence cells (.ak-grid-img) + vocab cells
   (.ak-image-slot) — the contexts the number keycaps appear in. */
.ak-grid-img   > img[src$="/one.svg"],
.ak-grid-img   > img[src$="/two.svg"],
.ak-grid-img   > img[src$="/three.svg"],
.ak-grid-img   > img[src$="/four.svg"],
.ak-grid-img   > img[src$="/five.svg"],
.ak-image-slot > img[src$="/one.svg"],
.ak-image-slot > img[src$="/two.svg"],
.ak-image-slot > img[src$="/three.svg"],
.ak-image-slot > img[src$="/four.svg"],
.ak-image-slot > img[src$="/five.svg"] {
  transform: scale(0.8);
  transform-origin: center;
}

/* Digit keycap (M6 numbers). */
.ak-image-slot.is-digit {
  background: var(--ak-ink);
  color: var(--ak-pill-bg);
  border-radius: 14%;
  font-weight: 800;
  font-size: calc(var(--ak-img) * 0.55);
  line-height: 1;
}

/* Color splatter (every color-cell / COLOR-slot visual across the curriculum).
   Painted as a CSS mask of `splatter.svg` — the SVG's alpha channel cuts the
   irregular splat shape, and the colour comes from `--ak-splatter-color`
   (set per-cell by the renderer from `colorTag` via `colorSwatch()`). The
   mask is orthogonal to `background`, so the multi-colour conic-gradient
   variant below (M4 A2) shows through the same splat outline.

   Was: an 80% rounded disc (CSS `border-radius: 50%`). Swapped to the
   splatter mask 2026-05-28 — the disc read as a generic colour chip and was
   visually identical to M1's CIRCLE shape lesson; the splat reads as a
   distinct "paint blob" pictogram that doesn't collide with the shape
   curriculum. */
.ak-image-slot.is-splatter > .ak-splatter,
.ak-splatter {
  width: 100%;
  height: 100%;
  background: var(--ak-splatter-color, #d8d2c5);
  -webkit-mask-image: url('/curriculum/assets/splatter.svg');
          mask-image: url('/curriculum/assets/splatter.svg');
  -webkit-mask-position: center;
          mask-position: center;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  -webkit-mask-size: contain;
          mask-size: contain;
}

/* CSS-mask shapes (cshape) — for M1 A3 COLORS and any later module that
   teaches geometric shapes. The shape outline comes from an SVG mask;
   the fill color comes from --ak-cshape-color (driven by item.color or
   a neutral default for vocab). */
.ak-cshape {
  display: block;
  width: 100%;
  height: 100%;
  background-color: var(--ak-cshape-color, #9e9e9e);
  -webkit-mask-position: center;
          mask-position: center;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  -webkit-mask-size: contain;
          mask-size: contain;
}
/* Shape size variants — used by CSS-mask shapes (`.ak-cshape`) when an
   activity teaches BIG vs SMALL with geometric shapes (which lack natural
   size).

   For raw SVG nouns (`<img>` inside `.ak-grid-img`), the renderer emits
   `ak-img-big` / `ak-img-small` ONLY when the activity opts in via the
   `scaleNounsBySize` flag (M5 A3 — the fruit catalog where no fruit has
   intrinsic size dominance). M1-M4 deliberately don't scale: their nouns
   are intrinsically sized (whale vs fish), so the noun word plus the SIZE
   archetype carry the BIG/SMALL signal on their own.

   The transforms below are scoped to the `.ak-card-size-scaled` parent so
   classes can land on cells of any activity, but only the opt-in module
   actually scales. Overflow:visible on the cell lets BIG (scale 2) spill
   past the cell's natural bounds without clipping. */
.ak-cshape.ak-cshape-big      { transform: scale(1);    }
.ak-cshape.ak-cshape-small    { transform: scale(0.55); }

/* Size-scaled cards: let the image spill past its cell, past the lineup-fit
   wrapper, and shrink the card's vertical padding so the BIG variant has
   room to render at its full transform-scaled size. */
.ak-card-size-scaled {
  padding-top:    clamp(12px, 1.6vw, 24px);
  padding-bottom: clamp(12px, 1.6vw, 24px);
}
.ak-card-size-scaled .ak-grid-img,
.ak-card-size-scaled .ak-image-slot {
  overflow: visible;
}
.ak-card-size-scaled .ak-lineup-fit {
  overflow: visible;
}
.ak-card-size-scaled .ak-grid-img.ak-img-big > img,
.ak-card-size-scaled .ak-image-slot.ak-img-big > img {
  transform: scale(1.2);
  transform-origin: center;
}
.ak-card-size-scaled .ak-grid-img.ak-img-small > img,
.ak-card-size-scaled .ak-image-slot.ak-img-small > img {
  transform: scale(0.5);
  transform-origin: center;
}

/* Shaded cell — used by M3 NOT's A3 "THIS IS NOT" pedagogy and by every
   M4 question activity (the silent subject the question is asking about).
   The picture in the subject cell is what THIS points at, but its label
   is wrong / hidden. Visually mute the picture so the contrast reads as
   "this thing is being asked about, but the words don't name it yet".
   Applies to both `.ak-image-slot` (used by buildCell / splash) and
   `.ak-grid-img` (used by buildImageGridCell / present + quiz). */
.ak-grid-img.ak-shaded > *,
.ak-image-slot.ak-shaded > * {
  filter: grayscale(70%) opacity(0.55);
}
.ak-grid-img.ak-shaded,
.ak-image-slot.ak-shaded {
  /* Dashed parenthesis brackets framing the picture on left + right (no
     top/bottom outline). Matches the legacy app's "( picture )" bracketed-
     subject pedagogy: the brackets read as "this referent is being
     considered", not as a frame around a peer pictogram. The brackets are
     ::before / ::after curves drawn with dashed borders, positioned just
     outside the cell box on each side. */
  position: relative;
  overflow: visible;
}
.ak-grid-img.ak-shaded::before,
.ak-grid-img.ak-shaded::after,
.ak-image-slot.ak-shaded::before,
.ak-image-slot.ak-shaded::after {
  content: '';
  position: absolute;
  top: 6%;
  bottom: 6%;
  /* Absolute width / offsets — previously % of cell width, which for wide
     group cells (M9 plural row) made the brackets enormous and pushed
     them off the cell. Fixed px keeps the bracket size consistent across
     standard cells AND wide group cells. */
  width: clamp(14px, 1.6vw, 22px);
  border: 3px dashed rgba(0, 0, 0, 0.28);
  pointer-events: none;
}
.ak-grid-img.ak-shaded::before,
.ak-image-slot.ak-shaded::before {
  left: clamp(-26px, -1.8vw, -16px);
  border-right: none;
  border-top-left-radius: 70% 50%;
  border-bottom-left-radius: 70% 50%;
}
.ak-grid-img.ak-shaded::after,
.ak-image-slot.ak-shaded::after {
  right: clamp(-26px, -1.8vw, -16px);
  border-left: none;
  border-top-right-radius: 70% 50%;
  border-bottom-right-radius: 70% 50%;
}
.ak-cshape.is-circle   { -webkit-mask-image: url('/curriculum/assets/circle.svg');   mask-image: url('/curriculum/assets/circle.svg');   }
.ak-cshape.is-square   { -webkit-mask-image: url('/curriculum/assets/square.svg');   mask-image: url('/curriculum/assets/square.svg');   }
.ak-cshape.is-triangle { -webkit-mask-image: url('/curriculum/assets/triangle.svg'); mask-image: url('/curriculum/assets/triangle.svg'); }
.ak-cshape.is-star     { -webkit-mask-image: url('/curriculum/assets/star.svg');     mask-image: url('/curriculum/assets/star.svg');     }
.ak-cshape.is-heart    { -webkit-mask-image: url('/curriculum/assets/heart.svg');    mask-image: url('/curriculum/assets/heart.svg');    }
.ak-cshape.is-moon     { -webkit-mask-image: url('/curriculum/assets/moon.svg');     mask-image: url('/curriculum/assets/moon.svg');     }
.ak-cshape.is-sun      { -webkit-mask-image: url('/curriculum/assets/sun.svg');      mask-image: url('/curriculum/assets/sun.svg');      }
.ak-cshape.is-balloon  { -webkit-mask-image: url('/curriculum/assets/balloon.svg');  mask-image: url('/curriculum/assets/balloon.svg');  }

/* ---------- Word slot (the pill) ----------
   Auto-sizes to text width. min-width keeps short words from looking lonely. */
.ak-word-slot {
  background: var(--ak-pill-bg);
  color: var(--ak-pill-ink);
  border: 2px solid var(--ak-pill-border);
  border-radius: 12px;
  padding: var(--ak-pill-pad);
  font-size: var(--ak-pill-font);
  font-weight: 700;
  letter-spacing: 0.04em;
  line-height: 1.1;
  min-width: 56px;
  text-align: center;
  white-space: nowrap;
}

/* ---------- Stage visibility ----------
   When a slot is hidden, its box stays (visibility:hidden, not display:none)
   so the lineup never deforms. This is the reserved-slot guarantee. */
.ak-image-slot[data-vis="hidden"] > * { visibility: hidden; }
.ak-image-slot[data-vis="hidden"]     { visibility: hidden; } /* group/digit */
.ak-word-slot[data-vis="hidden"]      { visibility: hidden; }

/* ---------- Vocab grid ----------
   Single-cell cards laid out in a wrap-friendly grid. Column count is
   chosen by the renderer based on cell count. */
.ak-vocab-grid {
  display: grid;
  grid-template-columns: repeat(var(--cols, 4), minmax(0, 1fr));
  gap: clamp(20px, 2.4vw, 40px);
  justify-items: center;
  width: 100%;
}
.ak-vocab-cell {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
}

/* Count-demo cell (M9 number vocab) — digit keycap on top, a row of N
   example pictograms below it, then the number word. Demonstrates the
   digit AS a quantity for early learners. */
.ak-count-demo {
  gap: clamp(6px, 0.9vw, 12px);
  padding: clamp(6px, 1vw, 14px);
}
.ak-count-demo-digit {
  width:  clamp(40px, 5vw, 72px);
  height: clamp(40px, 5vw, 72px);
}
.ak-count-demo-digit > img { width: 100%; height: 100%; object-fit: contain; }
.ak-count-demo-emojis {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: clamp(2px, 0.4vw, 6px);
  max-width: clamp(120px, 18vw, 240px);
}
.ak-count-demo-emojis > img {
  width:  clamp(24px, 2.8vw, 40px);
  height: clamp(24px, 2.8vw, 40px);
  object-fit: contain;
}

/* ---------- Quiz: drop targets + tray ----------
   Slot styling lives on `.ak-word-slot.is-slot` (see above); the renderer
   never emits a bare `.ak-slot` class. */
.ak-tray {
  display: flex;
  gap: clamp(10px, 1.8vw, 22px);
  justify-content: center;
  flex-wrap: wrap;
  /* Modest breathing room above the tray so pictograms don't touch the
     slot row above. padding-top (not margin-top) keeps the tray's box
     inside the parent flex column without compounding the parent gap. */
  padding-top: clamp(12px, 2vh, 22px);
}
.ak-pill-drag {
  cursor: grab;
  -webkit-user-select: none;
          user-select: none;
  /* Font size matches the middle row (full `--ak-pill-font`, not the prior
     0.92× shrink) — visual distinction comes from the outline, not size. */
  touch-action: none;
  /* Option A · "outlined" treatment (2026-05-28) — tray pills read as
     loose pieces visually distinct from the middle row's filled sentence
     pills. Background is a soft tint (much lighter than the filled pill)
     and the border thickens to 3 px in the pill's full border colour; the
     text stays at `var(--ak-pill-ink)` so it remains readable. When the
     pill is correctly dropped it gets `.is-used` (opacity 0) and its
     target `.ak-word-slot.is-slot` becomes `.is-correct`, which restores
     the filled `var(--ak-pill-bg)` look — so the pill visually "joins"
     the sentence row, taking the middle row's colour.

     Scope notes (one-source-of-truth design):
       • Sentence-quiz word pills → use `.ak-pill-drag` + `.ak-word-slot`;
         this rule overrides the filled `.ak-word-slot` bg via later source
         order, so they pick up the outlined look automatically.
       • Letter-fill pills → use `.ak-pill-drag` + `.ak-letter-pill`; the
         `.ak-letter-pill` rule (below, line ~1244) explicitly strips bg
         and border because the LETTER is the visual itself — its own
         rule wins via later source order, so letter pills look unchanged.
       • Match-quiz pills → use `.ak-match-pill` (separate class, not
         `.ak-pill-drag`), so they're untouched.
     Every quiz / letter-fill activity across every module inherits this
     behaviour with no per-module override. */
  background: color-mix(in srgb, var(--ak-pill-bg, #fff5d6) 25%, #ffffff);
  border-width: 3px;
}
.ak-pill-drag:active { cursor: grabbing; }
.ak-pill-drag.is-used { opacity: 0; pointer-events: none; }
.ak-pill-drag.dragging { z-index: 100; cursor: grabbing; }

@keyframes ak-pop {
  0%   { transform: scale(0.85); }
  60%  { transform: scale(1.08); }
  100% { transform: scale(1); }
}

/* (Quiz now uses the same .ak-content-grid as exercises — small images on
   top row, text pills on bottom, tray below. No separate support-row class
   needed.) */

/* ============================================================================
   M4 — Question-form card anatomy (cardKind: 'question').

   Layout: question grid → SHOW ANSWER button → reserved answer-reveal block.
   The reveal block is rendered into the DOM at full size from card-enter,
   then visibility-toggled. No height animation, no layout shift.
   ========================================================================== */

.ak-question-card {
  /* Same .ak-card surface; tighter row gap so the button + reveal pack
     under the question without dwarfing it. M4 also shrinks the cell
     sizing tokens so the question grid + answer reveal both fit inside
     the standard card vertical budget, and trims bottom padding so the
     answer-emoji row sits comfortably inside the card edge. */
  gap: clamp(6px, 1vw, 12px);
  padding-bottom: clamp(8px, 1.1vw, 14px);
  --ak-img:       clamp(60px, 9vw, 130px);
  --ak-pill-font: clamp(14px, 2.1vw, 26px);
  --ak-gap:       clamp(16px, 3vw, 48px);
  --ak-row-gap:   clamp(8px, 1.2vw, 16px);
}
/* Shrink the in-grid row-gap (between image row and text row inside each
   .ak-content-grid) for M4 cards — the default 16–32px adds up across the
   question grid + answer grid and pushes the bottom emoji past the card
   edge. The grid itself reads --ak-row-gap above for consistency. */
.ak-question-card .ak-content-grid {
  row-gap: clamp(6px, 1vw, 14px);
}
/* Answer-row text is a touch smaller than the question text so the answer
   reads as an "echo" rather than a peer sentence, but still comfortably
   readable for the 2–4 audience. */
.ak-question-card .ak-answer-grid .ak-grid-txt .ak-word-slot,
.stage .ak-question-card .ak-answer-grid .ak-grid-txt .ak-word-slot {
  font-size: calc(var(--ak-pill-font) * 0.88);
  padding: 6px 14px;
}

/* M4 WHAT cell — when the activity's pictogramOnlyOnStage flag has fired,
   the image cell is rendered with `.is-ghost`. We keep the slot dimensions
   (so the column width matches stage 1) but hide its visual content. */
.ak-grid-img.is-ghost > *,
.ak-grid-img.is-ghost {
  visibility: hidden;
}

/* In M4 A3 the shaded reference picture sits inline with the rest of the
   sentence row, so it competes for vertical space with the other cells +
   the answer-reveal block beneath. Shrink it inside .ak-question-card so
   the dashed frame stays compact and doesn't push the rest of the card
   downward. M3's standalone shaded cells (without .ak-question-card) keep
   their full size.
   `:not(.is-group)` exclusion — M9 A3's shaded plural cell needs its
   per-count width (calc(var(--ak-img) * 1.55–2.7)) so the dashed brackets
   actually flank all N pictograms. Without this exclusion, the compaction
   width above would beat the per-count rule (same specificity, later in
   file) and the emojis would overflow the brackets. */
.ak-question-card .ak-grid-img.ak-shaded:not(.is-group) {
  width:  calc(var(--ak-img) * 0.72);
  height: calc(var(--ak-img) * 0.72);
}
/* Shrink only the PICTURE inside the shaded brackets (the brackets are
   ::before/::after on the cell, so the cell size — and thus the brackets —
   stay put). At 82% the emoji sits centered within the dashed parens with
   even breathing room top and bottom, instead of the full-size picture
   poking past the bracket's bottom curve. */
.ak-grid-img.ak-shaded:not(.is-group) > img {
  width:  82%;
  height: 82%;
}
/* Splash shaded subject (the bracketed example, e.g. M3/M4 A3 "( picture )").
   At full size the brackets + picture dominated the splash and the emoji poked
   past the bracket. Shrink the CELL (the brackets are ::before/::after on it, so
   they scale down with it) and shrink the picture inside it MORE, so the
   bracketed subject reads as a compact cue rather than a peer-size emoji. */
.ak-image-slot.ak-shaded:not(.is-group) {
  width:  calc(var(--ak-img) * 0.72);
  height: calc(var(--ak-img) * 0.72);
}
.ak-image-slot.ak-shaded:not(.is-group) > img {
  width:  82%;
  height: 82%;
}
/* The shrunk shaded cell is shorter than its full-size splash siblings, and the
   splash lineup top-aligns cells (.ak-lineup { align-items: flex-start }) — so
   center this one vertically to sit on the same midline as the other emojis. */
.ak-lineup .ak-cell:has(> .ak-shaded) {
  align-self: center;
}

/* (??) chip — frame-level marker for yes/no question form (M4 A3). Dashed
   pill, sized to the standard image-row glyph so it columns alongside other
   cells. Uses the muted ink colour so it reads as a marker, not a peer
   sentence cell. */
.ak-question-chip {
  /* "??" rendered as plain text, framed with the same dashed brackets as
     .ak-shaded — visually pairs the chip with the shaded subject so they
     read as a unit. Position relative so the ::before/::after on the
     parent cell anchor against this element's bounds. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: calc(var(--ak-img) * 0.45);
  height:    calc(var(--ak-img) * 0.45);
  font-weight: 800;
  letter-spacing: 0.04em;
  font-size: calc(var(--ak-img) * 0.36);
  color: var(--ak-muted);
}
.ak-grid-img.ak-grid-chip {
  /* The (??) chip sits inside the image row; its sibling text-row cell
     (.ak-grid-chip-spacer) is empty so the column reserves just the chip's
     vertical band. Position relative + dashed parens (::before / ::after)
     match the .ak-shaded treatment — chip and shaded subject visually pair. */
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
  overflow: visible;
}
.ak-grid-img.ak-grid-chip::before,
.ak-grid-img.ak-grid-chip::after {
  content: '';
  position: absolute;
  top: 22%;
  bottom: 22%;
  width: 18%;
  border: 3px dashed rgba(0, 0, 0, 0.28);
  pointer-events: none;
}
.ak-grid-img.ak-grid-chip::before {
  left: 6%;
  border-right: none;
  border-top-left-radius: 70% 50%;
  border-bottom-left-radius: 70% 50%;
}
.ak-grid-img.ak-grid-chip::after {
  right: 6%;
  border-left: none;
  border-top-right-radius: 70% 50%;
  border-bottom-right-radius: 70% 50%;
}
/* .ak-grid-txt.ak-grid-chip-spacer — an empty grid cell that just holds
   the (??) chip's column in the text row. Needs no styling; the bare class
   exists only so the renderer can place a spacer cell. */
/* Splash variant: chip rendered as a cell inside the splash lineup.
   Mirrors the present-card chip's dashed brackets (::before / ::after) so
   the splash anatomy matches the exercise anatomy at a glance. Min-width
   is sized to match the .ak-grid-img.ak-grid-chip in present cards so the
   bracket curves have room to render visibly. */
.ak-cell.ak-cell-chip {
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
  overflow: visible;
  min-width: calc(var(--ak-img) * 0.7);
  height:    calc(var(--ak-img) * 0.7);
}
.ak-cell.ak-cell-chip::before,
.ak-cell.ak-cell-chip::after {
  content: '';
  position: absolute;
  top: 22%;
  bottom: 22%;
  width: 18%;
  border: 3px dashed rgba(0, 0, 0, 0.28);
  pointer-events: none;
}
.ak-cell.ak-cell-chip::before {
  left: 6%;
  border-right: none;
  border-top-left-radius: 70% 50%;
  border-bottom-left-radius: 70% 50%;
}
.ak-cell.ak-cell-chip::after {
  right: 6%;
  border-left: none;
  border-top-right-radius: 70% 50%;
  border-bottom-right-radius: 70% 50%;
}

/* Multi-colour splatter (M4 A2's COLOR-concept cell). Rainbow conic-gradient
   painted through the same rounded "splatter" shape used for single colours.
   Selectors match every context the splatter appears in (splash buildCell,
   vocab grid, present grid) at higher specificity than the base
   `.ak-image-slot.is-splatter > .ak-splatter { background: var(...) }` rule
   so the conic-gradient wins. */
.ak-splatter.is-multi,
.ak-image-slot.is-splatter > .ak-splatter.is-multi,
.ak-grid-img > .ak-splatter.is-multi {
  background: conic-gradient(
    from 0deg,
    var(--red)    0deg,
    var(--orange) 60deg,
    var(--yellow) 120deg,
    var(--green)  180deg,
    var(--blue)   240deg,
    var(--purple) 300deg,
    var(--red)    360deg
  );
}

/* SHOW ANSWER button — accent-tinted pill, centered. Compact size so it
   doesn't push the answer reveal too far down on small viewports. Acts
   as a toggle: each press flips the answer block visibility and the
   button label between SHOW ANSWER / HIDE ANSWER. */
.ak-show-answer-btn {
  align-self: center;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 8px 24px;
  min-width: 180px;
  font: inherit;
  font-weight: 800;
  letter-spacing: 0.05em;
  font-size: clamp(14px, 1.5vw, 18px);
  color: #fff;
  background: var(--blue);
  border: 2px solid var(--blue-deep);
  border-radius: 999px;
  cursor: pointer;
  box-shadow: 0 4px 0 var(--blue-deep), 0 8px 16px rgba(var(--shadow-rgb), 0.10);
  transition: transform 120ms ease, box-shadow 120ms ease, opacity 200ms ease;
  text-transform: uppercase;
}
.ak-show-answer-btn:hover {
  transform: translateY(-1px);
  box-shadow: 0 5px 0 var(--blue-deep), 0 10px 18px rgba(var(--shadow-rgb), 0.12);
}
.ak-show-answer-btn:active {
  transform: translateY(2px);
  box-shadow: 0 2px 0 var(--blue-deep);
}
.ak-show-answer-btn[aria-expanded="true"] {
  /* Visual hint that the answer is currently revealed — slight opacity
     drop on the otherwise actionable button (toggles back to hide). */
  opacity: 0.78;
}

/* Answer reveal — reserved space in the DOM (no layout shift). visibility
   flips on press; an opacity + translateY transition gives the "wow"
   without animating layout. */
.ak-answer-reveal {
  visibility: visible;
  opacity: 1;
  transform: translateY(0);
  transition: opacity 240ms ease, transform 240ms ease;
}
.ak-answer-reveal.is-hidden {
  visibility: hidden;
  opacity: 0;
  transform: translateY(-6px);
}
/* M8 paired-negation card — the correction sentence sits permanently below
   the negation, separated by an explicit dashed divider. Reuses the
   .ak-answer-reveal scaffolding from M4 but with two key differences:
     1) Never hidden (no `is-hidden` toggle).
     2) Cells render at FULL size, not the M4 hint-row .is-small.
        The correction is a co-equal sentence, not a "hint" below a
        question, so it deserves the same visual weight as the top row. */
.ak-paired-card {
  /* Tighter vertical rhythm than a standard card — two rows of sentence
     plus a divider needs a bit more breathing room without ballooning
     the whole card. */
  gap: clamp(8px, 1.2vw, 18px);
}
/* Correction row — always visible + a soft green-tinged panel so the
   affirmation reads as "this is right" without needing a label. */
.ak-paired-card .ak-correction-row {
  visibility: visible;
  opacity: 1;
  transform: none;
  background: rgba(92, 180, 92, 0.08);  /* matches --ak swatch green at 8% */
  border-radius: 14px;
  padding: clamp(10px, 1.4vw, 18px) clamp(8px, 1vw, 16px);
}
/* Co-equal sizing — override the .is-small that buildAnswerReveal applies
   for M4's hint context, so M8's correction row reads at parity with the
   negation row above. */
.ak-paired-card .ak-correction-row .ak-grid-img.is-small {
  width:  var(--ak-img);
  height: var(--ak-img);
}
.ak-paired-card .ak-correction-row .ak-grid-img.is-small > img {
  width: 100%;
  height: 100%;
}

/* M9 A2 paired-compare — the lower "THIS IS ONE …" row is a SECONDARY
   comparison (singular vs the plural above), so it renders slightly smaller
   than the plural row, not co-equal like M8's correction. Neutral tint —
   this is "one vs many", not a right/wrong judgment. These rules come after
   the .ak-paired-card block and share its specificity, so they win for
   cards carrying both classes. */
.ak-paired-compare .ak-correction-row {
  background: rgba(0, 0, 0, 0.04);
}
.ak-paired-compare .ak-correction-row .ak-grid-img.is-small {
  width:  calc(var(--ak-img) * 0.78);
  height: calc(var(--ak-img) * 0.78);
}
.ak-paired-compare .ak-correction-row .ak-grid-img.is-small > img {
  width: 100%;
  height: 100%;
}
.ak-paired-compare .ak-correction-row .ak-word-slot {
  font-size: calc(var(--ak-pill-font) * 0.85);
}
/* Dashed divider with a centered downward "leads-to" glyph (↓) so the visual
   doesn't read as just a horizontal line. The glyph sits ON the line via
   a small white circle that breaks the dash. */
.ak-paired-divider {
  position: relative;
  width: min(90%, 600px);
  margin: clamp(4px, 0.8vw, 10px) auto;
  border-top: 2px dashed var(--ak-pill-border, rgba(0, 0, 0, 0.22));
}

/* Text-only answer reveal (M9 A3) — bigger word pills, no pictogram row.
   The question card above already shows the shaded pictograms; the answer
   reads as a full text sentence. */
.ak-answer-grid.ak-answer-text-only {
  /* Override the standard 2-row content-grid to a single row of pills. */
  grid-template-rows: auto;
  row-gap: 0;
  padding: clamp(8px, 1.2vw, 16px) 0;
}
.ak-answer-grid.ak-answer-text-only .ak-grid-txt .ak-word-slot {
  font-size: clamp(20px, 2.6vw, 36px);
  padding: clamp(8px, 1vw, 14px) calc(var(--ak-pill-pad) + 4px);
}
.ak-paired-divider::after {
  content: '↓';
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: clamp(22px, 2.4vw, 32px);
  height: clamp(22px, 2.4vw, 32px);
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--ak-card-bg, #fff);
  color: var(--ak-ink, #2d2a26);
  border-radius: 50%;
  font-size: clamp(14px, 1.4vw, 20px);
  font-weight: 700;
  line-height: 1;
}

/* ============================================================================
   Letter-fill card (cardKind: 'letter-fill') — A7 in M5-M8.
   Sentence shown as text with one or more blanks; emoji lineup below;
   tray of letter pills. Drag a letter to the matching blank.
   ============================================================================ */
.ak-letter-card {
  align-items: center;
  gap: clamp(14px, 1.8vw, 26px);
}

/* ---------- Letter tracing (A8 + in-exercise tracing card) ----------
   The letter is a thin, smooth rounded STROKE drawn faint; tracing reveals the
   SAME stroke in green through a "corridor" clip that grows where the finger
   travels — so the green follows the fingertip with no gaps (ghost + ink are
   the identical path), start anywhere, any order. See renderer. */
.ak-trace-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(12px, 1.8vw, 24px);
  /* Top padding handled by the shared .ak-card-notitle rule (title is stripped). */
}
/* Single row — never wrap/stack, however long the word. Letters share the
   width and shrink to fit. */
.ak-trace-row {
  display: flex;
  flex-wrap: nowrap;
  justify-content: center;
  align-items: center;
  gap: clamp(2px, 0.6vw, 10px);
  width: 100%;
  /* Don't let the browser pan/scroll while a finger is tracing. */
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
}
/* Big letters that shrink to keep one row for long words. */
.ak-trace-letter {
  position: relative;
  flex: 1 1 0;
  min-width: 0;
  max-width: clamp(96px, 18vw, 200px);
  height: clamp(132px, 25vw, 270px);
}
.ak-trace-canvas {
  display: block;
  width: 100%;
  height: 100%;
  touch-action: none;
}
/* Smaller pictogram cue beneath the word. */
.ak-trace-pic {
  display: flex;
  justify-content: center;
  align-items: center;
  height: clamp(48px, 7.2vw, 86px);   /* 2026-05-31 user req: +20% (was 40/6vw/72) — emoji under the tracing letters was too small */
  opacity: 1;   /* natural colours — no longer faded until completion */
  transition: opacity .25s ease, transform .25s ease;
}
.ak-trace-pic img { height: 100%; width: auto; }
.ak-trace-card.is-complete .ak-trace-pic {
  opacity: 1;
  transform: scale(1.12);
}
/* Column-aligned 2-row grid — same shape as .ak-content-grid used by every
   other exercise's text+image lineup. Top row = word pills; bottom row =
   pictograms. Each emoji sits directly below its matching word. */
.ak-letter-grid {
  display: grid;
  grid-template-columns: repeat(var(--cols, 4), auto);
  /* Wider inter-word spacing so short sentences (3 cols, e.g. "GIRAFFE IS BIG")
     spread into the side space instead of bunching in the centre. The card's
     fit-to-container scale keeps the 4-col HAVE rows from overflowing at narrow
     landscape, so this only opens up where there's room. */
  column-gap: clamp(24px, 5vw, 72px);
  /* More vertical breathing room between the word row and the pictogram
     row so the two read as separate "layers" (primary text on top,
     meaning anchor below) rather than as one tight grid. */
  row-gap: clamp(22px, 3vw, 44px);
  justify-content: center;
  align-items: center;
  justify-items: center;
}
/* Each word reuses the .ak-word-slot pill so the letter-fill text matches
   every other exercise's lineup. Inline letters and blank slots live inside
   the pill. Font bumped vs. standard word-slot — the letter-fill text is
   the primary content of the card, not a supporting label. */
.ak-letter-word-slot {
  display: inline-flex;
  align-items: baseline;
  /* Centre the letters inside the pill — without this, a single-letter
     word like "I" sits at flex-start (the left edge) instead of centred. */
  justify-content: center;
  gap: 0;
  /* Primary content of the card — significantly larger than the standard
     word-slot so the child reads the sentence first and the pictograms
     below it act as a quiet meaning anchor. */
  font-size: clamp(36px, 4.4vw, 64px);
  padding: clamp(10px, 1.2vw, 18px) calc(var(--ak-pill-pad) + 8px);
}
/* Per-letter inline spans inside the word pill. ALL letters (regular
   and blank) share the same fixed slot width so the row reads as a
   uniform sequence — otherwise narrow letters (P, I) sit too tight and
   the wider blank slot would visually "gap" against its neighbours. */
.ak-letter-word-slot .ak-letter,
.ak-letter-word-slot .ak-letter-blank {
  display: inline-block;
  text-align: center;
  min-width: 0.7em;
  font: inherit;
  color: inherit;
  letter-spacing: 0;
  margin: 0;
}
.ak-letter-word-slot .ak-letter-blank {
  border-bottom: 2px solid var(--ak-ink, #2d2a26);
}
.ak-letter-word-slot .ak-letter-blank.is-filled {
  /* Once locked in place, the filled letter blends with its neighbours:
     inherits the word-slot text colour, loses the underline. Reads as a
     completed word, no longer as "the missing piece". The tray pill stays
     in its assigned colour so the child can still see which tray letters
     are spent (greyed via .is-used) vs. remaining. */
  color: inherit;
  border-bottom-color: transparent;
  animation: ak-pop 360ms cubic-bezier(.34, 1.56, .64, 1);
}
.ak-letter-emoji {
  /* Pictogram below each word — kept small (smaller than the standard
     image-slot) so it reads as a quiet meaning anchor under the bigger
     text, not as the primary content. */
  width:  clamp(28px, 3.6vw, 52px);
  height: clamp(28px, 3.6vw, 52px);
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0.85;
}
.ak-letter-emoji > img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}
.ak-letter-tray {
  margin-top: clamp(10px, 1.4vw, 18px);
  gap: clamp(12px, 1.6vw, 22px);
}
/* Tray "pill" — actually just a coloured letter glyph, no pill chrome. The
   per-letter colour (data-color-idx) marks which blank it pairs with so
   the child matches by colour and shape. Background / border stripped so
   it reads as text the child is moving, not as a button. Geometry kept
   wide enough to remain a comfortable touch target. */
.ak-letter-pill {
  min-width:  clamp(60px, 6.4vw, 84px);
  min-height: clamp(60px, 6.4vw, 84px);
  display: flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: none;
  /* Larger than the in-pill letters — without the pill's coloured frame
     around them, the tray glyphs read as smaller at the same numeric
     font-size. Bumping ~20% to match perceived size. */
  font-size: clamp(46px, 5.4vw, 78px);
  font-weight: 800;
  line-height: 1;
}
.ak-letter-pill[data-color-idx="0"] { color: #c44a6b; }
.ak-letter-pill[data-color-idx="1"] { color: #2f7fc3; }
.ak-letter-pill[data-color-idx="2"] { color: #4a9c4a; }
.ak-letter-pill[data-color-idx="3"] { color: #d4751a; }
.ak-letter-pill[data-color-idx="4"] { color: #8059b8; }

/* M8 negation-row "DON'T HAVE" / "DOESN'T HAVE" cell — the HAVE pictogram
   stays as the cell's primary image; a red X_sign is overlaid on top via
   a pseudo-element. Centered, slightly inset so the X reads as "denying
   what's in the hand" rather than replacing the picture entirely. */
.ak-grid-img.ak-img-neg-overlay,
.ak-image-slot.ak-img-neg-overlay {
  position: relative;
}
.ak-grid-img.ak-img-neg-overlay::after,
.ak-image-slot.ak-img-neg-overlay::after {
  content: '';
  position: absolute;
  /* Sits AT the tip of the purple down-arrow on have.svg (centre of the X
     aligns with the arrow tip), nudged right onto the palm. Box grown ~10%
     larger than the prior 44%×44% (now ~48%×48%) — same centre, more weight. */
  inset: 40% 20% 12% 31%;
  /* Recolour the X to gray via a mask (background-image can't be tinted).
     The X_sign.svg shape becomes a solid gray fill. */
  background-color: #6e6e6e;
  -webkit-mask: url('/curriculum/assets/X_sign.svg') no-repeat center / contain;
          mask: url('/curriculum/assets/X_sign.svg') no-repeat center / contain;
  pointer-events: none;
  filter: drop-shadow(0 1px 1px rgba(0,0,0,0.30));
  opacity: 0.95;
}
/* Standalone negation X — wherever X_sign.svg renders as a raw <img> (M8
   DON'T/DOESN'T tiles in match/connect/letters/quiz; M11 NOT) it shows
   gray instead of the harsh raw red, matching the #6e6e6e X used by the
   neg-have overlay above. The combined "DON'T HAVE" cell uses a mask (not an
   <img>) and is unaffected. */
img[src$="/X_sign.svg"] {
  filter: grayscale(100%) brightness(1.05);
}
/* The reveal grid uses the same .ak-content-grid as exercise cards, just
   with a subtle dashed top divider so it reads as a separate block. */
.ak-answer-fit {
  border-top: 2px dashed rgba(0, 0, 0, 0.10);
  padding-top: clamp(4px, 0.7vw, 9px);
}
/* The answer echo reuses .ak-content-grid, so its text cells inherit the
   question grid's tall min-height (0.75×--ak-img, set in v2/index.html for
   stage-flip row alignment). The echo never flips, so that min-height is
   pure dead space — it inflated the text row to ~2x the pill and pushed the
   emoji echo onto, and on shorter viewports past, the card's bottom edge.
   Collapse it so the echo packs tight: text row = pill height, emoji row
   directly beneath. (Higher specificity than the v2 min-height rule.) */
.stage .ak-question-card .ak-answer-grid .ak-grid-txt {
  min-height: 0;
}
.ak-question-card .ak-answer-grid {
  row-gap: clamp(4px, 0.8vw, 10px);
}
/* Answer-row emojis render at HALF the size of the question's emojis —
   the answer is a quick echo, not the main content. Higher specificity
   than v2's `.stage .ak-grid-img.is-small` override (two-class selector
   `.ak-answer-grid .ak-grid-img.is-small` wins). Compact answer keeps the
   whole card vertically symmetric: question above, answer-as-echo below. */
.ak-answer-grid .ak-grid-img.is-small,
.stage .ak-answer-grid .ak-grid-img.is-small {
  width:  calc(var(--ak-img) * 0.5);
  height: calc(var(--ak-img) * 0.5);
}
/* Answer text pills also compact, matching the smaller emoji scale. */
.ak-answer-grid .ak-grid-txt .ak-word-slot,
.stage .ak-answer-grid .ak-grid-txt .ak-word-slot {
  font-size: calc(var(--ak-pill-font) * 0.78);
  padding: 6px 14px;
}

/* A3 YES/NO truth marks — applied to the ✓ / ✗ cell in the answer row.
   The mark glyph sits inside .ak-grid-img on the emoji row; the word
   pill in the text row gets a colour-tinted background. */
.ak-mark-glyph {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  /* Sized below the image-cell box (0.55x of --ak-img) so the trailing
     comma stays inside the card's bottom padding instead of clipping. */
  font-size: calc(var(--ak-img) * 0.55);
  font-weight: 900;
  line-height: 1;
}
.ak-mark-glyph.is-yes { color: var(--agree-deep); }
.ak-mark-glyph.is-no  { color: var(--not-deep); }

.ak-grid-txt.ak-mark.ak-mark-yes > .ak-word-slot {
  background: #e6f7ec;
  border-color: var(--agree);
  color: var(--agree-deep);
}
.ak-grid-txt.ak-mark.ak-mark-no > .ak-word-slot {
  background: #fbeaea;
  border-color: var(--not);
  color: var(--not-deep);
}

/* ============================================================================
   Connect quiz (M4 A5) — tap-pair vertical match.

   Two columns: word pills on the left, emoji cells on the right. Middle
   "lane" is an SVG overlay where connector lines are drawn on each match.
   Cells are <button>s for keyboard accessibility + native click semantics.
   ========================================================================== */

.ak-connect-card {
  /* 4 rows × pill+emoji at a comfortable reading size for ages 2–4. The
     card grows to fill the stage vertical budget — there's no HEAR button
     below for the connect activity (tap-to-speak per cell handles audio).
     Tokens tuned so 4 rows comfortably fill the card without overflow. */
  gap: clamp(16px, 2.6vw, 32px);
  --ak-img:       clamp(86px, 11vw, 160px);
  --ak-pill-font: clamp(18px, 2.4vw, 32px);
}
.ak-connect-card .ak-card-title {
  font-size: clamp(24px, 2.8vw, 38px);
  padding-bottom: clamp(8px, 1.2vw, 14px);
}
.ak-connect-grid {
  position: relative;          /* anchor for the absolute SVG layer */
  display: grid;
  grid-template-columns: 1fr 1fr;
  column-gap: clamp(80px, 16vw, 220px);
  row-gap:    clamp(12px, 1.8vw, 24px);
  /* Each grid row sized to the tallest cell so pill[i] and emoji[i] share
     a height, giving the clean horizontal-row look. */
  align-items: stretch;
  justify-items: stretch;
  /* Cap the grid width so the lineup stays centered inside a wider card
     surface — the empty side area gives the rows visual breathing room. */
  max-width: clamp(500px, 72vw, 900px);
  margin-inline: auto;
  width: 100%;
}
/* Connect-card surface uses the same vertical budget as exercise cards
   (the question grid + button + reveal stack) so the layout matches the
   rest of the activity flow and leaves room beneath for the HEAR button. */
.stage > .ak-card.ak-connect-card {
  min-height: clamp(360px, 52vh, 540px);
  max-width:  clamp(720px, 90vw, 1100px);
}
/* Left column → cells hug the inner (right) edge so the dashed connector
   line starts at the pill's right side. Right column → cells hug the left
   edge so the line lands at the emoji's left side. */
.ak-connect-grid > .ak-connect-text  { justify-self: end;   }
.ak-connect-grid > .ak-connect-emoji { justify-self: start; }

.ak-connect-cell {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  background: var(--ak-pill-bg);
  color: var(--ak-pill-ink);
  border: 2px solid var(--ak-pill-border);
  border-radius: 12px;
  padding: 6px 18px;
  font: inherit;
  font-weight: 800;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  font-size: var(--ak-pill-font);
  min-height: calc(var(--ak-img) * 0.85);
  min-width:  calc(var(--ak-img) * 1.7);
  box-shadow: 0 3px 0 rgba(var(--shadow-rgb), 0.08);
  transition: background 160ms, border-color 160ms, transform 120ms, box-shadow 160ms;
  -webkit-user-select: none;
          user-select: none;
}
.ak-connect-cell.ak-connect-emoji {
  padding: 4px;
  min-width: calc(var(--ak-img) * 1.0);
  background: #fff;
}
.ak-connect-cell.ak-connect-emoji > img {
  width:  calc(var(--ak-img) * 0.8);
  height: calc(var(--ak-img) * 0.8);
  object-fit: contain;
  pointer-events: none;        /* clicks land on the button, not the <img> */
}
.ak-connect-cell:hover:not(.is-matched):not(.is-wrong) {
  transform: translateY(-1px);
  box-shadow: 0 5px 0 rgba(var(--shadow-rgb), 0.10);
}
.ak-connect-cell.is-selected {
  background: #fff3b0;
  border-color: var(--yellow-deep);
  transform: translateY(-1px);
  box-shadow: 0 5px 0 rgba(var(--shadow-rgb), 0.18);
}
.ak-connect-cell.is-matched {
  background: #e6f7ec;
  border-color: var(--agree);
  color: var(--agree-deep);
  cursor: default;
  box-shadow: 0 3px 0 rgba(var(--shadow-rgb), 0.12);
}
.ak-connect-cell.is-wrong {
  background: #fbeaea;
  border-color: var(--not);
  color: var(--not-deep);
  animation: ak-connect-shake 420ms cubic-bezier(.36, .07, .19, .97);
}
@keyframes ak-connect-shake {
  10%, 90% { transform: translateX(-2px); }
  20%, 80% { transform: translateX(3px);  }
  30%, 50%, 70% { transform: translateX(-4px); }
  40%, 60% { transform: translateX(4px);  }
}

/* SVG connector layer. Sits on top of the grid background but underneath
   the cell buttons — pointer-events:none lets clicks pass through. */
.ak-connect-lines {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  overflow: visible;
}
.ak-connect-line {
  stroke: var(--agree-deep);
  stroke-width: 3;
  stroke-dasharray: 9 7;
  stroke-linecap: round;
  fill: none;
  /* Subtle draw-in animation: dash offset shifts from full length to 0. */
  animation: ak-connect-line-draw 360ms ease-out both;
}
@keyframes ak-connect-line-draw {
  from { stroke-dashoffset: 200; opacity: 0; }
  to   { stroke-dashoffset:   0; opacity: 1; }
}

/* Reduced-motion accessibility lives in the legacy /styles.css (the canonical
   `*, *::before, *::after { animation/transition-duration: 0.001ms !important }`
   block) and applies to every element on the page, including all ak-* primitives
   defined above. No need to duplicate it here. The project's "no !important"
   rule has exactly ONE documented exception — that universal a11y block. */

/* ---------- Progress + sticker book ---------- */
.intro-header .sticker-book-btn { margin-top: 8px; }
/* Completed-module visual cue REMOVED 2026-05-28.
   User direction: all enabled module cards must look identical regardless
   of progress, so the picker reads as a single coherent design family.
   Earned/completed state is still tracked underneath via `progress.stickers`
   and surfaces in the sticker-book scene, the activity/module cheer screens,
   and the superhero capstone — but NOT on the picker. Previous styling
   attempts (outline-offset -3px / ::before overlay / box-shadow halo) all
   produced visual variants that broke card-to-card uniformity; the right
   answer is no special treatment at all. Keep the .is-earned class wired
   in boot.js (renderIntro still adds it) so re-introducing a cue later is
   one CSS rule away. (`.act-done` was removed 2026-05-29 — the broader CSS
   audit it was waiting for confirmed the `<span class="act-done">` path is
   dead; no JS emits it.) */

/* Container-driven like the picker (2026-05-29): auto-fit chooses the column
   count from width, so the two @media steps (700/460) are gone. minmax(150px)
   gives a pleasant responsive sticker wall — ~3 cols on a phone-in-landscape,
   ~5–6 on tablet/desktop — for the 13 hero/capstone slots. */
.sticker-book .sticker-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: clamp(10px, 1.6vw, 20px);
  width: 100%; max-width: var(--ak-content-w, 1100px); margin: 0 auto;
}
.sticker-slot {
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  padding: clamp(10px, 1.4vw, 18px);
  border-radius: var(--r-lg, 20px);
  background: var(--card, #fff); box-shadow: var(--shadow-3);
}
.sticker-slot .sticker-img { width: clamp(54px, 8vw, 86px); height: clamp(54px, 8vw, 86px); }
.sticker-slot.locked .sticker-img { filter: grayscale(1) opacity(0.28); }
.sticker-slot .sticker-label { font: 700 clamp(11px, 1.3vw, 14px)/1.1 'Fredoka', system-ui, sans-serif; color: var(--ink-soft); text-align: center; }
.sticker-slot.earned .sticker-label { color: var(--ink); }

.sticker-capstone {
  display: flex; align-items: center; gap: 12px; justify-content: center;
  margin: 0 auto clamp(12px, 1.6vw, 20px);
  padding: clamp(10px, 1.4vw, 16px) clamp(16px, 2vw, 24px);
  border-radius: var(--r-pill, 99px);
  background: var(--card, #fff); box-shadow: var(--shadow-4);
  font: 800 clamp(15px, 1.8vw, 20px)/1 'Fredoka', system-ui, sans-serif; color: var(--ink);
}
.sticker-capstone img { width: clamp(40px, 5vw, 60px); height: clamp(40px, 5vw, 60px); }
.sticker-capstone.locked { opacity: .6; }
.sticker-capstone.locked img { filter: grayscale(1) opacity(0.4); }

/* Sticker reward shown on the MODULE DONE cheer screen. */
.cheer-sticker {
  display: flex; flex-direction: column; align-items: center; gap: 6px; margin-top: 14px;
}
.cheer-sticker img { width: clamp(64px, 9vw, 96px); height: clamp(64px, 9vw, 96px); }
.cheer-sticker span { font: 800 clamp(15px, 1.8vw, 20px)/1 'Fredoka', system-ui, sans-serif; color: var(--green, #5bcb7a); }
.cheer-sticker.is-capstone span { color: var(--purple, #9b6dd6); }


/* ============================================================================
   MIGRATED 2026-05-30 from v2/index.html inline <style> — LESSON-card rules
   (.stage > .ak-card box, vocab grid, content-grid pills, tap-bob). These win
   over the base curriculum rules by specificity (.stage prefix).
   ============================================================================ */
/* ----- Balance quiz / match text pills against image cells -----
   The match quiz pairs word pills (auto-sized by text) with image
   placeholders (sized by --ak-img). Without this rule, pills look
   much smaller than the emojis. Give the .ak-grid-txt cell a min
   height matching the image-slot so the visual weight matches. */
.stage .ak-content-grid .ak-grid-txt {
  min-height: calc(var(--ak-img) * 0.75);
  display: flex;
  align-items: center;
}
.stage .ak-content-grid .ak-grid-txt .ak-word-slot {
  font-size: calc(var(--ak-pill-font) * 1.15);
  padding: 12px 18px;
}
/* Tray pills (sentence-quiz draggables) match the middle-row pill scale —
   same 1.15× font + same padding — so the only visible difference between
   tray and middle is the outlined treatment (light bg + 3px border) from
   `.ak-pill-drag` in curriculum/styles.css. Without this rule the tray
   reads ~15% smaller because the rule above only scopes to .ak-content-grid
   and the tray sits in .ak-tray. */
.stage .ak-tray .ak-pill-drag {
  font-size: calc(var(--ak-pill-font) * 1.15);
  padding: 12px 18px;
}

/* ----- Final-read support emojis — same size as other stages ----- */
/* The renderer marks the bottom-row images as `.is-small` on the final
   "all words flipped" stage. Override that shrink so the final stage
   matches the other stages visually. Targets only `.ak-grid-img.is-small`
   — that's the final-stage support-emoji flag, not a size hint from the
   catalog data. */
.stage .ak-grid-img.is-small {
  width:  var(--ak-img);
  height: var(--ak-img);
}

/* ----- Tap feedback ----- */
/* Sideways wiggle (horizontal only, no scale) so a tapped emoji never grows
   past a row's top OR bottom edge — safe even in tight lineups like the
   answer echo / bottom rows. A quick left-right-settle springs back to rest. */
.ak-tap-bob { animation: ak-tap-bob 360ms ease-in-out; }
@keyframes ak-tap-bob {
  0%, 100% { transform: translateX(0);    }
  30%      { transform: translateX(-6px); }
  65%      { transform: translateX(6px);  }
}

/* ----- Stage container -----
   The base `.stage` rule now lives ONLY in styles.css (consolidated
   2026-05-29) — it was previously split here too, and this inline copy's
   `height:100vh` silently overrode styles.css's `inset` bottom. Removed to
   end the cross-file split. Lesson-specific descendant tweaks below
   (`.stage > .ak-card`, vocab grid, compact NEXT/BACK/HEAR) stay here. */
.stage > .ak-card {
  /* Shared content-column width — see styles.css :root. */
  max-width: var(--ak-content-w);
  width: 100%;
  /* Consistent card size across all exercise cards (splash, vocab,
     present). Min-height keeps short cards from looking shrunken; quiz
     and match cards (with tray below) naturally grow beyond this. */
  min-height: clamp(360px, 52vh, 540px);
  max-height: 100%;
  box-sizing: border-box;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  justify-content: center;
  /* Anchor for the in-card triumph toast positioned at card bottom. */
  position: relative;
}
/* M4 question-form cards stack three sections (question grid → SHOW
   ANSWER → answer reveal). The default 540px max-height + center-align
   clips them — anchor to the top and allow taller cards. The answer
   reveal is reserved-space but visibility-hidden until pressed, so the
   card needs that full height from card-enter. flex-shrink:0 on the
   children keeps the question grid from collapsing under the answer
   reveal's reserved height (default flex-shrink:1 distributes overflow). */
.stage > .ak-card.ak-question-card {
  max-height: none;
  justify-content: flex-start;
  overflow: visible;
}
.stage > .ak-card.ak-question-card > * {
  flex-shrink: 0;
}
/* Vocab grid — slightly bigger than the tiny first pass, slightly smaller
   than the full-stretch pass. ~5% step toward "right size". */
.stage .ak-card .ak-vocab-grid {
  max-height: 100%;
  overflow: hidden;
  gap: clamp(12px, 1.6vw, 24px);
}
.stage .ak-card .ak-vocab-grid .ak-image-slot {
  width:  clamp(50px, 6.3vw, 90px);
  height: clamp(50px, 6.3vw, 90px);
}
.stage .ak-card .ak-vocab-grid .ak-word-slot {
  font-size: clamp(12px, 1.55vw, 19px);
  padding: 6px 12px;
}
.stage .ak-card .ak-vocab-grid .ak-vocab-cell { gap: 7px; }

/* Sparse vocab (1–4 new words) — DOUBLE the cell size so a handful of
   items doesn't look lost in a near-empty card. Applied via the
   `.is-sparse` class added by renderVocab when cells.length <= 4. */
.stage .ak-card .ak-vocab-grid.is-sparse .ak-image-slot {
  width:  clamp(100px, 12.6vw, 180px);
  height: clamp(100px, 12.6vw, 180px);
}
.stage .ak-card .ak-vocab-grid.is-sparse .ak-word-slot {
  font-size: clamp(24px, 3.1vw, 38px);
  padding: 10px 22px;
}
.stage .ak-card .ak-vocab-grid.is-sparse .ak-vocab-cell { gap: 14px; }
.stage .ak-card .ak-vocab-grid.is-sparse {
  gap: clamp(24px, 3.2vw, 48px);
}

/* Dense vocab — FLEXBOX so a partial LAST row CENTRES (CSS grid left-aligns it,
   stranding an empty cell on odd counts: 7→4+3-left, 9→5+4-left). The per-row
   count stays the renderer's --cols (chooseGridColumns) — flexbox ONLY adds the
   centring, so tablet/desktop keep their natural column layout. ALL viewports
   (2026-05-31 user req). Sparse (1-4 words) keeps the grid above; the §2
   landscape block (v2/media.css, loaded later) overrides the basis for its
   2-row layout. */
.stage .ak-card .ak-vocab-grid:not(.is-sparse) {
  --ak-vgap: clamp(12px, 1.6vw, 24px);
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-content: center;
  gap: var(--ak-vgap);
}
.stage .ak-card .ak-vocab-grid:not(.is-sparse) > .ak-vocab-cell {
  /* basis = one --cols-th of the row minus the gap it consumes, so exactly
     --cols cells fit per row and the partial last row centres. min-width:0 keeps
     a long label (AEROPLANE…) from expanding the cell and breaking the count. */
  flex: 0 0 calc(100% / var(--cols, 4) - var(--ak-vgap));
  min-width: 0;
}
