/* ============================================================
   STEP SOLVER — shared design system
   Singapore-math step dashboard for TIMO Primary papers.

   COLOUR IS COMMITTED, ONE HUE PER MATH ROLE:
     blue  = the first quantity / group A
     mango = the second quantity / group B
     pink  = the bonus, the remainder, the thing that surprises
     mint  = the answer, and only the answer
     ink   = structure (bars, top, bottom)

   SAFETY RULE: entrance animations use fill-mode BACKWARDS, never
   BOTH. If an animation fails to run, the element is still visible.
   A teaching tool must never hide the maths behind a transition.
   ============================================================ */
:root{
  --paper:#FCF8F2;
  --paper2:#F5EDE1;
  --card:#FFFFFF;
  --ink:#17132B;
  --ink2:#4A4260;
  --ink3:#8A8199;
  --line:#E6DBCA;

  --blue:#3D5AFE;  --blue-d:#2438C8;  --blue-w:#E6E9FF;
  --mango:#FF8A00; --mango-d:#C96900; --mango-w:#FFEDD6;
  --pink:#F0387B;  --pink-d:#C21D5A;  --pink-w:#FFE2EC;
  --mint:#00B894;  --mint-d:#00806A;  --mint-w:#DAF7EF;

  --r:16px;
  --shadow:0 2px 0 rgba(23,19,43,.06), 0 10px 26px -14px rgba(23,19,43,.42);
  --ez:cubic-bezier(.22,1.1,.36,1);

  /* stage type ramp — sized for CHILDREN across a room, not for an
     adult at a laptop. Every floor here is well above the 11px
     readability minimum on purpose; the minimum is a floor, not a target. */
  --t-title:clamp(19px,2.4vh,27px);
  --t-body:clamp(20px,2.7vh,27px);
  --t-eq:clamp(33px,4.9vh,56px);
  --t-huge:clamp(50px,7.6vh,90px);
}

*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%}
body{
  background:var(--paper);color:var(--ink);
  font-family:ui-rounded,-apple-system,"SF Pro Rounded","Avenir Next",system-ui,"Segoe UI",sans-serif;
  font-size:19px;overflow:hidden;-webkit-font-smoothing:antialiased;
}
.tnum{font-variant-numeric:tabular-nums}

/* "30th", "7th" — a superscript shrinks to ~0.8em of whatever it sits in,
   so inside any small label it silently drops under the readable floor.
   Clamp it once, globally, instead of chasing it per component. */
sup, sub{font-size:max(11.5px, .72em);line-height:0}

/* ================= SHELL ================= */
.app{
  height:100vh;display:grid;
  /* the stage is the star — the rails get only what they need, so the
     bigger kid-sized type still has room to breathe in the middle.
     Named areas, not row/column numbers: the responsive variants below
     only have to restate the picture, and panels cannot collide. */
  /* LAYOUT B — the running total is not a third column. It lives in the
     bottom bar, where it reads left-to-right as a sum beside the sentence
     that explains it, and the stage keeps the width it was borrowing. */
  grid-template-columns:clamp(272px,19vw,340px) minmax(0,1fr);
  grid-template-rows:auto minmax(0,1fr) auto;
  grid-template-areas:
    "top  top"
    "rail stage"
    "bot  bot";
  gap:13px;padding:13px;
}
.topbar{grid-area:top}
.rail  {grid-area:rail}
/* ---- the rail contains its own content ----
   Puay: "a number of problem that fall under the edge of the explanation
   pane." The panels were overflow:visible, so when the question and the
   givens together exceeded the rail they ran past its bottom and over the
   bottom bar — no scrollbar, nothing clipped, nothing to notice, and
   invisible to a gate whose clip check only looks at overflow:hidden.

   Two wrong turns before this, both worth remembering:
     · Letting the panels scroll individually starved the question panel to
       ZERO height on a short window — 0px holding 668px of text. Invisible,
       and it passed the spill check, because nothing can hang out of a box
       that is not drawn.
     · Clamping the rail with overflow:hidden merely moved the loss inside.

   So: ONE scroll container (the rail), and the question keeps a floor. On a
   normal window nothing scrolls at all; on a short one the supporting detail
   goes below the fold instead of off the edge, and is still reachable. */
.rail{min-height:0; overflow-y:auto; overscroll-behavior:contain}
.rail > .panel{min-height:0}
.rail > .panel:first-child, .rail .q-panel{
  display:flex; flex-direction:column; flex:0 0 auto;
}
.rail .problem{min-height:0}

/* ---- the question is a summary; the whole of it lives in a popup ----
   Puay: "when the student want to see full screen of problem statement
   they can click on it." Clamping the panel is what lets the nuggets
   panel below it have real estate: the gate measured this panel holding
   668px of text in a 640px rail, which is how the rail came to scroll at
   all. A question that cannot grow past a third of the rail cannot push
   what we know off the bottom. */
.rail .q-panel .problem{
  max-height:34vh; overflow:hidden; position:relative; cursor:zoom-in;
}
/* The fade IS the affordance — it says "there is more" without a
   scrollbar the child has to discover. Hidden once the text fits. */
.rail .q-panel .problem.clipped::after{
  content:''; position:absolute; left:0; right:0; bottom:0; height:54px;
  background:linear-gradient(to bottom,rgba(255,255,255,0),var(--card) 86%);
  pointer-events:none;
}
.rail .q-panel h3{display:flex;align-items:center;gap:8px;justify-content:space-between}
.qfull{
  font:inherit; font-size:11.5px; font-weight:800; letter-spacing:.6px;
  border:1.5px solid var(--line); background:var(--paper2); color:var(--ink3);
  border-radius:999px; padding:3px 10px; cursor:pointer; white-space:nowrap;
  text-transform:none; letter-spacing:.2px;
}
.qfull:hover{background:#fff;color:var(--ink);border-color:var(--mint)}

/* ---- "What we know" fills the rest of the rail ----
   Puay: "The what we know section should extend all the way down."
   The question panel takes the height its (now clamped) text needs; this
   one takes every pixel that is left, so a nugget never lands in a box
   that has already run out of room. */
.rail .g-panel{flex:1 1 auto; display:flex; flex-direction:column; min-height:0}
/* position:relative makes THIS the offset parent, so a nugget's offsetTop
   is measured from the scroll box and can be compared with scrollTop.
   Without it offsetTop came from some ancestor and every nugget looked
   like it was below the fold. */
.rail .g-panel .givens{flex:1 1 auto; min-height:0; overflow-y:auto; position:relative}

/* ---- nothing may go missing quietly ----
   Puay: "Try not to let nuggets falls off or at least having scroll bar
   or notice showing." macOS hides the overlay scrollbar until a scroll is
   already under way, so `overflow:auto` on its own is not a notice — a
   fact below the fold looks identical to a fact that was never there.
   Hence a scrollbar that is always drawn, AND a counted notice. */
.rail .g-panel .givens::-webkit-scrollbar{width:9px}
.rail .g-panel .givens::-webkit-scrollbar-track{background:var(--paper2);border-radius:9px}
.rail .g-panel .givens::-webkit-scrollbar-thumb{
  background:var(--line); border-radius:9px; border:2px solid var(--paper2);
}
.rail .g-panel .givens::-webkit-scrollbar-thumb:hover{background:var(--ink3)}
.rail .g-panel .givens{scrollbar-width:thin; scrollbar-color:var(--line) var(--paper2)}

.gmore{
  flex:0 0 auto; margin-top:8px; width:100%;
  font:inherit; font-size:12.5px; font-weight:800; letter-spacing:.2px;
  border:1.5px dashed var(--line); background:var(--paper2); color:var(--ink3);
  border-radius:10px; padding:6px 10px; cursor:pointer;
}
.gmore:hover{background:#fff;color:var(--ink);border-color:var(--mint);border-style:solid}
.gmore[hidden]{display:none}

/* Before the first nugget the panel is legitimately empty, and an empty
   half-rail reads as something failing to load. Say what it is for. */
.rail .g-panel .givens:empty::before,
.rail .g-panel .givens:not(:has(.given.shown))::before{
  content:'Facts we work out appear here, one at a time.';
  display:block; padding:14px 4px; color:var(--ink3);
  font-size:14px; font-weight:600; line-height:1.5; font-style:italic;
}

/* A figure in the rail is a thumbnail: it must fit the column rather than
   set the column's width. Full size is what the popup is for. */
.rail .problem svg, .rail .problem img{
  display:block; max-width:100%; height:auto; max-height:20vh; margin:8px auto 0;
}
/* Puay: "the origin of the question is not important. it should be in
   full question popup." The citation was four lines of competition name
   in the rail, above the nuggets and read by nobody solving the problem.
   It still ships — the popup copies .problem wholesale, so provenance is
   one click away rather than permanently in the way. */
.rail .problem .src{display:none}

.pfig{margin-top:10px}
/* On the read card the figure is the point of the question, so it gets
   real height — but bounded, because fitReadCard() has to be able to make
   question + figure + answer box fit the stage together. */
/* ---- split layout: read on the left, answer on the right ----
   Chosen per question by layoutReadCard(); see the judgement recorded
   there. The figure gets the left column's full width, which is the whole
   reason for moving the answering UI out of the way. */
.readcard.split{
  display:grid; grid-template-columns:minmax(0,1.1fr) minmax(0,.9fr);
  align-content:center; gap:clamp(16px,3vw,46px); text-align:left;
}
.readcard.split .rc-left,
.readcard.split .rc-right{
  display:flex; flex-direction:column; justify-content:center;
  gap:clamp(10px,1.8vh,18px); min-width:0;
}
.readcard.split .rc-right{text-align:center}
.readcard.split .rq{max-width:none; text-align:left}
.readcard.split .pfig.rfig svg{max-height:var(--rfig-max,46vh)}

.readcard .pfig.rfig{width:100%;display:flex;justify-content:center;margin:2px 0 0}
.readcard .pfig.rfig svg{max-height:var(--rfig-max,31vh);max-width:min(560px,86%);height:auto;width:auto}

/* ---------- the full-question popup ---------- */
.qmodal{
  position:fixed; inset:0; z-index:60; display:grid; place-items:center;
  background:rgba(20,18,31,.72); padding:4vmin;
  animation:qmIn .18s var(--ez) backwards;
}
.qmodal[hidden]{display:none}
@keyframes qmIn{from{opacity:0}}
.qmodal .qm-card{
  background:var(--card); border-radius:22px; border:1.5px solid var(--line);
  box-shadow:0 24px 70px rgba(0,0,0,.4);
  max-width:min(980px,92vw); max-height:88vh; overflow:auto;
  padding:clamp(20px,3.4vh,34px) clamp(22px,3.6vw,44px);
}
.qmodal .qm-card .rq-text{
  font-size:clamp(22px,3.4vh,34px); line-height:1.5; font-weight:700; color:var(--ink);
}
.qmodal .qm-card .src{margin-top:18px}
/* full size here — this is the reason the popup exists */
.qmodal .qm-card svg, .qmodal .qm-card img{
  display:block; max-width:100%; height:auto; margin:20px auto 0;
}
/* the photograph of the printed page, below our rendering of it */
.qm-scan{margin-top:22px;border-top:1.5px solid var(--line);padding-top:16px}
.qm-scanhead{font-size:12px;font-weight:800;letter-spacing:1.3px;text-transform:uppercase;
  color:var(--ink3);margin-bottom:10px}
.qm-scan img{display:block;width:100%;height:auto;border-radius:12px;
  border:1.5px solid var(--line);background:#fff}
.qmodal .qm-close{
  margin-top:22px; font:inherit; font-size:15px; font-weight:800;
  border:none; border-radius:12px; padding:10px 20px; cursor:pointer;
  background:var(--mint); color:#04241D;
}
.stage {grid-area:stage}
.strip {grid-area:strip}
.bottom{grid-area:bot}

/* ---------- top ---------- */
.topbar{
  background:var(--ink);color:#fff;border-radius:var(--r);
  padding:11px 18px;display:flex;align-items:center;gap:14px;
  /* NEVER wrap. This used to be flex-wrap:wrap, to stop the bar running
     off the edge on a narrow or zoomed viewport — but wrap resolves
     overflow by breaking to a second row, and flex-shrink only applies
     WITHIN a row, so nothing ever shrank. The result was a bar that was
     one row or two depending on how long that page's topic name was, and
     a stage that was 40px shorter on seven pages for no visible reason.
     nowrap forces the shrink path instead: labels ellipsize, the bar
     stays 56px, and the stage is the same height on every page at every
     width. Gate check 7 enforces it. */
  flex-wrap:nowrap;min-width:0;overflow:hidden;
}
.topbar > *{min-width:0}
/* Nothing in this bar may be rigid.
   Every item used to be flex-shrink:0, so the bar's width was the SUM of
   whatever text each page happened to carry — and seven pages tipped a few
   pixels over and wrapped to a second row, costing 40px of stage. Same
   layout, different stage height, purely because a topic name was longer.
   Now the label text yields first and the bar keeps one row. Gate check 7
   fails if this ever regresses. */
.brand{display:flex;align-items:baseline;gap:10px;flex-shrink:0}
.brand b{font-size:22px;letter-spacing:-.2px}
.brand span{font-size:15px;color:#B9B2CC;white-space:nowrap}
/* decorative — the first thing to go when the bar needs room */
@media (max-width:1500px){ .brand span{display:none} }
@media (max-width:1080px){ .badge-t{display:none} }
/* a short window is the case that overflows; give the supporting detail
   less room before the question has to give any up */
@media (max-height:820px){
  .givens .given{padding:7px 9px}
  .givens .gt{font-size:14px}
  .givens .gt small{font-size:12.5px}
}
@media (max-height:730px){
  .rail .panel h3{margin-bottom:5px}
  .givens{gap:5px}
  .givens .gi{width:30px;height:30px;font-size:14px}
}
.badge-q{background:var(--mint);color:#04241D;font-weight:800;font-size:15px;
  padding:6px 12px;border-radius:999px;white-space:nowrap;
  flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis}
.badge-t{background:rgba(255,255,255,.14);color:#fff;font-weight:700;font-size:14.5px;
  padding:6px 12px;border-radius:999px;white-space:nowrap;
  flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis}
.home{color:#B9B2CC;text-decoration:none;font-size:13.5px;font-weight:700;
  padding:6px 11px;border-radius:9px;background:rgba(255,255,255,.09);white-space:nowrap}
.home:hover{background:rgba(255,255,255,.2);color:#fff}
.audio{display:flex;gap:6px;align-items:center;flex-shrink:0}
.abtn{width:34px;height:34px;border-radius:10px;background:rgba(255,255,255,.11);
  font-size:15px;line-height:1;display:grid;place-items:center}
.abtn:hover{background:rgba(255,255,255,.24)}
.vpick{background:rgba(255,255,255,.11);color:#fff;border:none;border-radius:10px;
  padding:7px 8px;font-family:inherit;font-size:12.5px;font-weight:700;max-width:132px;cursor:pointer}
.vpick option{background:#241E3A;color:#fff}
/* the music-track picker sits beside the 🎵 button. Narrower than the
   voice picker on purpose — it holds two or three short names, and the
   topbar is already carrying a lot. */
.mpick{max-width:118px}

/* The paper prints some questions with the expression on its own line
   below the sentence. Reproducing that is not decoration: for Q8 the
   whole insight is SEEING the group repeat, and a paraphrase that folded
   it into prose gave the answer away. */
.problem .disp{display:block;margin-top:9px;font-weight:800;
  font-size:clamp(15px,1.35vw,19px);line-height:1.5;color:var(--ink);
  white-space:pre-wrap;word-break:break-word}

/* ---- report a problem ----
   Quiet until wanted: it sits under the question as a plain line, and
   only looks like a button on hover. A child should be able to find it,
   and should not be invited to press it instead of doing the maths. */
.rptbtn{margin-top:10px;align-self:flex-start;background:transparent;border:2px dashed var(--line);
  border-radius:11px;padding:7px 12px;font:inherit;font-size:13.5px;font-weight:700;
  color:var(--ink3);cursor:pointer;flex-shrink:0}
.rptbtn:hover{border-style:solid;border-color:var(--pink);color:var(--pink-d);background:var(--pink-w)}
.rpt-back{position:fixed;inset:0;background:rgba(23,19,43,.55);z-index:80;
  display:grid;place-items:center;padding:22px}
.rpt{background:var(--card);border-radius:22px;padding:26px 28px;width:min(560px,100%);
  max-height:92vh;overflow:auto;box-shadow:0 30px 70px -20px rgba(23,19,43,.6)}
.rpt h2{font-size:23px;letter-spacing:-.5px;margin-bottom:14px;color:var(--ink)}
.rpt-p{font-size:16px;color:var(--ink2);line-height:1.55;font-weight:600;margin-bottom:16px}
.rpt-kinds{display:grid;gap:8px;margin-bottom:16px}
.rpt-kind{display:flex;gap:11px;align-items:flex-start;border:2px solid var(--line);
  border-radius:13px;padding:10px 13px;cursor:pointer;background:var(--paper2)}
.rpt-kind.on{border-color:var(--ink);background:var(--card)}
.rpt-kind input{width:19px;height:19px;accent-color:var(--ink);flex-shrink:0;margin-top:2px;cursor:pointer}
.rpt-kind b{display:block;font-size:15.5px;font-weight:800;color:var(--ink)}
.rpt-kind small{display:block;font-size:13px;color:var(--ink3);font-weight:600;margin-top:1px}
.rpt-lab{display:block;font-size:14px;font-weight:800;color:var(--ink2);margin-bottom:6px}
.rpt textarea{width:100%;border:2px solid var(--line);border-radius:13px;padding:11px 13px;
  font:inherit;font-size:16px;font-weight:600;color:var(--ink);background:var(--paper2);resize:vertical}
.rpt textarea:focus{outline:none;border-color:var(--blue);background:#fff}
.rpt-err{color:var(--pink-d);font-size:14px;font-weight:800;margin-top:7px}
.rpt-act{display:flex;gap:10px;justify-content:flex-end;margin-top:18px}
.rpt-ghost,.rpt-go{border-radius:999px;padding:11px 20px;font:inherit;font-size:15.5px;
  font-weight:800;cursor:pointer;border:2px solid var(--line);background:#fff;color:var(--ink2)}
.rpt-go{background:var(--mint);border-color:var(--mint);color:#04241D;box-shadow:0 4px 0 var(--mint-d)}
.rpt-go:active{transform:translateY(3px);box-shadow:none}
@media (hover:none){ .rptbtn{padding:11px 14px;font-size:15px} .rpt-ghost,.rpt-go{min-height:44px} }
.dots{display:flex;gap:7px;align-items:center;margin-left:auto}
.dot{width:11px;height:11px;border-radius:50%;background:rgba(255,255,255,.2);transition:all .35s var(--ez)}
.dot.done{background:rgba(255,255,255,.55)}
.dot.now{background:var(--mint);transform:scale(1.65);box-shadow:0 0 0 5px rgba(0,184,148,.22)}
.stepcount{font-size:13.5px;color:#B9B2CC;white-space:nowrap}

/* ---------- left rail ---------- */
.rail{display:flex;flex-direction:column;gap:12px;min-height:0}
.panel{background:var(--card);border:1.5px solid var(--line);border-radius:var(--r);
  padding:15px 16px;box-shadow:var(--shadow)}
.panel h3{font-size:13.5px;letter-spacing:1.4px;text-transform:uppercase;color:var(--ink3);
  font-weight:800;margin-bottom:10px}
.problem{font-size:21px;line-height:1.58;color:var(--ink2);overflow-y:auto;min-height:0}
.problem .k{font-weight:800;color:var(--ink);padding:1px 5px;border-radius:6px;
  background:transparent;display:inline-block;
  /* grow to the RIGHT only — scaling from the centre pushed the first
     highlighted word out through the panel's clipping edge */
  transform-origin:left center;
  transition:background .4s var(--ez),color .4s var(--ez),transform .4s var(--ez)}
.problem .k.lit{transform:scale(1.11)}
.k.lit[data-c=blue]{background:var(--blue-w);color:var(--blue-d)}
.k.lit[data-c=mango]{background:var(--mango-w);color:var(--mango-d)}
.k.lit[data-c=pink]{background:var(--pink-w);color:var(--pink-d)}
.k.lit[data-c=mint]{background:var(--mint-w);color:var(--mint-d)}
.src{margin-top:11px;font-size:14px;color:var(--ink3);line-height:1.45}

.givens{display:flex;flex-direction:column;gap:8px}
.given{display:flex;align-items:center;gap:11px;padding:9px 11px;border-radius:12px;
  background:var(--paper2);border:1.5px dashed var(--line);
  opacity:.34;filter:grayscale(1);transition:all .5s var(--ez)}
/* ---- gradual reveal ----
   Puay: "this is great nuggets of knowledge concept. but it should
   gradually revealing as the problem progress."
   A fact she has not been told yet is not in the layout at all. Dimming
   it still shows the shape of what is coming AND fills the panel with
   things she has not earned — which is what made the rail overflow.
   `backwards`, never `both`: with `both` the element keeps the keyframe's
   opacity forever and the .on state can never brighten it. */
.given:not(.shown){display:none}
.given.shown{animation:givenIn .42s var(--ez) backwards}
@keyframes givenIn{from{opacity:0;transform:translateY(-9px)}}
.given.on{opacity:1;filter:none;border-style:solid;background:#fff;transform:translateX(3px)}
.given .gi{width:40px;height:40px;border-radius:11px;flex-shrink:0;display:grid;place-items:center;
  font-size:18px;font-weight:900;color:#fff;line-height:1}
.given .gt{font-size:17.5px;font-weight:700;line-height:1.3}
.given .gt small{display:block;font-size:14.5px;font-weight:600;color:var(--ink3)}

/* ---------- stage ---------- */
.stage{background:var(--card);border:1.5px solid var(--line);border-radius:22px;
  box-shadow:var(--shadow);position:relative;overflow:hidden;min-height:0;
  background-image:radial-gradient(var(--line) 1.1px,transparent 1.1px);background-size:26px 26px}
.scene{position:absolute;inset:0;display:none;flex-direction:column;align-items:center;
  justify-content:center;gap:clamp(12px,2.1vh,26px);padding:clamp(16px,2.6vh,32px) 26px;text-align:center}
.scene.live{display:flex}
.scene-title{font-size:var(--t-title);font-weight:800;color:var(--ink3);
  letter-spacing:.5px;text-transform:uppercase}

/* ---------- the READ card ----------
   Step 0 of every problem is reading the question, so the question
   gets the whole stage at a size a child can read from across a room.
   It steps aside the moment the solving starts. */
/* `justify-content:center` overflows a flex container at BOTH ends, so a
   question taller than the stage lost its first line off the top — and
   .stage is overflow:hidden, so it was gone, not scrolled. Q14 lost 127px
   this way at 1440x900 and no check saw it: the gate compares children
   against .rail and .bottom, and nothing looks at the stage.
   `safe center` centres only while it fits and falls back to flex-start
   the moment it does not; overflow-y is the backstop under that. */
.readcard{
  position:absolute;inset:0;z-index:5;background:var(--card);
  display:flex;flex-direction:column;align-items:center;
  justify-content:safe center; overflow-y:auto;
  gap:clamp(14px,2.4vh,28px);padding:clamp(20px,4vh,46px) clamp(24px,5vw,72px);
  text-align:center;opacity:0;pointer-events:none;
  transition:opacity .4s var(--ez);
  background-image:radial-gradient(var(--line) 1.1px,transparent 1.1px);background-size:26px 26px
}
.readcard.show{opacity:1;pointer-events:auto}
.readcard .rlabel{
  font-size:clamp(13px,1.7vh,16px);font-weight:800;letter-spacing:1.6px;
  text-transform:uppercase;color:var(--ink3)
}
/* --rq-fs is set by fitReadCard() when the question is too tall to fit;
   the clamp is the natural size it starts from and returns to. */
.readcard .rq{
  font-size:var(--rq-fs,clamp(25px,4.3vh,50px));font-weight:800;line-height:1.32;
  color:var(--ink);max-width:26ch;letter-spacing:-.6px
}
/* keep the colour coding switched on while reading — it is teaching,
   not decoration, and it primes what will light up later */
.readcard .k{
  font-weight:900;padding:2px 9px;border-radius:9px;display:inline-block;transform:none
}
.readcard .k[data-c=blue] {background:var(--blue-w); color:var(--blue-d)}
.readcard .k[data-c=mango]{background:var(--mango-w);color:var(--mango-d)}
.readcard .k[data-c=pink] {background:var(--pink-w); color:var(--pink-d)}
.readcard .k[data-c=mint] {background:var(--mint-w); color:var(--mint-d)}
.readcard .src{display:none}
.readcard .rgo{
  display:flex;align-items:center;gap:10px;
  background:var(--mint-w);border:2.5px solid var(--mint);color:var(--mint-d);
  border-radius:999px;padding:9px 22px;font-size:clamp(15px,2vh,19px);font-weight:800;
  animation:pulse 2.2s var(--ez) infinite
}
/* re-opened mid-solve rather than shown at the start */
.readcard.peek{background:rgba(252,248,242,.985);backdrop-filter:blur(3px)}
.readcard.peek .rgo{
  background:var(--blue-w);border-color:var(--blue);color:var(--blue-d);animation:none
}

/* ---------- vocabulary ----------
   A word a child may not know is underlined, not hidden behind an icon.
   Hover two seconds, or tap, and the explanation opens — and the lesson
   waits for them. */
.vocab{
  border-bottom:2.5px dotted var(--blue);cursor:help;
  color:inherit;font-weight:inherit;position:relative;
  transition:background .2s var(--ez)
}
.vocab:hover,.vocab.dwell{background:var(--blue-w);border-bottom-style:solid}
.vocab.dwell{box-shadow:inset 0 -3px 0 var(--blue)}
.bottom .vocab{border-bottom-color:var(--mint)}
.bottom .vocab:hover,.bottom .vocab.dwell{background:rgba(0,184,148,.2)}

/* Puay: "the glossary UI should be on top."
   The full-question popup is also z-index 60, so with the two equal the
   winner was decided by DOM order — and .qmodal is built once at start-up
   while .vback already existed, so the question card covered the word card
   the child had just asked for. The glossary is the innermost thing she
   opened and answers a question about the layer beneath it, so it sits
   above; the report overlay at 80 still outranks both. */
.vback{
  position:fixed;inset:0;z-index:70;background:rgba(23,19,43,.55);
  display:none;align-items:center;justify-content:center;padding:24px;
  backdrop-filter:blur(2px)
}
.vback.show{display:flex}
.vcard{
  background:var(--card);border-radius:22px;max-width:min(620px,94vw);
  max-height:88vh;overflow:auto;padding:26px 30px 24px;
  box-shadow:0 24px 60px -20px rgba(23,19,43,.8);
  animation:pop .32s var(--ez)
}
.vcard .vk{font-size:13px;letter-spacing:1.5px;text-transform:uppercase;
  color:var(--ink3);font-weight:800}
.vcard h2{font-size:clamp(28px,4.4vh,40px);letter-spacing:-.8px;margin:2px 0 12px;color:var(--blue-d)}
.vcard .vs{font-size:clamp(19px,2.6vh,24px);font-weight:800;line-height:1.4;color:var(--ink)}
.vcard .vfig{margin:16px 0;background:var(--paper2);border-radius:14px;padding:14px;
  display:flex;justify-content:center}
.vcard .vrow{margin-top:13px;font-size:clamp(16px,2.1vh,19px);line-height:1.5;
  font-weight:600;color:var(--ink2)}
.vcard .vrow b{display:block;font-size:12.5px;letter-spacing:1.1px;text-transform:uppercase;
  color:var(--ink3);margin-bottom:3px}
.vcard .vrow.no{background:var(--pink-w);border-radius:12px;padding:11px 14px;color:var(--pink-d)}
.vcard .vrow.eg{background:var(--mint-w);border-radius:12px;padding:11px 14px;color:var(--mint-d)}
.vclose{
  margin-top:20px;width:100%;background:var(--mint);color:#04241D;border-radius:14px;
  height:54px;font-size:19px;font-weight:900;box-shadow:0 4px 0 var(--mint-d)
}
.vclose:hover{transform:translateY(-2px);box-shadow:0 6px 0 var(--mint-d)}

/* ---------- question-to-question navigation ---------- */
.qnav{display:flex;gap:6px;align-items:center;flex-shrink:0}
.qnav a{
  color:#B9B2CC;text-decoration:none;font-size:14px;font-weight:800;
  padding:7px 12px;border-radius:10px;background:rgba(255,255,255,.09);white-space:nowrap
}
.qnav a:hover{background:rgba(255,255,255,.24);color:#fff}
.qnav a.off{opacity:.3;pointer-events:none}

/* Applied while re-establishing state that the child has ALREADY been
   shown. Without it, every step replays all the earlier animations. */
.no-anim, .no-anim *{transition:none !important; animation:none !important}

/* entrance choreography — BACKWARDS, so a failed animation still shows content */
.live .an{animation:pop .55s var(--ez) backwards;animation-delay:var(--d,0s)}
.live .fl{animation:fly .7s var(--ez) backwards;animation-delay:var(--d,0s)}
.live .gw{animation:grow .55s var(--ez) backwards;animation-delay:var(--d,0s)}
.live .sl{animation:slide .6s var(--ez) backwards;animation-delay:var(--d,0s)}
@keyframes pop{from{opacity:0;transform:translateY(14px) scale(.86)}to{opacity:1;transform:none}}
@keyframes fly{0%{opacity:0;transform:translate(var(--fx,0),var(--fy,-70px)) scale(.3)}60%{opacity:1}100%{opacity:1;transform:none}}
@keyframes grow{from{transform:scaleX(0);opacity:.2}to{transform:scaleX(1);opacity:1}}
@keyframes slide{from{opacity:0;transform:translateX(var(--sx,-40px))}to{opacity:1;transform:none}}
@keyframes shake{0%,100%{transform:translateX(0)}20%{transform:translateX(-7px)}40%{transform:translateX(7px)}60%{transform:translateX(-4px)}80%{transform:translateX(4px)}}
@keyframes pulse{0%,100%{transform:scale(1)}45%{transform:scale(1.09)}}
@keyframes glow{0%,100%{box-shadow:0 0 0 0 rgba(0,184,148,0)}50%{box-shadow:0 0 0 12px rgba(0,184,148,.16)}}

.row{display:flex;align-items:center;justify-content:center;gap:clamp(8px,1.3vw,18px);flex-wrap:wrap}
.col{display:flex;flex-direction:column;align-items:center;gap:9px}
.arrow{font-size:clamp(26px,3.4vh,38px);color:var(--ink3);font-weight:900;line-height:1}

/* ---------- shared maths objects ---------- */
.eq{font-size:var(--t-eq);font-weight:900;letter-spacing:-.6px;line-height:1.15;
  display:flex;align-items:center;gap:11px;flex-wrap:wrap;justify-content:center}
.eq.sm{font-size:clamp(20px,2.8vh,30px)}
.eq .res{background:var(--ink);color:#fff;padding:2px 17px;border-radius:13px}
.eq .res.b{background:var(--blue)}
.eq .res.m{background:var(--mango)}
.eq .res.p{background:var(--pink)}
.eq .res.g{background:var(--mint);color:#04241D}

.bar{display:flex;border-radius:11px;overflow:hidden;box-shadow:0 4px 14px -8px rgba(23,19,43,.6)}
.unit{display:grid;place-items:center;color:#fff;font-weight:900;
  font-size:clamp(21px,2.9vh,32px);border-right:2.5px solid #fff;transform-origin:left center;
  height:clamp(52px,7.2vh,78px)}
.unit:last-child{border-right:none}
.unit.b{background:var(--blue)}
.unit.m{background:var(--mango)}
.unit.p{background:var(--pink)}
.unit.g{background:var(--mint);color:#04241D}
.unit.n{background:var(--ink3)}
.barwrap{display:flex;flex-direction:column;align-items:center}
.barlabel{font-size:clamp(15px,2vh,19px);font-weight:800;color:var(--ink3);
  margin-bottom:5px;letter-spacing:.4px}
.brace{border:3px solid var(--ink2);border-top:none;border-radius:0 0 12px 12px;
  height:14px;margin-top:7px}
.bracelabel{margin-top:8px;font-size:clamp(21px,2.8vh,29px);font-weight:900;color:var(--ink)}

.callout{display:flex;align-items:center;gap:10px;background:var(--pink-w);
  border:2.5px solid var(--pink);color:var(--pink-d);border-radius:14px;padding:11px 20px;
  font-size:clamp(18px,2.4vh,25px);font-weight:800;max-width:94%;line-height:1.34}
.callout.warn{background:#FFF4D6;border-color:#E8A700;color:#8A5C00}
.callout.good{background:var(--mint-w);border-color:var(--mint);color:var(--mint-d)}
.callout.info{background:var(--blue-w);border-color:var(--blue);color:var(--blue-d)}

.answer{display:inline-flex;align-items:baseline;gap:13px;background:var(--mint);color:#04241D;
  padding:9px 30px;border-radius:18px;font-weight:900;
  box-shadow:0 6px 0 var(--mint-d),0 18px 30px -14px rgba(0,128,106,.8);
  animation:pulse 1.5s var(--ez) 1.1s 2}
.answer .n{font-size:var(--t-huge);letter-spacing:-2px;line-height:1}
.answer .u{font-size:clamp(15px,2vh,20px)}

/* chips — a number/letter/token in a sequence */
.chip{
  min-width:clamp(66px,8.6vh,98px);height:clamp(66px,8.6vh,98px);
  border-radius:17px;display:grid;place-items:center;
  font-size:clamp(26px,3.7vh,44px);font-weight:900;
  background:#fff;border:3px solid var(--line);color:var(--ink);
  padding:0 10px;transition:all .4s var(--ez);flex-shrink:0
}
/* A long sequence must never run off the stage. Inside .track the
   chips are sized from the AVAILABLE WIDTH, not from a fixed ramp,
   so seven chips fit the same box that four chips do. */
.track{width:100%}
.track .chip{
  min-width:0;flex:1 1 0;max-width:clamp(58px,7.4vh,92px);
  height:clamp(58px,7.4vh,92px);
  font-size:clamp(20px,3vh,38px);padding:0 4px
}
.track .hop{flex:0 1 auto;width:clamp(24px,3.4vw,54px);min-width:20px}
.chip.b{background:var(--blue);border-color:var(--blue-d);color:#fff}
.chip.m{background:var(--mango);border-color:var(--mango-d);color:#fff}
.chip.p{background:var(--pink);border-color:var(--pink-d);color:#fff}
.chip.g{background:var(--mint);border-color:var(--mint-d);color:#04241D}
.chip.ghost{border-style:dashed;color:var(--ink3);background:var(--paper2)}
.chip.dim{opacity:.42}
.chip small{display:block;font-size:12px;font-weight:700;opacity:.85;margin-top:-2px}

/* the +n hop drawn between two chips */
.hop{position:relative;width:clamp(30px,4vw,56px);height:34px;flex-shrink:0}
.hop::before{content:"";position:absolute;left:2px;right:2px;top:14px;height:18px;
  border:3px solid var(--pink);border-bottom:none;border-radius:22px 22px 0 0}
.hop b{position:absolute;left:50%;top:-9px;transform:translateX(-50%);
  background:var(--pink);color:#fff;font-size:clamp(15px,2vh,19px);font-weight:900;
  padding:2px 11px;border-radius:999px;white-space:nowrap}
.hop.mute::before{border-color:var(--ink3);opacity:.4}
.hop.mute b{background:var(--ink3)}

/* a labelled fact card, used for constraints and comparisons */
.factrow{display:flex;gap:12px;justify-content:center;flex-wrap:wrap}
.fact{border:2.5px solid var(--line);border-radius:14px;background:#fff;
  padding:9px 16px;text-align:left;min-width:180px}
.fact .ft{font-size:14px;font-weight:800;color:var(--ink3);text-transform:uppercase;letter-spacing:.7px}
.fact .fv{font-size:clamp(21px,2.9vh,30px);font-weight:900;margin-top:2px;line-height:1.2}
.fact .fw{font-size:14px;font-weight:700;color:var(--mint-d);margin-top:2px}
.fact.win{border-color:var(--mint);background:var(--mint-w)}
.fact.win .fv{color:var(--mint-d)}
.fact.lose{opacity:.5}

/* ---------- the answer strip ----------
   Authored in each page as a vertical column; the engine relocates it
   into the bottom bar and adds .ledger, which turns it into a horizontal
   running sum. Pages need no markup change. */
.strip{display:flex;flex-direction:column;gap:10px;min-height:0}

.bottom .strip.ledger{
  flex-direction:row;align-items:stretch;gap:7px;margin:0;flex:0 0 auto;min-height:0
}
.bottom .strip.ledger .op{
  display:flex;align-items:center;margin:0;color:#6f6889;font-size:20px;font-weight:900
}
.bottom .strip.ledger .tile{
  background:rgba(255,255,255,.10);border:none;box-shadow:none;
  padding:6px 13px;border-radius:12px;gap:9px;flex:0 0 auto;opacity:.32;
  flex-direction:column;align-items:flex-start;justify-content:center
}
.bottom .strip.ledger .tile.on{opacity:1;transform:none}
.bottom .strip.ledger .tile .sw{display:none}
.bottom .strip.ledger .tile .lab{color:#8f88a6;font-size:11.5px;letter-spacing:.4px;order:2}
.bottom .strip.ledger .tile .lab small{display:none}
.bottom .strip.ledger .tile .val{
  margin-left:0;font-size:clamp(23px,3vh,30px);line-height:1;order:1;color:#fff
}
.bottom .strip.ledger .tile.on[data-c=blue]  .val{color:#8FA2FF}
.bottom .strip.ledger .tile.on[data-c=mango] .val{color:#FFB05C}
.bottom .strip.ledger .tile.on[data-c=pink]  .val{color:#FF7EA8}
.bottom .strip.ledger .tile.on[data-c=ink]   .val{color:#D8D2E6}
.bottom .strip.ledger .total{
  margin:0;background:rgba(0,184,148,.16);border-radius:12px;padding:6px 15px;
  display:flex;flex-direction:column;justify-content:center;opacity:.32;animation:none
}
.bottom .strip.ledger .total.on{opacity:1;background:var(--mint)}
.bottom .strip.ledger .total .lab{display:none}
.bottom .strip.ledger .total .val{
  font-size:clamp(27px,3.6vh,36px);letter-spacing:-1.5px;color:#04241D;line-height:1
}
.bottom .strip.ledger .total .val.word{font-size:clamp(20px,2.6vh,27px)}
.bottom .strip.ledger .total.on .val{color:#04241D}
.bottom .strip.ledger .total .u{color:#04241D;font-size:11.5px;opacity:.75;font-weight:800}
.tile{background:var(--card);border:1.5px solid var(--line);border-radius:var(--r);
  padding:12px 14px;box-shadow:var(--shadow);display:flex;align-items:center;gap:11px;
  opacity:.4;transition:all .55s var(--ez)}
.tile.on{opacity:1;transform:translateY(-2px)}
.tile .sw{width:8px;align-self:stretch;border-radius:99px;background:var(--line)}
.tile.on[data-c=blue] .sw{background:var(--blue)}
.tile.on[data-c=mango] .sw{background:var(--mango)}
.tile.on[data-c=pink] .sw{background:var(--pink)}
.tile.on[data-c=ink] .sw{background:var(--ink2)}
.tile .lab{font-size:16px;font-weight:800;color:var(--ink3);line-height:1.25}
.tile .lab small{display:block;font-weight:600;font-size:14px}
.tile .val{margin-left:auto;font-size:clamp(29px,3.9vh,41px);font-weight:900;letter-spacing:-1px;line-height:1}
.tile.on[data-c=blue] .val{color:var(--blue-d)}
.tile.on[data-c=mango] .val{color:var(--mango-d)}
.tile.on[data-c=pink] .val{color:var(--pink-d)}
.tile.on[data-c=ink] .val{color:var(--ink)}
.op{text-align:center;font-size:18px;font-weight:900;color:var(--ink3);margin:-5px 0}
.total{margin-top:auto;background:var(--ink);color:#fff;border-radius:var(--r);
  padding:15px 16px;box-shadow:var(--shadow);opacity:.4;transition:all .6s var(--ez)}
.total.on{opacity:1;animation:glow 2s var(--ez) 2}
.total .lab{font-size:13.5px;letter-spacing:1.4px;text-transform:uppercase;color:#B9B2CC;font-weight:800}
.total .val{font-size:clamp(46px,6.6vh,74px);font-weight:900;letter-spacing:-3px;line-height:1.08;color:var(--mint)}
.total .val.word{font-size:clamp(30px,4.4vh,48px);letter-spacing:-1.5px}
.total .u{font-size:16px;color:#B9B2CC;font-weight:700}

/* ---------- bottom ---------- */
/* NEVER wrap — the same lesson the top bar already learned, and the
   bottom bar was still making the mistake. Puay: "the UI button has falls
   on the new line... keep it fixed. keep the size of the bottom band fixed
   and auto fit the fonts of the steps instead."
   Once the ledger stopped taking space the caption grew to whatever the
   step's sentence needed, and wrap answered by dropping the controls to a
   second row — so the Next button moved down the screen between steps.
   Fixed height + nowrap + a caption that is shrunk to fit by fitCaption(). */
.bottom{background:var(--ink);border-radius:var(--r);
  padding:12px 18px;display:flex;align-items:center;gap:18px;
  flex-wrap:nowrap;min-width:0;
  height:clamp(98px,13.4vh,134px)}
/* ---- the bottom bar is the narration, and nothing else ----
   Puay: "The bottom panel shows the current narration. just keep the text.
   No graphic needed. (Those are already on the solution board)."
   The running-total tiles were a second rendering of numbers the stage
   already shows, competing with the sentence that explains them. The
   caption takes the whole bar; the ledger markup stays in the DOM so a
   lesson that writes to it still works, it simply is not drawn. */
.strip, .bottom .strip.ledger{display:none}
.caption{min-width:0;flex:1 1 auto;align-self:stretch;overflow:hidden;
  display:flex;flex-direction:column;justify-content:center}
/* --cap-en / --cap-th are set by fitCaption() when a step's sentence is
   too long for the fixed band; the clamps are the natural size. */
.caption .en{font-size:var(--cap-en,clamp(23px,3.1vh,34px));font-weight:800;color:#fff;line-height:1.24;letter-spacing:-.3px}
.caption .th{font-size:var(--cap-th,clamp(16px,2.1vh,22px));color:#BCB4CE;margin-top:6px;line-height:1.4;font-weight:600}
.caption .en b{color:var(--mint)}
.controls{display:flex;align-items:center;gap:10px;flex-shrink:0}
button{font-family:inherit;cursor:pointer;border:none;transition:all .18s var(--ez)}
button:disabled{opacity:.28;cursor:not-allowed}
.ghostbtn{width:56px;height:56px;border-radius:15px;background:rgba(255,255,255,.11);
  color:#fff;font-size:22px;font-weight:900}
.ghostbtn:hover:not(:disabled){background:rgba(255,255,255,.22)}
.next{background:var(--mint);color:#04241D;font-size:22px;font-weight:900;
  padding:0 26px;height:62px;border-radius:17px;display:flex;align-items:center;
  justify-content:center;gap:11px;box-shadow:0 5px 0 var(--mint-d);
  /* Fixed footprint, not a floor. The label swaps between "Wait…",
     "Next step", "Next question ▶" and "Back to all ▶" — a min-width
     still let the longest one grow past it, so the button changed size
     and shifted sideways on every step, moving the target under the
     child's finger. Width is set above the longest label instead. */
  width:clamp(272px,21vw,300px);max-width:100%;white-space:nowrap}
.next:hover:not(:disabled){transform:translateY(-2px);box-shadow:0 7px 0 var(--mint-d)}
.next:active:not(:disabled){transform:translateY(3px);box-shadow:0 2px 0 var(--mint-d)}
.next .kbd{background:rgba(4,36,29,.17);border-radius:7px;padding:2px 8px;font-size:12.5px}
.next.done{background:var(--blue);color:#fff;box-shadow:0 5px 0 var(--blue-d)}
.next.done:hover{box-shadow:0 7px 0 var(--blue-d)}

/* TRAFFIC LIGHT — amber while the narrator is still explaining this step,
   green once it has finished. A child who moves on mid-sentence misses
   the reasoning, and "green means go" is a rule they already know. The
   button stays clickable: this signals, it does not lock them out. */
.next.waiting,
.next.done.waiting{background:#F2B01E;color:#3A2600;box-shadow:0 5px 0 #B57F00}
.next.waiting:hover:not(:disabled),
.next.done.waiting:hover:not(:disabled){box-shadow:0 7px 0 #B57F00}
.next .kbd{transition:none}
.next.waiting .kbd{background:rgba(58,38,0,.18)}
.next .lamp{
  width:13px;height:13px;border-radius:50%;background:currentColor;
  opacity:.55;flex-shrink:0
}
.next.waiting .lamp{opacity:1;animation:blink 1s steps(1,end) infinite}
@keyframes blink{0%,50%{opacity:1}51%,100%{opacity:.25}}

/* ------------------------------------------------------------------
   RESPONSIVE — the dashboard must fill the screen and never scroll
   sideways, at any window size or browser zoom. Chrome is shed in
   order of least value first; the STAGE is the last thing to give up
   space, because it is the thing being taught with.
   ------------------------------------------------------------------ */
.app{max-width:100vw;overflow:hidden}

@media (max-width:1400px){ .brand span{display:none} }
@media (max-width:1240px){ .vpick{max-width:104px} }
@media (max-width:1150px){
  .badge-t{display:none}
  .stepcount{display:none}
  .home span,.home{font-size:12.5px}
}

/* ------------------------------------------------------------------
   TOUCH DEVICES (iPad). Two things change on a screen with no pointer:
   a 2-second hover can never happen, so the vocabulary affordance has
   to be visible; and Apple's 44px minimum touch target applies.
   ------------------------------------------------------------------ */
@media (hover:none){
  /* dwell is impossible — say "tap me" instead of relying on hover */
  .vocab::after{
    content:'ⓘ';font-size:.72em;vertical-align:super;margin-left:2px;
    color:var(--blue);font-weight:900
  }
  .bottom .vocab::after{color:var(--mint)}
  .vocab{border-bottom-style:solid}

  /* Apple HIG minimum touch target */
  .abtn{width:44px;height:44px;font-size:18px;border-radius:12px}
  .vpick{min-height:44px}
  .qnav a,.home{padding:11px 13px}
  .dot{width:13px;height:13px}
}

/* the ledger drops to its own line before the caption gets squeezed */
@media (max-width:1180px){
  .caption{flex:1 1 100%;order:1}
  .bottom .strip.ledger{order:2}
  .controls{order:3;margin-left:auto}
}

/* single column: the problem rail becomes a short scrollable strip */
@media (max-width:820px){
  .app{grid-template-columns:1fr;grid-template-rows:auto auto minmax(0,1fr) auto;
       grid-template-areas:"top" "rail" "stage" "bot";gap:10px;padding:10px}
  .rail{flex-direction:row;overflow-x:auto;flex-shrink:0}
  .rail .panel{min-width:min(78vw,300px);flex-shrink:0}
  .bottom .strip.ledger{overflow-x:auto;max-width:100%}
}

/* ============================================================
   QUIZ — the answer box on the read card
   Sized to sit under the question without pushing it off the stage:
   the question stays the biggest thing on screen, because reading it
   properly is most of the work.
   ============================================================ */
.quiz{margin-top:clamp(14px,2.2vh,26px);width:min(620px,92%);align-self:center;
  display:flex;flex-direction:column;gap:10px;position:relative}
.qhead{display:flex;align-items:center;gap:10px;justify-content:center}
.qask{font-size:clamp(15px,1.5vw,18px);font-weight:800;color:var(--ink2)}
.qwon{font-size:clamp(15px,1.5vw,18px);font-weight:800;color:var(--mint-d)}
.qcoin{background:var(--mango-w);color:var(--mango-d);font-weight:900;font-size:14.5px;
  padding:4px 12px;border-radius:999px;white-space:nowrap}
.qwhy{text-align:center;font-size:13.5px;font-weight:700;color:var(--ink3)}
.qrow{display:flex;gap:9px;justify-content:center}
.qinput{flex:1;max-width:320px;border:2.5px solid var(--line);border-radius:14px;
  padding:12px 16px;font:inherit;font-size:clamp(18px,2vw,22px);font-weight:900;
  color:var(--ink);background:#fff;text-align:center;min-width:0}
.qinput:focus{outline:none;border-color:var(--blue);box-shadow:0 0 0 4px var(--blue-w)}
.qgo{border:none;border-radius:14px;background:var(--blue);color:#fff;
  font:inherit;font-size:clamp(16px,1.6vw,19px);font-weight:900;padding:12px 24px;
  cursor:pointer;box-shadow:0 4px 0 var(--blue-d);white-space:nowrap}
.qgo:active{transform:translateY(3px);box-shadow:none}
.qerr{text-align:center;font-size:14.5px;font-weight:800;color:var(--pink-d)}
/* A format nudge is advice, not a buzzer — it must not wear the same red
   as "not quite", or writing "twenty one" feels like getting it wrong. */
.qerr.tip{color:var(--blue-d)}
.qchoices{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:8px}
/* [hidden] sets display:none, but an explicit display: wins over it — so
   the choices were on screen from the start and the typing box was
   pointless. Any element given display: here needs this alongside. */
.qchoices[hidden]{display:none}
.qopt{border:2.5px solid var(--line);background:#fff;border-radius:14px;padding:12px 10px;
  font:inherit;font-size:clamp(15px,1.5vw,18px);font-weight:900;color:var(--ink);cursor:pointer}
.qopt:hover{border-color:var(--blue);background:var(--blue-w)}
.qopt.no{border-color:var(--pink);background:var(--pink-w);color:var(--pink-d);opacity:.75}
.qopt.yes{border-color:var(--mint-d);background:var(--mint);color:#04241D}
.qfoot{display:flex;gap:14px;justify-content:center;flex-wrap:wrap}
.qlink{background:none;border:none;font:inherit;font-size:14.5px;font-weight:800;
  color:var(--ink3);cursor:pointer;text-decoration:underline;text-underline-offset:3px;padding:4px}
.qlink:hover{color:var(--ink)}
.qlink.show{color:var(--ink2)}
/* a miss nudges, it does not scold */
.quiz.wrong .qrow{animation:shake .45s var(--ez) 1}

/* the congratulation — a nod, not a firework */
.qwin{display:flex;align-items:center;justify-content:center;gap:13px;
  background:var(--mint-w);border:2.5px solid var(--mint);border-radius:16px;
  padding:12px 18px;position:relative;
  animation:qpop .42s var(--ez) backwards}
@keyframes qpop{from{opacity:0;transform:scale(.9)}to{opacity:1;transform:none}}
.qtick{width:38px;height:38px;border-radius:50%;background:var(--mint);color:#04241D;
  display:grid;place-items:center;font-size:22px;font-weight:900;flex-shrink:0}
.qmsg{display:flex;flex-direction:column;line-height:1.25}
.qmsg b{font-size:clamp(17px,1.8vw,21px);color:var(--mint-d)}
.qmsg span{font-size:14.5px;font-weight:800;color:var(--ink2)}
.qspark{position:absolute;left:50%;top:50%;width:7px;height:7px;border-radius:50%;
  background:var(--mango);pointer-events:none;
  animation:qspark .75s var(--ez) var(--d) backwards}
@keyframes qspark{
  from{opacity:1;transform:rotate(var(--a)) translateX(6px) scale(1)}
  to  {opacity:0;transform:rotate(var(--a)) translateX(92px) scale(.3)}
}
@media (prefers-reduced-motion:reduce){
  .qspark{display:none}
  .qwin,.quiz.wrong .qrow{animation:none}
}
@media (hover:none){ .qgo,.qopt,.qlink{min-height:44px} }

/* ---- per-question audio preload ----
   Puay: "The clip related to that problem should be fetched (with some
   progress bar before the question starts... do like loading... logic."
   A first clip that takes two seconds reads as a broken narrator — this
   project has diagnosed that exact symptom twice. Showing the wait turns
   it into something the child can see finishing. */
.preload{position:fixed;inset:0;z-index:90;display:grid;place-items:center;
  background:var(--paper);animation:plIn .2s var(--ez) backwards}
@keyframes plIn{from{opacity:0}}
.pl-card{width:min(420px,84vw);text-align:center}
.pl-t{font-size:19px;font-weight:800;color:var(--ink);margin-bottom:16px}
.pl-bar{height:12px;border-radius:999px;background:var(--paper2);
  border:1.5px solid var(--line);overflow:hidden}
.pl-bar i{display:block;height:100%;width:0;background:var(--mint);
  border-radius:999px;transition:width .25s var(--ez)}
.pl-n{margin-top:11px;font-size:13.5px;font-weight:700;color:var(--ink3);
  font-variant-numeric:tabular-nums}

/* ---- the receipt for the audio preload ----
   Puay: "I still didn't see the evidence of sound loading... how can I know
   it actually works." On a fast link the progress bar is correctly never
   drawn, so the bar cannot be the evidence. This states the outcome. */
.achip{font-size:11.5px;font-weight:800;letter-spacing:.2px;white-space:nowrap;
  border-radius:999px;padding:3px 9px;border:1.5px solid transparent}
.achip.busy{background:rgba(242,176,30,.18);color:#F2B01E;border-color:rgba(242,176,30,.45)}
.achip.done{background:rgba(0,184,148,.16);color:#5FE3C0;border-color:rgba(0,184,148,.4)}
.achip.live{background:rgba(255,255,255,.08);color:#BCB4CE}
