/* ═══════════════════════════════════════════════════════════════════════
   dna.css — the shared design system.

   Loaded by every surface: the site, the projected wall, the player. It is
   the only place pigments, faces, and the qalam's hand are defined, so
   "the site and the games share a DNA" is a fact about the build rather
   than a claim in a deck. If a surface needs a colour that isn't here, the
   answer is almost always no.

   Derived from §10 of engineering-plan.md.
   ═══════════════════════════════════════════════════════════════════════ */

/* ═══════════════════════════════════════════════════════════════════════
   THE FACES — self-hosted, block-subsetted, CI-gated.

   Built by tools/font-subset/subset.py and verified by golden.py, which shapes
   58 fixtures through every subset and asserts HarfBuzz produces byte-identical
   glyphs and positions to the upstream font. §2 demanded exactly this, because
   the failure mode ships with zero errors, zero warnings and no tofu.

   Measured, site.html first paint:

       Google Fonts CDN   284.2 KB
       these subsets      221.7 KB     -22%

   The saving is NOT from subsetting. Google already block-subsets Arabic and
   splits by unicode-range — §2 says so ("Google Fonts refuses to
   character-subset Arabic; it ships the whole block as one unit"). Doing it
   again, worse, first came out 382 KB — LARGER than the CDN. The wins were:

     · pinning axes nobody uses. Cairo ships a `slnt` axis and §10 has no
       italic: 114 KB -> 43 KB. Plex Sans ships `wdth`: 64 KB -> 34 KB.
     · obeying §10's own budget. Plex Arabic is static, so each weight is a
       file, and §10 says "the player ships 2 weights" — it was shipping three.
       .key-answer moved 500 -> 600 to make that true.

   Still 2.6x over §10's 86 KB. The remainder is Plex Arabic's two static
   weights, and closing it means changing the face, not the pipeline.

   What self-hosting buys beyond bytes: §13's offline PWA promise, no
   third-party request on a filtered school network, and no CDN between a class
   and its lesson.
   ═══════════════════════════════════════════════════════════════════════ */
@font-face {
  font-family: 'Reem Kufi';
  src: url('fonts/ReemKufi.subset.woff2') format('woff2-variations'),
       url('fonts/ReemKufi.subset.woff2') format('woff2');
  font-weight: 400 700;   /* variable — the extra weights are free */
  font-display: swap;
}
@font-face {
  font-family: 'Cairo';
  src: url('fonts/Cairo.subset.woff2') format('woff2-variations'),
       url('fonts/Cairo.subset.woff2') format('woff2');
  font-weight: 400 700;   /* variable; slnt pinned out */
  font-display: swap;
}
@font-face {
  font-family: 'IBM Plex Sans Arabic';
  src: url('fonts/IBMPlexSansArabic-Regular.subset.woff2') format('woff2');
  font-weight: 400;
  font-display: swap;
}
@font-face {
  font-family: 'IBM Plex Sans Arabic';
  src: url('fonts/IBMPlexSansArabic-SemiBold.subset.woff2') format('woff2');
  font-weight: 600;
  font-display: swap;
}
@font-face {
  font-family: 'IBM Plex Sans';
  src: url('fonts/IBMPlexSans.subset.woff2') format('woff2-variations'),
       url('fonts/IBMPlexSans.subset.woff2') format('woff2');
  font-weight: 400 600;
  font-display: swap;
}
/* Amiri is NOT declared here. §10 calls it opt-in for Arabic-language subjects,
   and at 119 KB per weight it is the most expensive face in the system — a maths
   game must never pay for a Naskh it renders no glyph of. Qalam.ensureFace('amiri')
   injects fonts/amiri.css when the first vocalised question renders. */

/* ── Pigments ──────────────────────────────────────────────────────────
   Manuscript illumination, not edtech candy. Separated in lightness, not
   only hue, so they survive greyscale, projector washout, and the ~8% of
   boys with red-green colour blindness. */
:root {
  --ink:       #14110F;  /* carbon ink   */
  --lapis:     #1B3A8C;  /* ultramarine  — أ */
  --vermilion: #D6402B;  /* cinnabar     — ب, and the corrector's mark */
  --saffron:   #E8A621;  /* orpiment     — ج, the clock */
  --verdigris: #1F8A70;  /* copper green — د */
  --gypsum:    #EFF1F4;  /* ground chalk — cool, never cream */

  --face-display: 'Reem Kufi', sans-serif;
  --face-content: 'IBM Plex Sans Arabic', 'IBM Plex Sans', system-ui, sans-serif;
  --face-naskh:   'Amiri', serif;
  --face-chrome:  'Cairo', system-ui, sans-serif;
}

/* A cheap projector has no black — it only adds light, so anything dark
   reads as whatever is already on the wall. The ground is a token with two
   variants because §10 names the washout risk and refuses to guess: it gets
   settled by a real projector in a real classroom, not in a design tool. */
[data-ground="dark"] {
  --ground: var(--ink);
  --on-ground: var(--gypsum);
  --mistara: rgba(239, 241, 244, .17);
  --spent:   rgba(239, 241, 244, .07);
  /* ── وهج ──
     The product is named after emitted light, and this is the only place
     the system spends it. A projector adds light; the kashida is the
     brightest thing it adds, and it is the one element that IS light rather
     than merely being coloured. So the clock glows and nothing else does.

     A glow on every tile would be a neon dark-mode page — the exact
     generated-looking default this palette was built to avoid. One element,
     one bloom, and it happens to be the signature. */
  --wahaj: drop-shadow(0 0 16px rgba(232, 166, 33, .40));
}
[data-ground="light"] {
  --ground: var(--gypsum);
  --on-ground: var(--ink);
  --mistara: rgba(20, 17, 15, .22);
  --spent:   rgba(20, 17, 15, .08);
  /* No glow in a lit room. Light on light is invisible — that's physics,
     not styling, and it's why the ground toggle changes what the design
     MEANS rather than just inverting it. */
  --wahaj: none;
}

*, *::before, *::after { box-sizing: border-box; }

/* The UA stylesheet says [hidden] { display: none } — and ANY author `display`
   rule beats it, because they tie on specificity and the author sheet wins.
   `.wall { display: grid }` therefore silently outranked it: hiding the wall did
   nothing and the honour board rendered straight on top of the answer tiles.
   Nothing errored; two screens just occupied one grid cell.
   Every design system needs this line, and it is the reason why. */
[hidden] { display: none !important; }

/* Grid and flex items default to min-width:auto, so a track refuses to
   shrink past its longest word and an RTL layout silently overflows off the
   edge. Opt the shared components out up front. */
:where(.wall, .wall-body, .keys, .key-slot, .key, .key-answer, .device, .pad) {
  min-width: 0;
}

/* ── The pigment set ───────────────────────────────────────────────────
   Four pigments, and nothing here says what they mean. That's deliberate:
   the set is DNA, the mapping is the mechanic's business. A quiz keys them
   to the abjad answers (أ ب ج د); a matching game keys them to the pairs.
   Keying colour off [data-abjad] would mean a template without A/B/C/D
   answers has no palette — which is how a "design system" quietly becomes
   one screen's stylesheet.

   --pigment is the element's identity and never changes. --fill is its
   current state. Keeping them apart is what stops the ghost and restore
   rules from fighting over a single custom property. */
[data-pigment="lapis"]     { --pigment: var(--lapis);     --on-pigment: var(--gypsum); --rim: color-mix(in oklab, var(--lapis) 62%, var(--gypsum)); }
[data-pigment="vermilion"] { --pigment: var(--vermilion); --on-pigment: var(--gypsum); --rim: color-mix(in oklab, var(--vermilion) 62%, var(--gypsum)); }
[data-pigment="saffron"]   { --pigment: var(--saffron);   --on-pigment: var(--ink);    --rim: color-mix(in oklab, var(--saffron) 58%, var(--ink)); }
[data-pigment="verdigris"] { --pigment: var(--verdigris); --on-pigment: var(--gypsum); --rim: color-mix(in oklab, var(--verdigris) 62%, var(--gypsum)); }

/* The quiz's mapping of that set. Kahoot keys its answers with a triangle,
   diamond, circle and square — shapes that encode nothing, a colourblindness
   workaround wearing a costume. أ ب ج د is how Arabic exams have been keyed
   for centuries: the subject's own vernacular, a redundant channel for
   colour, and a thing that cannot exist on an LTR product. */

/* ── The mistara ───────────────────────────────────────────────────────
   The guideline a scribe rules across a page before writing on it. It is
   the one structural device both surfaces share: on the wall it's what the
   kashida depletes off, and on the site it's what the writing sits on.

   It is deliberately NOT a broadsheet hairline rule. A rule divides things;
   a mistara is a baseline that content rests on and overruns. If it ever
   starts fencing sections off from each other, it has become the newspaper
   default and should be cut. */
.mistara {
  border: 0;
  block-size: 1.5px;
  background: var(--mistara);
  margin: 0;
}

/* ═══════════════════════════════════════════════════════════════════════
   THE WALL
   Thirty kids, eight metres, one projector. Every size below is set for the
   back row, not for the monitor it's being designed on.

   Sized in cqi, not vw: the wall is a component, and it is mounted both
   full-screen (the classroom) and inside a hero box (the site). Querying
   its own container instead of the viewport is what lets one component do
   both without a second stylesheet.
   ═══════════════════════════════════════════════════════════════════════ */
.wall-frame { container-type: inline-size; }

.wall {
  block-size: 100%;
  display: grid;
  grid-template-rows: auto 1fr auto;
  padding: clamp(12px, 3.2cqi, 56px);
  gap: clamp(8px, 2cqi, 32px);
  background: var(--ground);
  color: var(--on-ground);
  transition: background 420ms ease, color 420ms ease;
}

.wall-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 24px;
}

/* Reem Kufi: modular, grid-built, holds its form through a washed-out
   projection. Its squared construction reads simultaneously as Arabic
   calligraphy and as game tiles — the one place where the subject's script
   and the product's medium are the same shape. Never body text. */
.wall-counter {
  font-family: var(--face-display);
  font-weight: 500;
  font-size: clamp(.8rem, 2.4cqi, 2.5rem);
  letter-spacing: .04em;
  font-variant-numeric: tabular-nums;
  opacity: .62;
}
.wall-counter b { font-weight: 500; opacity: 1; }
.wall-counter i { font-style: normal; margin: 0 .35em; opacity: .5; }

.wall-subject {
  font-family: var(--face-display);
  font-size: clamp(.55rem, 1.3cqi, 1.4rem);
  letter-spacing: .1em;
  opacity: .45;
}

.wall-body {
  display: flex;
  flex-direction: column;
  justify-content: center;
  /* The clock belongs to the question, not to the room. */
  gap: clamp(2px, 1cqi, 16px);
  min-block-size: 0;
}

/* IBM Plex Sans Arabic + IBM Plex Sans — not a pairing, a superfamily. One
   design, two scripts, matching metrics. `ما هو ناتج ٧ × ٨ ؟` is a mixed
   Arabic/digit run and `H₂O` is a mixed Arabic/Latin one — exactly the bidi
   hotspots §8 flags. One superfamily instead of two families removes a
   whole class of typographic fracture. 35/35 extended marks. */
.wall-question {
  margin: 0;
  font-family: var(--face-content);
  font-weight: 600;
  font-size: clamp(.95rem, 4.6cqi, 4.75rem);
  line-height: 1.42;
  letter-spacing: -.005em;
  text-align: center;
  text-wrap: balance;
  max-inline-size: 22ch;
  margin-inline: auto;
}

/* Amiri — opt-in for Arabic-language subjects. True Naskh, built for
   vocalised typesetting. This is pedagogy, not taste: children are taught
   to read Naskh, so setting a grammar question in a geometric sans teaches
   the wrong letterforms. Tashkeel needs the leading. */
.wall-question[data-face="amiri"] {
  font-family: var(--face-naskh);
  font-weight: 400;
  font-size: clamp(1rem, 5cqi, 5.25rem);
  line-height: 1.78;
}

/* ── The signature: the kashida timer ──────────────────────────────────
   Arabic has one device Latin has no equivalent for — the kashida, the
   calligraphic elongation that stretches a letter's joining line. It is
   pure Arabic. It has no LTR counterpart to flip.

   The timer is a kashida. Elongation IS duration: the form and the function
   are the same idea, which is the only honest reason to use an ornament as
   a mechanism. It earns the signature slot because it appears in every live
   question, on both surfaces, at the exact moment thirty people are looking
   at it — and a competitor cannot copy it without committing to Arabic.

   Clipped, not visible: a nib terminal drawn past the box edge widens the
   scroll area and shoves the whole RTL page sideways. */
.kashida {
  display: block;
  inline-size: min(100%, 1180px);
  block-size: clamp(18px, 4.75cqi, 56px);
  margin-inline: auto;
  overflow: hidden;
}
.kashida-rule   { stroke: var(--mistara); stroke-width: 1.5; }
.kashida-stroke { fill: var(--saffron); filter: var(--wahaj); }
.wall[data-state="revealed"] .kashida-stroke { opacity: 0; transition: opacity 320ms ease; }
.wall[data-state="revealed"] .kashida-rule   { opacity: .5; transition: opacity 320ms ease; }

/* ── The answer field ──────────────────────────────────────────────────
   أ sits rightmost. dir="rtl" puts it there for free, and that single
   ordering is the thing a flipped LTR theme gets wrong — every Arab teacher
   in the room clocks it instantly.

   minmax(0,…) not 1fr: a bare 1fr means minmax(auto,1fr), whose auto
   minimum floors the track at the tiles' min-content width. */
.keys {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: clamp(5px, 1.1cqi, 20px);
}

/* The slot reserves the strip the corrector's mark lives in, so the mark
   sits on the ground rather than on the tile. The tile lifts away from it;
   the mark never moves. */
.key-slot {
  position: relative;
  display: grid;
  padding-block-end: clamp(10px, 2cqi, 32px);
}

.key {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  gap: clamp(6px, 1.4cqi, 28px);
  padding: clamp(8px, 1.6cqi, 30px) clamp(9px, 1.6cqi, 30px);
  min-block-size: clamp(44px, 9cqi, 156px);
  border: 0;
  border-radius: 3px;
  background: var(--fill, var(--pigment));
  color: var(--label, var(--on-pigment));
  font: inherit;
  text-align: start;
  /* A ruled rim, like the ruling around an illuminated panel. Lapis on ink
     is only ~2:1 — the rim is what keeps the block readable on a dark wall,
     and it earns its keep in both grounds. */
  box-shadow: inset 0 0 0 3px var(--edge, var(--rim));
  transition:
    background 480ms cubic-bezier(.22, 1, .36, 1),
    color 480ms ease,
    box-shadow 480ms ease,
    transform 520ms cubic-bezier(.22, 1, .36, 1),
    filter 480ms ease;
}

/* Solo (self-paced) keys are the control the student taps — so they invite the
   tap: a pointer, a lift on hover, a visible press, and a keyboard focus ring.
   On the projected wall the key has no [data-index] and stays inert. */
.key[data-index] { cursor: pointer; }
@media (hover: hover) {
  .key[data-index]:hover { transform: translateY(-2px); filter: brightness(1.06); }
}
.key[data-index]:active { transform: translateY(0) scale(.99); }
.key[data-index]:focus-visible { outline: 3px solid var(--on-ground); outline-offset: 3px; }
.key[aria-disabled="true"] { cursor: default; }

.key-abjad {
  font-family: var(--face-display);
  font-weight: 600;
  font-size: clamp(.85rem, 2.5cqi, 2.9rem);
  line-height: 1;
  inline-size: 1.6em;
  text-align: center;
  opacity: .78;
}

.key-answer {
  font-family: var(--face-content);
  /* 600, not 500: Plex Arabic is static, so a third weight is a third 59 KB file,
     and §10 budgets two. On a wall at eight metres the heavier answer is better
     anyway — the budget and the back row agree. */
  font-weight: 600;
  font-size: clamp(.65rem, 1.95cqi, 2.35rem);
  line-height: 1.35;
  letter-spacing: -.005em;
}
/* Latin runs stay in the superfamily's Latin sibling rather than falling
   back to a system face with different proportions. */
.key-answer[dir="ltr"] { font-family: 'IBM Plex Sans', system-ui, sans-serif; display: inline-block; }

/* The face travels with the content, not just the question — a child reads
   the answers too. This is what the manifest's diacritics face preference
   has to carry. */
.keys[data-face="amiri"] .key-answer {
  font-family: var(--face-naskh);
  font-weight: 700;
  font-size: clamp(.7rem, 2.05cqi, 2.5rem);
  line-height: 1.6;
}

/* ── Lock → reveal ─────────────────────────────────────────────────────
   One orchestrated moment: the kashida finishes, answers lock, wrong tiles
   recede to ink, the correct tile lifts and takes the mark. Nothing else on
   the wall animates. Scattered micro-animation is one of the loudest tells
   of generated design. */
.wall[data-state="revealed"] .key-slot[data-correct="false"] .key {
  --fill: var(--ground);
  --edge: var(--spent);
  --label: var(--spent);
  transform: scale(.965);
}
.wall[data-state="revealed"] .key-slot[data-correct="true"] .key {
  transform: translateY(clamp(-26px, -2.2cqi, -10px));
  filter: drop-shadow(0 20px 44px rgba(0, 0, 0, .5));
}

/* Rubrication — the corrector's mark, in cinnabar, in the same hand as the
   timer, drawn right-to-left because that is the direction Arabic is
   written.

   §10 cut vermilion tashkeel on real technical grounds: inline element
   boundaries break the shaping cluster, so a combining mark would lose its
   GPOS anchoring. A tile has no shaping cluster, so the idea returns where
   it is actually buildable. It sits on the ground under the lifted tile,
   not on the tile — "correct = vermilion" would vanish on ب, whose key
   colour is already vermilion. A corrector marks the margin, not the text.

   inline-size is not optional: an <svg> with no width attribute is a
   replaced element with a 300x150 intrinsic default, so `inset-inline: 0`
   alone leaves it 300px wide and over-constrained — and RTL resolves
   over-constraint by dropping the start offset, hanging it off the left
   edge and widening the entire page. Silently. */
.rubric {
  position: absolute;
  inline-size: 100%;
  inset-inline-start: 0;
  inset-block-end: 0;
  block-size: 14px;
  overflow: hidden;
  pointer-events: none;
  clip-path: inset(0 0 0 100%);
}
.rubric-stroke { fill: var(--vermilion); }
.wall[data-state="revealed"] .key-slot[data-correct="true"] .rubric {
  clip-path: inset(0 0 0 0);
  transition: clip-path 560ms cubic-bezier(.32, .72, .3, 1) 260ms;
}

/* ═══════════════════════════════════════════════════════════════════════
   THE PHONE — input only. The question is on the wall, so heads stay up.
   It also ships almost no payload per question: the low-bandwidth promise
   falling out of the interaction model rather than being retrofitted as an
   optimisation.
   ═══════════════════════════════════════════════════════════════════════ */
.device {
  inline-size: 236px;
  border-radius: 26px;
  padding: 10px;
  background: color-mix(in oklab, var(--ground) 82%, var(--on-ground));
  box-shadow: 0 26px 60px rgba(0, 0, 0, .5), inset 0 0 0 1px var(--spent);
}
.device-screen {
  border-radius: 18px;
  background: var(--ground);
  box-shadow: inset 0 0 0 1px var(--spent);
  padding: 16px 14px 18px;
  display: grid;
  gap: 12px;
}
.device-head { display: flex; align-items: center; justify-content: space-between; }
.device-counter {
  font-family: var(--face-display);
  font-size: 1.05rem;
  font-variant-numeric: tabular-nums;
  opacity: .6;
}
.device-name { font-family: var(--face-chrome); font-size: .72rem; opacity: .42; }
.device-kashida { display: block; inline-size: 100%; block-size: 14px; overflow: hidden; }

.pad { display: grid; gap: 7px; }
.pad-key {
  border: 0;
  border-radius: 3px;
  padding: 13px;
  background: var(--fill, var(--pigment));
  color: var(--label, var(--on-pigment));
  box-shadow: inset 0 0 0 2px var(--edge, var(--rim));
  font-family: var(--face-display);
  font-size: 1.5rem;
  font-weight: 600;
  line-height: 1;
  cursor: pointer;
  transition: background 420ms ease, color 420ms ease, box-shadow 420ms ease,
              transform 260ms cubic-bezier(.22, 1, .36, 1);
}
.pad-key:active { transform: scale(.97); }

/* Locked: every key the student didn't pick goes quiet. The correct one
   stays quiet too — the phone must not leak the answer before the wall
   reveals it. */
.pad[data-picked="true"] .pad-key[aria-pressed="false"] {
  --fill: var(--ground);
  --edge: var(--spent);
  --label: var(--spent);
}
.pad[data-picked="true"] .pad-key[aria-pressed="true"] {
  box-shadow: inset 0 0 0 2px var(--edge, var(--rim)), 0 0 0 3px var(--on-ground);
}
/* Revealed: the correct key comes back up, picked or not. Same specificity
   as the ghost rule and declared after it, so it wins on order rather than
   on an escalating selector. */
.pad[data-state="revealed"] .pad-key[data-correct="true"] {
  --fill: var(--pigment);
  --edge: var(--rim);
  --label: var(--on-pigment);
}
.device-note {
  font-family: var(--face-chrome);
  font-size: .78rem;
  text-align: center;
  min-block-size: 1.2em;
  opacity: .5;
}

/* ═══════════════════════════════════════════════════════════════════════
   THE DICTATION SURFACE — type-answer
   Everything above this line is one mechanic: four abjad keys, a wall, a pad.
   That is `stimulus → MC`, and it is one of the ~10 mechanics the catalogue
   collapses to (games-catalog.md §2.5). `type-answer` is another, it has no
   keys to key, and it is the mechanic the whole differentiator lives on — so
   its components belong here rather than in a template's local stylesheet.
   The rule at the top of this file is what forces that: if a surface needs a
   colour that isn't here, the answer is almost always no. The corollary is
   that the surface comes here instead.

   Unlike the wall, this is read at arm's length on a phone or a laptop, so
   nothing below is sized for the back row. It still takes its ground from
   the same two-variant token, because a dictation round hosted live is
   projected like anything else.
   ═══════════════════════════════════════════════════════════════════════ */
.imla {
  display: grid;
  gap: clamp(10px, 2cqi, 18px);
  padding: clamp(16px, 3cqi, 28px);
  background: var(--ground);
  color: var(--on-ground);
  border-radius: 4px;
  box-shadow: inset 0 0 0 1px var(--spent);
}

.imla-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 20px;
}
.imla-counter {
  font-family: var(--face-display);
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  letter-spacing: .04em;
  opacity: .62;
}
.imla-score {
  font-family: var(--face-display);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
/* A word coming back for a second pass is a fact about the session, not a
   reward — saffron, the clock's pigment, because that is what it is: time
   being spent again on the same word. */
.imla-again {
  font-family: var(--face-chrome);
  font-size: .7rem;
  font-weight: 600;
  color: var(--saffron);
  border: 1px solid currentColor;
  border-radius: 999px;
  padding: 0 .6em;
  margin-inline-start: .5em;
}

.imla-instructions {
  font-family: var(--face-chrome);
  font-size: .9rem;
  opacity: .62;
  margin: 0;
}

/* Lapis is the precious one and gets spent once per surface. On the wall
   that is answer أ. Here it is the listen button, because in a dictation
   game the audio IS the question — every other control is downstream of it. */
.imla-listen {
  font-family: var(--face-chrome);
  font-size: 1rem;
  font-weight: 600;
  border: 0;
  border-radius: 3px;
  padding: .75em 1.6em;
  background: var(--lapis);
  color: var(--gypsum);
  box-shadow: inset 0 0 0 2px color-mix(in oklab, var(--lapis) 62%, var(--gypsum));
  cursor: pointer;
}
.imla-listen:disabled {
  background: transparent;
  color: var(--spent);
  box-shadow: inset 0 0 0 2px var(--spent);
  cursor: not-allowed;
}
.imla-controls { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.imla-replays { font-family: var(--face-chrome); font-size: .78rem; opacity: .5; }
.imla-hint-btn {
  font-family: var(--face-chrome);
  font-size: .8rem;
  margin-inline-start: auto;
  background: none;
  color: inherit;
  border: 1px solid var(--spent);
  border-radius: 3px;
  padding: .5em 1.1em;
  cursor: pointer;
}
.imla-hint { font-family: var(--face-chrome); font-size: .86rem; opacity: .72; margin: 0; }

/* The student writes Arabic here, so it is set in the Naskh face for the
   same reason the wall's Amiri variant exists: children are taught to read
   and write Naskh, and a child checking their own spelling against a
   geometric sans is checking it against the wrong letterforms. This is the
   one field in the product where that matters most — it is literally a
   handwriting-to-type transcription. Tashkeel needs the leading. */
.imla-answer {
  inline-size: 100%;
  font-family: var(--face-naskh);
  font-size: clamp(1.5rem, 5cqi, 2.1rem);
  line-height: 1.75;
  padding: .35em .5em;
  border: 0;
  border-radius: 3px;
  background: color-mix(in oklab, var(--ground) 88%, var(--on-ground));
  color: var(--on-ground);
  box-shadow: inset 0 0 0 2px var(--spent);
}
.imla-answer:focus { outline: 3px solid var(--saffron); outline-offset: 2px; }
[data-ground="light"] .imla-answer:focus { outline-color: var(--lapis); }
.imla-answer:disabled { opacity: .55; }

.imla-submit {
  font-family: var(--face-chrome);
  font-size: 1rem;
  font-weight: 600;
  border: 0;
  border-radius: 3px;
  padding: .7em 1.8em;
  background: var(--on-ground);
  color: var(--ground);
  cursor: pointer;
}
.imla-submit:disabled { background: var(--spent); color: var(--ground); cursor: not-allowed; }

/* ── Rubrication — the corrector's marks ───────────────────────────────
   §10 cut vermilion tashkeel on real technical grounds: inline element
   boundaries break the shaping cluster, so a combining mark loses its GPOS
   anchoring to its base glyph. The wall's rubric brought the idea back on a
   tile, which has no shaping cluster. This brings it back where it was
   always going: a scribe's correction.

   Rubrication is what cinnabar was FOR — a corrector working over a copyist's
   page marked the error in red in the margin, never in the text. The imlaʾ
   error classifier is a corrector. Each chip below is one marginal mark, and
   it names the rule rather than the letter, because "you broke the ة/ه rule"
   is what a teacher says and "wrong" is what a machine says.

   The marks sit BESIDE the writing, never on it — same reasoning as the
   wall's rubric sitting on the ground under the lifted tile rather than on
   the tile itself. */
.imla-verdict {
  display: flex;
  align-items: baseline;
  gap: .8em;
  font-family: var(--face-chrome);
  font-weight: 600;
  margin: 0;
}
.imla-target {
  font-family: var(--face-naskh);
  font-size: 1.6rem;
  font-weight: 700;
}
.imla-verdict[data-correct="true"]  { color: var(--verdigris); }
.imla-verdict[data-correct="false"] { color: var(--vermilion); }

.imla-marks { list-style: none; margin: 0; padding: 0; display: grid; gap: .55em; }
.imla-mark {
  display: flex;
  gap: .7em;
  align-items: baseline;
  flex-wrap: wrap;
  /* The margin the corrector writes in. */
  border-inline-start: 2px solid var(--vermilion);
  padding-inline-start: .7em;
}
.imla-mark-rule {
  font-family: var(--face-chrome);
  font-size: .72rem;
  font-weight: 700;
  color: var(--vermilion);
  white-space: nowrap;
}
/* The fix is a sentence a child reads, and it quotes Arabic words inside it,
   so it is set in the reading face, not the chrome face. */
.imla-mark-fix {
  font-family: var(--face-naskh);
  font-size: 1.05rem;
  line-height: 1.7;
  flex: 1 1 14ch;
}

.imla-waiting { font-family: var(--face-chrome); font-size: .84rem; opacity: .5; margin: 0; }
.imla-final {
  font-family: var(--face-display);
  font-size: 3rem;
  font-variant-numeric: tabular-nums;
  text-align: center;
  color: var(--lapis);
  margin: 0;
}
[data-ground="dark"] .imla-final { color: var(--saffron); }

/* The audio gap, shown rather than hidden. Not a pigment moment — it is a
   note to the teacher that the device cannot do the thing the game needs,
   and it should read as plainly as that. */
.imla-noaudio {
  font-family: var(--face-chrome);
  font-size: .82rem;
  border: 1px solid var(--saffron);
  border-radius: 3px;
  padding: .7em .9em;
  margin: 0;
}
.imla-noaudio b { display: block; font-weight: 400; opacity: .6; margin-block-start: .4em; }

/* ═══════════════════════════════════════════════════════════════════════
   THE PAIRS — a second mechanic, and the test of whether any of this is
   actually a system.

   Matching has no abjad keys: nothing here is "answer أ". If the DNA were
   really "four coloured tiles with Arabic letters on them", this template
   would need its own design, and the catalogue would fracture the moment it
   grew past a quiz.

   It doesn't. Every decision below was already made:
     · the pigment SET keys the pairs instead of the answers
     · the kashida is still the clock
     · the mistara is still the ruling
     · rubrication is still what "correct" looks like
     · lock → recede → lift → mark is still the reveal
   Zero new colours, zero new faces, zero new gestures. That is the whole
   claim of §3's template contract, cashed out as CSS: ship the mechanic,
   inherit the design.
   ═══════════════════════════════════════════════════════════════════════ */
.pairs {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: clamp(5px, 1.1cqi, 18px);
  align-content: center;
}
.pair { position: relative; display: grid; padding-block-end: clamp(8px, 1.7cqi, 26px); }

.pair-tile {
  display: grid;
  place-items: center;
  text-align: center;
  padding: clamp(6px, 1.4cqi, 24px) clamp(5px, 1.1cqi, 16px);
  min-block-size: clamp(40px, 7.5cqi, 124px);
  border: 0;
  border-radius: 3px;
  /* Unmatched tiles are unpainted — a ruled panel waiting for pigment. The
     game IS the act of illuminating them. */
  background: var(--fill, transparent);
  color: var(--label, var(--on-ground));
  box-shadow: inset 0 0 0 3px var(--edge, var(--mistara));
  font-family: var(--face-content);
  font-weight: 600;   /* see .key-answer — §10's two-weight budget */
  /* The floor is doing real work. cqi is proportional, which is right for a
     wall — but a proportional 570px card renders 9px text, and a preview
     nobody can read is not a preview. The floor is the smallest size that's
     still legible; below that the layout should shrink, not the words. */
  font-size: clamp(.82rem, 1.6cqi, 1.85rem);
  line-height: 1.45;
  cursor: pointer;
  transition:
    background 420ms cubic-bezier(.22, 1, .36, 1),
    color 420ms ease,
    box-shadow 300ms ease,
    transform 460ms cubic-bezier(.22, 1, .36, 1);
}
.pairs[data-face="amiri"] .pair-tile { font-family: var(--face-naskh); font-weight: 700; line-height: 1.7; }

/* Picked: the ruling brightens. No new colour — the tile is simply being
   attended to. */
.pair-tile[aria-pressed="true"] { --edge: var(--on-ground); }

/* Wrong: recede to the ground. The same sentence the quiz's wrong tiles
   speak, so a child who has played one template already reads this one. */
.pair-tile[data-wrong="true"] { --edge: var(--spent); --label: var(--spent); }

/* Matched: the pair takes its pigment and lifts. */
.pair[data-matched="true"] .pair-tile {
  --fill: var(--pigment);
  --edge: var(--rim);
  --label: var(--on-pigment);
  transform: translateY(clamp(-14px, -1.4cqi, -6px));
  cursor: default;
}
.pair[data-matched="true"] .rubric {
  clip-path: inset(0 0 0 0);
  transition: clip-path 520ms cubic-bezier(.32, .72, .3, 1) 120ms;
}
/* Time out: whatever is still unpainted gets painted, so the wall always
   ends by showing the answer rather than by scolding. */
.pairs[data-state="revealed"] .pair[data-matched="false"] .pair-tile {
  --fill: var(--pigment);
  --edge: var(--rim);
  --label: var(--on-pigment);
  opacity: .55;
}

/* ── A rule the second template found ──────────────────────────────────
   This was Reem Kufi, and it was wrong — though not for the reason first
   claimed here. That comment said Reem Kufi piles tashkeel up over the
   letters. **Measured at 4x, it does not:** it positions the core marks
   correctly. That claim was asserted, not tested, which is the exact
   failure §2 exists to prevent, committed against §2's own rule. Corrected.

   The rule stands on the grounds that actually hold:

   1. **Pedagogy — §10's real argument.** Children are taught to read Naskh.
      Authored content is the text a child reads, so setting it in a
      geometric Kufi teaches the wrong letterforms. This is the load-bearing
      reason and it never depended on a rendering bug.
   2. **Unmeasured coverage.** §2 measured Plex and Noto Naskh at 35/35
      extended marks, Cairo at 1/35. **Reem Kufi is unmeasured.** Authored
      Arabic can carry extended marks; the system's own strings cannot,
      because the system writes them. Shipping authored text through an
      unmeasured face is the subsetting trap wearing a different hat.
   3. A display face is not a body face at any coverage.

   So: **Reem Kufi renders system marks only** — question numbers, abjad
   keys, the wordmark, counts. Everything a teacher typed goes to the
   superfamily, or to Amiri when the subject is Arabic. An instruction is a
   question's equivalent, so it takes the question's treatment.

   One template could hide this. Two couldn't. */
.pair-instruct {
  margin: 0;
  font-family: var(--face-content);
  font-weight: 600;
  font-size: clamp(.95rem, 2.3cqi, 2.2rem);
  line-height: 1.5;
  text-align: center;
  text-wrap: balance;
  max-inline-size: 26ch;
  margin-inline: auto;
  opacity: .92;
}
.pair-instruct[data-face="amiri"] {
  font-family: var(--face-naskh);
  font-weight: 700;
  line-height: 1.8;
}

/* ═══════════════════════════════════════════════════════════════════════
   THE SORT — a third mechanic, and the one where the Arabic is the RULE.

   sort-to-bins: a word appears, the student drops it in the right bin. The
   catalogue calls this the highest-leverage wave, because ONE surface is
   Sun-or-Moon, Ta-or-Ha, Alif-Maqsura and more — the only thing that changes
   is the item pool and which pure rule module grades it. There is no generic
   Wordwall template that can decide whether «ال» assimilates; this can,
   because a RuleModule tells it.

   Everything below is inherited, exactly as THE PAIRS argued: the pigment set
   keys the bins (not answers), the kashida is still the clock, rubrication is
   still "correct", the item recedes/lifts like every other tile. Zero new
   colours, zero new faces.

   The ONE new thing is the signature, and it is not a new gesture — it is the
   subject itself: on a correct sort, the bare word VOCALISES to prove the
   rule. الشمس becomes الشّمس, the shadda blooming on the ش exactly where the
   lām assimilated. The reveal is a whole word (§2/§10: per-letter spans break
   the shaping cluster), cross-faded. Form and function are the same thing —
   the same honesty the kashida earns its keep by.
   ═══════════════════════════════════════════════════════════════════════ */
.sort {
  block-size: 100%;
  display: grid;
  grid-template-rows: auto 1fr;
  padding: clamp(12px, 3.2cqi, 56px);
  gap: clamp(8px, 2cqi, 28px);
  background: var(--ground);
  color: var(--on-ground);
  transition: background 420ms ease, color 420ms ease;
}

.sort-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 24px;
}
.sort-counter {
  font-family: var(--face-display);
  font-weight: 500;
  font-size: clamp(.8rem, 2.4cqi, 2.3rem);
  letter-spacing: .04em;
  font-variant-numeric: tabular-nums;
  opacity: .62;
}
/* The streak is saffron because it is time-adjacent — a run held, like the
   clock. It is the one number a child chases. */
.sort-streak {
  font-family: var(--face-display);
  font-weight: 600;
  font-size: clamp(.8rem, 2.4cqi, 2.3rem);
  color: var(--saffron);
  filter: var(--wahaj);
  font-variant-numeric: tabular-nums;
}
.sort-streak[data-active="false"] { opacity: .3; filter: none; }

.sort-body {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: clamp(10px, 2.4cqi, 32px);
  min-block-size: 0;
}

.sort-prompt {
  font-family: var(--face-chrome);
  font-size: clamp(.62rem, 1.5cqi, 1.15rem);
  letter-spacing: .04em;
  opacity: .5;
  text-align: center;
  margin: 0;
}

/* The word under test. Naskh, because these are vocalised Arabic-language
   items and §10 is explicit that a child must read the letterforms they are
   taught. Two layers stacked in one grid cell: the bare skeleton, and the
   vocalised reveal that cross-fades over it on a correct answer. */
.sort-item {
  display: grid;
  place-items: center;
  font-family: var(--face-naskh);
  font-weight: 700;
  font-size: clamp(1.8rem, 8cqi, 7rem);
  line-height: 1.6;
  text-align: center;
  min-block-size: 1.6em;
}
.sort-item > * { grid-area: 1 / 1; }   /* stack the two layers */

.sort-bare { transition: opacity 300ms ease; }
.sort-reveal {
  opacity: 0;
  color: var(--saffron);
  filter: var(--wahaj);
  transition: opacity 460ms ease 120ms;
}
/* On reveal, the bare word fades and the vocalised one blooms in its place —
   the shadda appearing IS the assimilation. Only fires when the rule provided
   a reveal (data-revealed), so Ta-or-Ha and Alif-Maqsura, which have no
   phonological transform, simply keep the bare word and lean on the note. */
.sort[data-state="revealed"] .sort-item[data-has-reveal="true"] .sort-bare { opacity: 0; }
.sort[data-state="revealed"] .sort-item[data-has-reveal="true"] .sort-reveal { opacity: 1; }

.sort-kashida {
  display: block;
  inline-size: min(100%, 640px);
  block-size: clamp(12px, 3cqi, 34px);
  overflow: hidden;
}
.sort[data-state="revealed"] .kashida-stroke { opacity: 0; transition: opacity 300ms ease; }

/* The bins. repeat(N) is set inline by the template from the rule's bin count
   (2 for Sun/Moon and Alif-Maqsura, 3 for Ta/Ha). minmax(0,1fr) not 1fr, so a
   long bin label can't push the row off a phone. */
.sort-bins {
  display: grid;
  gap: clamp(6px, 1.6cqi, 22px);
  inline-size: min(100%, 760px);
}
.sort-bin {
  display: grid;
  place-items: center;
  min-block-size: clamp(48px, 9cqi, 128px);
  padding: clamp(8px, 1.8cqi, 24px);
  border: 0;
  border-radius: 3px;
  /* Unpainted until chosen, like an unmatched pair — a ruled panel waiting for
     pigment. The pigment itself is set by the template via [data-pigment]. */
  background: var(--fill, transparent);
  color: var(--label, var(--on-ground));
  box-shadow: inset 0 0 0 3px var(--edge, var(--mistara));
  font-family: var(--face-content);
  font-weight: 600;
  font-size: clamp(.85rem, 2.4cqi, 2.1rem);
  cursor: pointer;
  transition:
    background 420ms cubic-bezier(.22, 1, .36, 1),
    color 300ms ease,
    box-shadow 300ms ease,
    transform 420ms cubic-bezier(.22, 1, .36, 1);
}
.sort-bin:disabled { cursor: default; }

/* Hover/focus brightens the ruling — attended to, no new colour. */
.sort-bin:not(:disabled):hover,
.sort-bin:focus-visible { --edge: var(--on-ground); }

/* Revealed: the CORRECT bin takes its pigment and lifts; a wrong CHOSEN bin
   recedes to the ground. Same three sentences every other mechanic speaks. */
.sort[data-state="revealed"] .sort-bin[data-correct="true"] {
  --fill: var(--pigment);
  --edge: var(--rim);
  --label: var(--on-pigment);
  transform: translateY(clamp(-10px, -1.2cqi, -4px));
}
.sort[data-state="revealed"] .sort-bin[data-chosen="true"][data-correct="false"] {
  --edge: var(--spent);
  --label: var(--spent);
  transform: scale(.97);
}

/* The rule, named. This is the §0 differentiator on screen: not "wrong" but
   WHICH rule — «ش حرف شمسي، فاللام لا تُنطق». Cinnabar margin-mark, the same
   corrector's hand as the dictation surface, because that is what it is: a
   correction. role=status/aria-live on the element, so it is spoken too. */
.sort-note {
  min-block-size: 3.2em;
  max-inline-size: 42ch;
  font-family: var(--face-content);
  font-size: clamp(.72rem, 1.7cqi, 1.15rem);
  line-height: 1.6;
  text-align: start;
  border-inline-start: 3px solid transparent;
  padding-inline-start: .8em;
  opacity: 0;
  transition: opacity 300ms ease 200ms;
}
.sort[data-state="revealed"] .sort-note { opacity: 1; }
.sort-note[data-correct="true"]  { border-inline-start-color: var(--verdigris); }
.sort-note[data-correct="false"] { border-inline-start-color: var(--vermilion); }

/* Reduced motion: the reveal must not depend on a cross-fade a child can't
   see. Everything snaps; the information (which bin, which word) is in the
   final state, not the transition. */
@media (prefers-reduced-motion: reduce) {
  .sort-bare, .sort-reveal, .sort-bin, .sort-note { transition-duration: 1ms; }
}

/* ═══════════════════════════════════════════════════════════════════════
   THE ANSWER BAND — the teacher's surface.

   Every edtech dashboard opens with a big number, a small label and a
   gradient. A teacher cannot do anything with 73%. She can do something
   with "fifteen of your children think plants absorb oxygen."

   So the band is the class's votes, drawn as the wall's own four tiles,
   each one as wide as the number of children who chose it — in abjad order,
   أ rightmost, exactly as the class saw them. The teacher's mental model is
   the wall, so the report is the wall, weighted.

   **It has no legend, and that is the point.** A generic dashboard needs one
   because its colours are arbitrary. Here the teacher already knows أ is
   lapis, because thirty children just shouted it at her. The design earns
   its silence.

   No kashida on this surface: the kashida is the clock, and a report has
   nothing to time. Rubrication stays, doing the job it was always doing —
   it never meant "correct", it meant *look here*.
   ═══════════════════════════════════════════════════════════════════════ */
.votes {
  display: flex;
  gap: 3px;
  align-items: stretch;
}
.votes-slot {
  position: relative;
  display: grid;
  min-inline-size: 5px;   /* one child out of thirty is still a child */
  padding-block-end: clamp(10px, 1.2vw, 16px);
}
.votes-seg {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: .5em;
  min-inline-size: 0;
  block-size: clamp(40px, 4.4vw, 58px);
  border-radius: 2px;
  background: var(--pigment);
  color: var(--on-pigment);
  box-shadow: inset 0 0 0 2px var(--rim);
  font-family: var(--face-display);
  font-weight: 600;
  font-size: clamp(.8rem, 1.15vw, 1.05rem);
  font-variant-numeric: tabular-nums;
  overflow: hidden;
  white-space: nowrap;
}
.votes-seg b { font-weight: 600; opacity: .72; }

/* Nobody chose it. The slot stays — the option existed, and dropping it
   would quietly redraw the question the class actually saw — but it goes
   hollow, because a filled 5px sliver and a one-child sliver look identical
   and one of them is a lie. */
.votes-slot[data-empty="true"] .votes-seg {
  background: transparent;
  box-shadow: inset 0 0 0 1.5px var(--mistara);
}

/* Always on. This is a report, not a reveal — nothing is being withheld. */
.votes-slot[data-correct="true"] .rubric { clip-path: inset(0 0 0 0); }

/* ═══════════════════════════════════════════════════════════════════════
   THE JOIN — the first screen a student ever sees, and the whole of what
   the system will ever know about them.

   §7 is why there is no text input here. Its position is that the teacher
   knows who «الأسد» is and we never receive the mapping — the re-identifying
   knowledge lives in a human's head, never in the database. But it names the
   thing that breaks it: *"students type their real names into nickname
   fields, constantly."* «أحمد العتيبي» is a name — the first enumerated
   example in Saudi PDPL Art. 1(4) — and in a class of 25 even «أحمد»
   indirectly identifies.

   So the field is a picker. Not a placeholder that says "don't use your real
   name", not validation that rejects one — **there is nothing to type into.**
   §7: "a legal position that depends on a child not typing their own name
   isn't one." This is that sentence as an interface.

   Kahoot's join screen is a free-text box. Copying this one costs them the
   feature; keeping it costs us nothing, because a nine-year-old would rather
   be الصقر than themselves anyway.
   ═══════════════════════════════════════════════════════════════════════ */
.join { gap: 14px; }
.join-mark { text-align: center; opacity: .5; margin-block-end: 2px; }
.join-label {
  margin: 0;
  font-family: var(--face-chrome);
  font-size: .72rem;
  opacity: .45;
}
.join-pin {
  font-family: var(--face-display);
  font-weight: 600;
  font-size: 2.1rem;
  letter-spacing: .22em;
  text-align: center;
  padding-block: 8px;
  border-radius: 3px;
  box-shadow: inset 0 0 0 1.5px var(--mistara);
  font-variant-numeric: tabular-nums;
}
.join-names {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 6px;
}
.join-name {
  border: 0;
  border-radius: 3px;
  padding: 9px 6px;
  background: transparent;
  color: inherit;
  box-shadow: inset 0 0 0 1.5px var(--mistara);
  font-family: var(--face-content);
  font-size: .85rem;
  font-weight: 600;
  cursor: pointer;
  transition: background 220ms ease, color 220ms ease, box-shadow 220ms ease;
}
/* The chosen name takes a pigment, because from here on it IS the student —
   it's what the leaderboard says and what the teacher reads out. */
.join-name[aria-pressed="true"] {
  background: var(--lapis);
  color: var(--gypsum);
  box-shadow: none;
}
.join-go {
  border: 0;
  border-radius: 3px;
  padding: 11px;
  background: var(--vermilion);
  color: var(--gypsum);
  box-shadow: inset 0 0 0 2px color-mix(in oklab, var(--vermilion) 62%, var(--gypsum));
  font-family: var(--face-chrome);
  font-size: .9rem;
  font-weight: 600;
  cursor: pointer;
}
/* Disabled says what's missing, rather than just going grey at you. */
.join-go[disabled] {
  background: transparent;
  color: inherit;
  box-shadow: inset 0 0 0 1.5px var(--mistara);
  opacity: .5;
  cursor: default;
}

/* ═══════════════════════════════════════════════════════════════════════
   لوحة الشرف — THE HONOUR BOARD
   The moment a seven-year-old actually plays for, and the one §10 named and
   never built: "Reem Kufi — question numbers, the reveal, THE LEADERBOARD."

   Every Arab school already has a لوحة الشرف on the wall. It is the subject's
   own artifact, so the ranking doesn't need inventing — and it's where the
   animal names from the join screen finally pay for themselves. الصقر passing
   الأسد is what thirty children scream at.

   WHAT THIS IS NOT: a podium with bars and confetti. That is Kahoot's screen
   and it is the template answer for "leaderboard" — it appears on every quiz
   product regardless of subject. There are no bars here at all.

   THE MOTION IS THE DESIGN. Names hold a fixed row height and slide past each
   other (FLIP). You don't read your rank; you watch it change. That is the
   whole reason to show a board between questions rather than a number.

   THE SIGNATURE: the glow moves to the leader. The product is named وهج —
   emitted light — and --wahaj is spent on the kashida. Between questions there
   is no clock, so the light is free, and it lands on whoever is ahead. The
   brand does the celebrating, and it costs no new element.
   ═══════════════════════════════════════════════════════════════════════ */
.honour {
  display: grid;
  gap: clamp(4px, .8cqi, 10px);
  align-content: center;
  max-inline-size: min(100%, 900px);
  margin-inline: auto;
  inline-size: 100%;
}
.honour-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  margin-block-end: clamp(6px, 1.4cqi, 18px);
}
.honour-title {
  margin: 0;
  font-family: var(--face-display);
  font-weight: 600;
  font-size: clamp(1rem, 3.2cqi, 3rem);
}
.honour-when {
  font-family: var(--face-display);
  font-size: clamp(.6rem, 1.4cqi, 1.3rem);
  opacity: .45;
}

/* One row per child. Fixed height so the FLIP has something to slide. */
.honour-row {
  display: grid;
  /* rank · name · streak · score. The streak column is `auto`, so it collapses to
     nothing on the rows that don't have one — no reserved gap, no ragged edge. */
  grid-template-columns: auto minmax(0, 1fr) auto auto;
  align-items: center;
  gap: clamp(8px, 1.8cqi, 26px);
  padding: clamp(5px, 1.1cqi, 14px) clamp(8px, 1.6cqi, 20px);
  border-radius: 3px;
  box-shadow: inset 0 0 0 2px var(--mistara);
  /* transform, not top: FLIP animates the compositor, and thirty rows at 60fps
     on a classroom laptop is not the place to animate layout. */
  transition: transform 620ms cubic-bezier(.22, 1, .36, 1),
              box-shadow 420ms ease, background 420ms ease, color 420ms ease;
  will-change: transform;
}
.honour-rank {
  font-family: var(--face-display);
  font-weight: 500;
  font-size: clamp(.7rem, 1.9cqi, 1.8rem);
  opacity: .5;
  inline-size: 1.6em;
  text-align: center;
  font-variant-numeric: tabular-nums;
}
.honour-name {
  font-family: var(--face-display);
  font-weight: 600;
  font-size: clamp(.85rem, 2.5cqi, 2.4rem);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.honour-score {
  font-family: var(--face-display);
  font-weight: 600;
  font-size: clamp(.85rem, 2.5cqi, 2.4rem);
  font-variant-numeric: tabular-nums;
}

/* The streak chip — saffron, because a streak is the clock's colour: both are the
   reward for keeping pace. It reads "٣" for three-in-a-row. On the leader's saffron
   row it would vanish, so there it inverts to ink-on-the-row. */
.honour-streak {
  justify-self: end;
  min-inline-size: 1.7em;
  padding: .05em .5em;
  border-radius: 999px;
  background: var(--saffron);
  color: var(--ink);
  filter: var(--wahaj);
  font-family: var(--face-display);
  font-weight: 600;
  font-size: clamp(.6rem, 1.6cqi, 1.4rem);
  font-variant-numeric: tabular-nums;
  text-align: center;
}
.honour-row[data-lead="true"] .honour-streak {
  background: var(--ink);
  color: var(--saffron);
  filter: none;
}

/* The leader. The glow, and nothing else — no crown, no medal, no gold. */
.honour-row[data-lead="true"] {
  background: var(--saffron);
  color: var(--ink);
  box-shadow: none;
  filter: var(--wahaj);
}
/* You. A child scanning thirty names finds themselves first, or the board is
   just a list of other people. */
.honour-row[data-you="true"] { box-shadow: inset 0 0 0 2px var(--on-ground); }
.honour-row[data-you="true"] .honour-name::after {
  content: "أنت";
  font-family: var(--face-chrome);
  font-size: .5em;
  opacity: .55;
  margin-inline-start: .8em;
}

/* Whoever just moved up. Vermilion is the corrector's mark — "look here" —
   and this is the one place on the board worth looking. */
.honour-delta {
  font-family: var(--face-display);
  font-size: clamp(.55rem, 1.4cqi, 1.2rem);
  color: var(--vermilion);
  margin-inline-start: .6em;
}
.honour-row[data-lead="true"] .honour-delta { color: var(--ink); opacity: .6; }

@media (prefers-reduced-motion: reduce) {
  /* The ranking still changes; it just stops sliding. The information is the
     order, not the animation — a child who can't see motion has lost nothing. */
  .honour-row { transition: none; }
}

/* ═══════════════════════════════════════════════════════════════════════
   THE WORDMARK — وهـــج
   The kashida inside the name is the same stroke that times every question,
   drawn by the same function. The logo contains the clock. See brand.js for
   why the string can be split at all without destroying the shaping.
   ═══════════════════════════════════════════════════════════════════════ */
/* Inline layout, not flex, and deliberately so: a replaced element's
   baseline IS its bottom margin edge, so `vertical-align: baseline` drops
   the SVG's floor exactly onto the text's baseline for free. A kashida runs
   along the baseline, so that's the whole alignment problem solved by the
   layout mode instead of by a magic offset that breaks at another size. */
.wm {
  display: inline-block;
  font-family: var(--face-display);
  font-weight: 600;
  line-height: 1;
  letter-spacing: 0;
  white-space: nowrap;
}
.wm-kashida {
  display: inline-block;
  vertical-align: baseline;
  /* The elongation. Wide enough to read as a stretched join rather than a
     gap the letters fell into. */
  inline-size: .86em;
  block-size: .42em;
  overflow: visible;
}
.wm-stroke { fill: currentColor; filter: var(--wahaj); }

/* ═══════════════════════════════════════════════════════════════════════
   THE COLLAPSE — a container query, not a media query.

   This was a viewport media query on each page, and it was quietly wrong the
   moment the wall became a component. A game mounted in a 570px card on a
   1440px monitor would stay four-across and shrink its text to nothing,
   because the *viewport* was wide. The wall doesn't care how big the screen
   is. It cares how big IT is.

   One rule, and every mount does the right thing: the classroom wall, the
   card on the site, and a phone.
   ═══════════════════════════════════════════════════════════════════════ */
@container (max-width: 720px) {
  .keys  { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .pairs { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .wall-question { max-inline-size: 18ch; }
  .key-answer { overflow-wrap: anywhere; }
}

/* ── Said, not shown ───────────────────────────────────────────────────
   A live region that carries information the visual surface already carries
   another way.

   This exists because of a real gap: a grep for `aria-live` across the whole
   project returned ZERO. Every verdict this product gives a student — a tile
   receding, a mark drawn in cinnabar, a kashida running out — is delivered in
   a channel a screen reader cannot perceive. The design is right that the
   moment should be orchestrated and silent (§10: "one orchestrated moment,
   not scattered effects" — a note appearing under the board would be exactly
   the scatter it rejects). But "no extra visual" and "no information" are not
   the same requirement, and the second one was never intended.

   So the information is SAID rather than SHOWN. Not display:none and not
   visibility:hidden — both remove it from the accessibility tree too, which
   would be an elaborate way of changing nothing.

   The wall's `.device-note` deliberately does NOT use this: on the phone the
   note is visible, because there the note IS the surface. */
.sr-only {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Matching's verdict. Said, not shown — the tile already says it in pigment. */
.pair-note {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ── Floor ─────────────────────────────────────────────────────────────
   Not announced, just met. */
:where(button, a, [tabindex]):focus-visible {
  outline: 3px solid var(--saffron);
  outline-offset: 3px;
}
[data-ground="light"] :where(button, a, [tabindex]):focus-visible { outline-color: var(--lapis); }

@media (prefers-reduced-motion: reduce) {
  /* The kashida carries information — a kid who can't see it sweep has lost
     the clock. It steps once per second instead (see qalam.js). Everything
     else simply stops. */
  *, *::before, *::after {
    transition-duration: 1ms !important;
    animation-duration: 1ms !important;
  }
}

/* The honour board is an <ol> — a ranking is a list, and <ol> announces "3 of 6"
   without a duplicate digit. It just must not look like one. */
.honour .js-rows { list-style: none; margin: 0; padding: 0; display: grid; gap: inherit; }
