/* Light or Dark — the app's stylesheet. Moved verbatim out of index.html's
   <style> block; the PANDA-VERSION badge keeps its own inline styles. Bump the
   ?v= in index.html on every edit, so returning browsers fetch the change. */
  :root{
    --ink:#0b0b0b; --paper:#ffffff; --accent:#4fe6e6; --good:#12a150; --bad:#e5484d; --muted:#86868b; --line:#0b0b0b;
    /* Board colors. The "dark" square is a light blue, the way real boards do
       it — it is still plainly the darker of the pair, which is all the drill
       asks, and it reads far better than a slab of black. These two vars are
       the single source of truth: the answer buttons use them too, so LIGHT and
       DARK are literally painted in the colors they name. */
    --sq-light:#e9eff5; --sq-dark:#6d9dc5; --frame:#f5f5f7;
    --mono:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;
    /* Peak Apple: SF Pro on Mac/iPad, graceful fall-back everywhere else.
       --disp for display sizes (≥20px), --text for body runs — on Apple
       devices -apple-system switches optical sizes itself, so the split only
       does work on Windows/Android, where Display metrics look loose at 15px. */
    --disp:-apple-system,BlinkMacSystemFont,"SF Pro Display","SF Pro Text","Helvetica Neue",system-ui,sans-serif;
    --text:-apple-system,BlinkMacSystemFont,"SF Pro Text","Helvetica Neue",system-ui,sans-serif;
    /* The type ramp. Every fixed font-size in the app is one of these six —
       sizes off the ramp read as "slightly wrong" without saying why. The
       poster sizes (.square, h1, .big, .count) stay on their own clamps. */
    --t-caption:11px; --t-body:15px; --t-callout:17px;
    --t-title3:20px; --t-title2:22px; --t-title1:28px;
    --r:12px; --r2:9px;
  }
  *{box-sizing:border-box;-webkit-tap-highlight-color:transparent}
  html,body{margin:0;height:100%}
  body{
    background:var(--paper);color:var(--ink);font-family:var(--disp);
    -webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;
    display:flex;min-height:100dvh;overflow-x:hidden;
  }
  /* Every number in this app is a number that CHANGES — clock, score, combo.
     Proportional digits make them jitter; tabular locks the column. */
  #clock,.scores .cell b,.big,.lb .n,.lb .a{font-variant-numeric:tabular-nums}
  :focus-visible{outline:2.5px solid var(--accent);outline-offset:2px;border-radius:4px}

  /* progress bar pinned to the very top edge */
  #fill{position:fixed;top:0;left:0;height:6px;width:100%;background:var(--accent);z-index:50;
    transition:width .1s linear}
  body.low #fill{background:var(--bad)}
  body.low #clock{color:var(--bad);animation:clockpulse .5s ease-in-out infinite}
  @keyframes clockpulse{0%,100%{opacity:1}50%{opacity:.45}}

  button{font-family:inherit;cursor:pointer;border:none;background:none;color:inherit}
  .mono{font-family:var(--mono);text-transform:uppercase;letter-spacing:.1em;font-size:var(--t-caption);font-weight:600}

  .poster{width:100%;max-width:1060px;margin:0 auto;padding:20px 26px 18px;
    display:flex;flex-direction:column;min-height:100dvh;gap:0}

  /* ---- header: mark · clock · tools. The old strapline row under it said the
     same thing as the logo and the title bar, so it's gone and the clock moved
     up into the space it was wasting. ---- */
  header.top{display:flex;align-items:center;gap:12px}
  /* Half light, half dark — the app in one glyph. The light half is white on a
     white page, so without the ring the mark read as a lopsided black blob. */
  .mark{width:38px;height:38px;background:var(--ink);border-radius:var(--r2);overflow:hidden;
    display:flex;flex:none;padding:0;border:none;cursor:pointer;transition:transform .1s;
    box-shadow:0 0 0 1.5px var(--ink)}
  .mark:active{transform:scale(.94)}
  .mark i{display:block;width:50%;height:100%;background:var(--paper)}

  .clockwrap{flex:1;display:flex;align-items:baseline;justify-content:center;gap:2px;
    font-weight:600;letter-spacing:-.03em;color:var(--ink)}
  #clock{font-size:var(--t-title1)}
  .clockwrap i{font-style:normal;font-size:var(--t-body);color:var(--muted);font-weight:500}
  body.low .clockwrap i{color:var(--bad)}
  /* Survival hearts, riding next to the clock. Lost hearts go hollow rather
     than vanishing, so "how many did I start with" stays readable at a glance. */
  .lives{font-size:var(--t-callout);color:var(--bad);letter-spacing:.08em;line-height:1;align-self:center}
  .lives[hidden]{display:none}
  .lives.hurt{animation:shake .32s}

  .chip{border:1.5px solid var(--ink);border-radius:var(--r2);padding:0 13px;height:38px;background:var(--paper);
    display:flex;align-items:center;gap:8px;flex:none;transition:background .12s,color .12s}
  /* display:flex above beats the UA's [hidden] rule, so hidden chips (the
     zen End button when it isn't a zen round) need it restated. */
  .chip[hidden]{display:none}
  .chip .dot{width:9px;height:9px;border-radius:50%;background:var(--ink);flex:none}
  .chip.blind .dot{background:var(--accent);box-shadow:inset 0 0 0 1.5px var(--ink)}
  .toolbar{display:flex;gap:7px;align-items:center;flex:none}
  .ico{width:38px;height:38px;border:1.5px solid var(--ink);border-radius:var(--r2);background:var(--paper);
    display:grid;place-items:center;font-size:16px;line-height:1;transition:background .12s,color .12s,transform .1s}
  .ico:active,.chip:active{transform:translateY(1px)}
  .ico.on{background:var(--ink);color:var(--paper)}

  .rule{height:1.5px;background:var(--ink);margin:14px 0 0}

  /* ---- the call ---- */
  /* The coordinate is dead-center and plain black. No color box behind it — a
     tint here is a Stroop cue on a question that is literally about color. */
  main.play{position:relative;flex:1;display:flex;flex-direction:column;justify-content:center;
    align-items:center;gap:clamp(18px,3.5vh,34px);padding:10px 0}
  /* The call, the answers and the (classroom) name badge travel together as one
     block, so the wide board layout is a plain two-up and both halves center on
     the same line — no grid-row arithmetic to get wrong. */
  .callcol{display:flex;flex-direction:column;align-items:center;justify-content:center;
    gap:clamp(20px,4vh,44px);width:100%;min-width:0}
  .square{font-family:var(--disp);font-weight:700;letter-spacing:-.045em;line-height:.82;
    font-size:clamp(140px,30vw,290px);text-transform:none;text-align:center;color:var(--ink);
    transition:color .1s}
  /* line-height:.82 crops the line box tighter than the glyph, which is right
     for the blind-mode hero (it kills the dead band above the caps) but wrong
     in board mode: the flex gap is measured off that short box, so the ink of a
     descender-free pair like "e1" still landed on the answer buttons at phone
     width. Board mode gets an honest box; the gap then means what it says. */
  body.board .square{line-height:1}
  .square.in{animation:swap .16s ease-out}
  @keyframes swap{0%{opacity:0;transform:translateY(12px) scale(.96)}100%{opacity:1;transform:none}}
  .square.good{color:var(--good);animation:goodpop .32s cubic-bezier(.2,1.4,.4,1)}
  @keyframes goodpop{0%{transform:scale(1)}45%{transform:scale(1.12)}100%{transform:scale(1)}}
  .square.bad{animation:shake .32s}
  @keyframes shake{0%,100%{transform:translateX(0)}20%{transform:translateX(-16px)}40%{transform:translateX(16px)}60%{transform:translateX(-10px)}80%{transform:translateX(8px)}}

  /* ---- answers ---- */
  .answers{display:grid;grid-template-columns:1fr 1fr;gap:14px;width:100%;max-width:660px}
  .ans{border:1.5px solid var(--ink);border-radius:var(--r);padding:20px 18px;text-align:center;
    display:flex;flex-direction:column;gap:7px;min-height:104px;align-items:center;justify-content:center;
    transition:transform .08s,filter .1s,box-shadow .15s}
  .ans .w{font-family:var(--disp);font-weight:700;font-size:36px;letter-spacing:-.025em;line-height:1}
  .ans .k{font-family:var(--mono);text-transform:uppercase;letter-spacing:.14em;font-size:var(--t-caption);font-weight:600;opacity:.5}
  /* Painted in the board's own two colors — the button IS the swatch. */
  .ans.light{background:var(--sq-light);color:var(--ink);box-shadow:0 2px 10px rgba(0,0,0,.08)}
  .ans.dark{background:var(--sq-dark);color:var(--ink);box-shadow:0 4px 16px rgba(45,85,120,.28)}
  .ans:active{transform:translateY(2px);box-shadow:0 1px 4px rgba(0,0,0,.1)}
  .ans.flash{filter:brightness(1.12)}

  /* ---- juice: floating points + streak fire ---- */
  .pts{position:absolute;left:50%;top:26%;transform:translate(-50%,-50%);pointer-events:none;z-index:6;
    font-family:var(--disp);font-weight:700;font-size:clamp(38px,7vw,60px);color:var(--good);
    white-space:nowrap;animation:rise .72s ease-out forwards}
  .pts.fire{color:var(--ink)}
  .pts.fire b{background:var(--accent);padding:0 .1em;margin-left:.1em}
  @keyframes rise{0%{opacity:0;transform:translate(-50%,-18%) scale(.7)}
    22%{opacity:1;transform:translate(-50%,-50%) scale(1)}
    100%{opacity:0;transform:translate(-50%,-165%) scale(1.05)}}

  /* ---- juice: 3·2·1·GO countdown ---- */
  .count{position:fixed;inset:0;display:grid;place-items:center;z-index:70;background:var(--paper);pointer-events:none}
  .count[hidden]{display:none}
  .count b{font-family:var(--disp);font-weight:700;letter-spacing:-.05em;line-height:1;
    font-size:clamp(130px,34vw,320px);animation:countpop .6s ease-out both}
  .count b.go span{background:var(--accent);padding:0 .09em}
  @keyframes countpop{0%{opacity:0;transform:scale(.4)}28%{opacity:1;transform:scale(1.12)}
    70%{opacity:1;transform:scale(1)}100%{opacity:0;transform:scale(.92)}}

  /* ---- board (lookup aid, board mode only) ----
     It used to be a 380px afterthought stapled under the answers. In board mode
     the board IS the task, so it gets the hero slot: on a wide screen it sits
     beside the call, and it's sized off viewport HEIGHT so it can't outgrow the
     window. Light squares are pure white on a white page, so the board only
     reads as an object if the frame around it is tinted — that's what --frame
     and the shadow are for. No highlight on the called square, ever: finding it
     yourself is the whole exercise. */
  .boardwrap{display:none}
  /* width:100% here is load-bearing. Without it the wrap is an auto-width flex
     item, the frame's own `100%` has nothing definite to resolve against, and
     the whole board collapses to the width of the coordinate strip — which is
     what made it a postage stamp in the first place. */
  body.board .boardwrap{display:flex;justify-content:center;width:100%;min-width:0}
  /* Coordinates scale WITH the board and are set in the display face at full
     ink, not small muted mono. They're the thing a kid actually reads to find
     the square — on a classroom TV a 10px gray label is unreadable from the
     third row, which makes the whole board-mode lookup useless. */
  .boardframe{--gut:max(20px, calc(var(--bsize,420px) * .068));
    --coord:max(13px, calc(var(--bsize,420px) * .05));
    background:var(--frame);border:1.5px solid var(--ink);border-radius:var(--r);
    padding:11px;margin:0 auto;box-shadow:0 12px 34px rgba(0,0,0,.14),0 2px 6px rgba(0,0,0,.07);
    width:min(100%,var(--bsize,420px));
    /* A gutter on ALL FOUR sides, not just the two that carry labels. The
       coordinates only live left and bottom, so with two tracks the board sat
       hard against the top-right of its frame and the whole graphic looked
       lopsided. The top and right gutters stay empty and exist purely to mirror
       the other two, which centers the board inside the frame. */
    display:grid;grid-template-columns:var(--gut) 1fr var(--gut);
    grid-template-rows:var(--gut) 1fr var(--gut);gap:5px}
  .ranks{display:grid;grid-template-rows:repeat(8,1fr);grid-column:1;grid-row:2}
  .files{display:grid;grid-template-columns:repeat(8,1fr);grid-column:2;grid-row:3}
  #board,.hboard{grid-column:2;grid-row:2}
  .ranks div,.files div{display:grid;place-items:center;font-family:var(--disp);
    font-size:var(--coord);font-weight:700;color:var(--ink);letter-spacing:-.02em;
    font-variant-numeric:tabular-nums;line-height:1}
  #board{position:relative;width:100%;aspect-ratio:1/1;display:grid;
    grid-template-columns:repeat(8,1fr);grid-template-rows:repeat(8,1fr);
    border-radius:6px;overflow:hidden;box-shadow:0 0 0 1.5px var(--ink)}
  .sq{position:relative}                 /* the find-drill feedback overlays inset into this */
  .sq.l{background:var(--sq-light)} .sq.d{background:var(--sq-dark)}

  /* ---- FIND drill ----
     The board stops being a lookup aid and becomes the answer control, so it
     gets a pointer and the answer buttons go away entirely. Feedback lands ON
     the square: green where you tapped if it was right, red if it wasn't, plus
     a cyan ring on the square you were actually asked for. At 24s a round
     there's no time to go hunting for it afterwards, and a miss that doesn't
     show you the answer teaches nothing. */
  body.find .sq{cursor:pointer}
  body.find .answers{display:none}
  .typerow{display:none}
  body.find .typerow{display:flex;align-items:baseline;justify-content:center;gap:12px;
    width:100%;max-width:660px;min-height:34px}
  .typebuf{font-family:var(--disp);font-weight:700;font-size:26px;letter-spacing:.02em;
    text-transform:lowercase;min-width:2.2em;text-align:right}
  .typebuf.empty::before{content:'·';color:var(--muted)}
  .typehint{font-size:var(--t-caption);letter-spacing:.1em;text-transform:uppercase;color:var(--muted)}
  /* Blind here means NO COORDINATES, not no board — see applyBoard(). The
     gutters keep their space so the board doesn't resize when you flip it. */
  body.nolabels .ranks div, body.nolabels .files div{visibility:hidden}
  .sq.hit::after,.sq.miss::after,.sq.showme::after{content:'';position:absolute;inset:0;
    animation:sqflash .42s ease-out both}
  .sq.hit::after{background:var(--good)}
  .sq.miss::after{background:var(--bad)}
  .sq.showme::after{box-shadow:inset 0 0 0 4px var(--accent)}
  @keyframes sqflash{0%{opacity:0}18%{opacity:.85}100%{opacity:0}}
  @media(prefers-reduced-motion:reduce){.sq.hit::after,.sq.miss::after,.sq.showme::after{animation-duration:.01s}}

  /* Wide screen, board on: ONE centered column — board on top, the called
     coordinate under it, the answers under that. Side-by-side put the board and
     the buttons on separate horizontal halves, so answering meant a sideways
     eye-flick away from the thing you just looked at; stacked, the whole task
     reads straight down the middle. The cost is vertical room, so --bsize is
     driven off HEIGHT (not 46vw) and the coordinate steps down again — the board
     has to give way first, because the buttons and the call have hard floors.
     --bsize stays on body: the frame's `width:min(100%,var(--bsize))` needs it
     resolvable from an ancestor, and losing it collapses the board to a stamp. */
  @media(min-width:900px){
    body.board{--bsize:calc(min(80vw,60vh,680px) * var(--bscale,1))}
    body.board main.play{gap:clamp(14px,2.4vh,26px)}
    body.board .callcol{gap:clamp(12px,2.2vh,24px)}
    body.board .square{font-size:clamp(56px,7vh,104px)}
    body.board .answers{max-width:max(560px,var(--bsize))}
    body.board .ans{min-height:84px;padding:15px 18px}
  }
  /* Wide but SHORT (a laptop at 900×600, a projector at 1024×768): stacking
     spends height, and at 50vh the board pushed the footer off the bottom. The
     board yields first, then the call — the buttons keep a thumb-sized target. */
  @media(min-width:900px) and (max-height:760px){
    body.board{--bsize:calc(min(80vw,50vh,560px) * var(--bscale,1))}
    body.board .square{font-size:clamp(44px,6vh,80px)}
    body.board .ans{min-height:70px;padding:11px 16px}
  }
  /* Narrow, board on: call up top, board to look it up on, answers under the
     thumbs. Everything shrinks so all three fit without a scroll. */
  @media(max-width:899px){
    body.board{--bsize:calc(min(94vw,48vh,560px) * var(--bscale,1))}
    body.board .square{font-size:clamp(56px,12vw,96px)}
    body.board main.play{gap:14px}
    body.board .callcol{gap:18px}
    body.board .ans{min-height:78px;padding:13px}
    body.board .ans .w{font-size:25px}
    body.board .ans .k{display:none}
  }

  /* The advanced drawer on the start sheet: indented and hairlined so the two
     pickers read as one optional group hanging off the toggle, not as two more
     equal-weight panels competing with Start. */
  .adv{display:flex;flex-direction:column;gap:10px;border-left:2px solid var(--line);
    padding-left:10px;margin:2px 0 4px}

  /* ---- miss heatmap ----
     Reuses .boardframe wholesale (frame, gutters, coordinate sizing) at a
     smaller --bsize, so the heatmap is unmistakably the same board you drill on.
     The tint is a red overlay on top of the real square color rather than a
     replacement — you can still see whether a hot square is light or dark,
     which is usually the actual pattern ("all my dark queenside squares"). */
  /* --bsize must be a PLAIN LENGTH, never a percentage. It feeds --gut, which
     feeds both grid axes, and a percentage resolves against the inline size in
     one axis and against an indefinite block size in the other — so the row
     tracks came out 20px where the columns came out 20.125px, the middle row
     mis-sized to 218px against a 245px board, and the heatmap spilled 27px down
     over its own file letters. The frame still shrinks on a narrow screen via
     width:min(100%,var(--bsize)); only the gutter maths is now axis-agnostic. */
  .boardframe.mini{--bsize:320px;margin:0 0 10px;
    box-shadow:0 5px 16px rgba(0,0,0,.10),0 1px 3px rgba(0,0,0,.06)}
  .hboard{position:relative;width:100%;aspect-ratio:1/1;display:grid;
    grid-template-columns:repeat(8,1fr);grid-template-rows:repeat(8,1fr);
    border-radius:6px;overflow:hidden;box-shadow:0 0 0 1.5px var(--ink)}
  .hs{position:relative;display:grid;place-items:center}
  .hs.l{background:var(--sq-light)} .hs.d{background:var(--sq-dark)}
  .hs::after{content:'';position:absolute;inset:0;background:var(--bad);opacity:var(--h,0)}
  .hs b{position:relative;z-index:1;font-family:var(--disp);font-weight:700;
    font-size:max(11px,calc(var(--bsize,320px) * .042));color:var(--tc,var(--ink));
    font-variant-numeric:tabular-nums}
  /* mastery medals: an inset ring over the real square color, so light/dark
     stays readable underneath — the same rule the heatmap's red overlay follows */
  .hs.t1{box-shadow:inset 0 0 0 3px #b07a3c}
  .hs.t2{box-shadow:inset 0 0 0 3px #97a1ac}
  .hs.t3{box-shadow:inset 0 0 0 3px #d4a017}
  .hs.t3::after{content:'';position:absolute;inset:0;background:#d4a017;opacity:.28}

  /* ---- pair drill (Same or Different?) ----
     The answer buttons go NEUTRAL here. A white button and a black button on a
     question about color would be read as the answer — the same Stroop trap
     the coordinate avoids by staying plain black. SAME/DIFFERENT aren't colors,
     so they're told apart by fill weight, not by light vs dark. */
  body.pair .ans.light{background:var(--paper);color:var(--ink);box-shadow:0 2px 10px rgba(0,0,0,.08)}
  body.pair .ans.dark{background:var(--accent);color:var(--ink);box-shadow:0 2px 10px rgba(0,0,0,.08)}
  body.pair .ans .w{font-size:30px}
  body.pair .square{font-size:clamp(64px,13vw,150px);letter-spacing:-.035em}
  body.pair .square .sep{color:var(--muted);font-weight:400;padding:0 .06em}
  @media(min-width:900px){
    body.board.pair .square{font-size:clamp(44px,5.4vh,80px)}
  }
  @media(max-width:899px){
    body.board.pair .square{font-size:clamp(40px,9vw,74px)}
    body.pair .ans .w{font-size:22px}
  }
  @media(max-width:600px){
    body.pair .ans .w{font-size:21px}
  }

  /* ---- footer ---- */
  footer.bottom{display:flex;justify-content:space-between;align-items:flex-end;gap:12px;
    margin-top:14px;padding-top:12px;border-top:1.5px solid var(--ink)}
  .scores{display:flex;gap:26px}
  .scores .cell b{font-family:var(--disp);font-weight:700;font-size:var(--t-title1);letter-spacing:-.025em;display:block;line-height:1}
  .scores .cell.hot b{color:var(--ink);background:var(--accent);padding:0 6px;margin-left:-6px}
  @keyframes pop{0%{transform:scale(1)}45%{transform:scale(1.22)}100%{transform:scale(1)}}
  .scores .cell.bump b{animation:pop .38s cubic-bezier(.2,1.4,.4,1)}

  /* ---- overlay ---- */
  .overlay{position:fixed;inset:0;background:var(--paper);display:grid;place-items:center;padding:24px;z-index:80}
  .overlay[hidden]{display:none}
  .sheet{width:100%;max-width:560px}
  .sheet .kick{color:var(--muted);margin-bottom:14px}
  /* Uppercase can't take the same crush as mixed case — the caps have no
     descenders to fill the gaps, so -.045em reads as touching. One step looser. */
  .sheet h1{font-family:var(--disp);font-weight:700;letter-spacing:-.035em;text-transform:uppercase;
    font-size:clamp(46px,11vw,84px);line-height:.9;margin:0 0 4px}
  /* An INLINE element's background box is sized from the font (~1.15em of
     ascent + descent), not from line-height. These headings run at .85–.95, so
     the cyan block was a quarter of an em taller than its own line and bled up
     over the unhighlighted line above — "LIGHT OR" sat inside the box belonging
     to "DARK." inline-block sizes the box from line-height instead, and .82
     hugs the caps: measured, the block top lands 1.5px BELOW the baseline of the
     line above, where inheriting .9 left it 0.5px above, i.e. touching.
     line-height on an inline-block moves the box without moving the text, so no
     word shifts a pixel — only the cyan does. */
  .hl{display:inline-block;line-height:.82}
  .sheet h1 .hl{background:var(--accent);padding:0 .05em}
  .sheet .lead{font-family:var(--text);font-size:var(--t-callout);color:var(--ink);margin:14px 0 22px;max-width:44ch;line-height:1.45}
  .sheet .lead b{font-weight:700}
  .grid2{display:grid;grid-template-columns:1fr 1fr;gap:0;border:1.5px solid var(--ink);border-radius:var(--r);
    overflow:hidden;margin-bottom:18px}
  .grid2 .c{padding:13px 15px}
  .grid2 .c+.c{border-left:1.5px solid var(--ink)}
  .grid2 .c b{display:block;font-family:var(--disp);font-weight:700;font-size:var(--t-callout);margin-bottom:4px}
  .grid2 .c span{font-family:var(--mono);font-size:var(--t-caption);letter-spacing:.08em;color:var(--muted)}
  .pickrow{display:grid;grid-template-columns:1fr 1fr;gap:0;border:1.5px solid var(--ink);border-radius:var(--r);
    overflow:hidden;margin-bottom:18px}
  .pickrow button{padding:14px 15px;text-align:left;background:var(--paper)}
  .pickrow button+button{border-left:1.5px solid var(--ink)}
  .pickrow button b{display:block;font-family:var(--disp);font-weight:700;font-size:var(--t-callout)}
  .pickrow button small{font-family:var(--mono);font-size:var(--t-caption);letter-spacing:.08em;color:var(--muted)}
  .pickrow button.sel{background:var(--accent)}
  .pickrow button.sel small{color:var(--ink)}
  /* ---- daily challenge ---- */
  .dailycard{display:flex;align-items:center;text-align:left;gap:12px;width:100%;
    border:1.5px solid var(--ink);border-radius:var(--r);padding:12px 15px;margin-bottom:18px;
    background:var(--paper);transition:transform .08s}
  .dailycard:active{transform:translateY(1px)}
  .dailycard .em{flex:none;display:flex;align-items:center;width:22px;height:22px}
  .dailycard b{display:block;font-family:var(--disp);font-weight:700;font-size:var(--t-callout)}
  .dailycard small{font-family:var(--mono);font-size:var(--t-caption);letter-spacing:.08em;color:var(--muted);text-transform:uppercase}
  .dailycard.done{background:var(--accent)}
  .dailycard.done small{color:var(--ink)}
  /* The daily IS the front door, so on the start sheet it is painted as the
     primary button rather than as one more outlined card among several. Same
     ink fill and padding as .cta, so nothing else on the screen outranks it. */
  .dailycard.hero{background:var(--ink);color:var(--paper);padding:17px 18px;margin-bottom:10px}
  .dailycard.hero b{font-size:var(--t-title3);text-transform:uppercase;letter-spacing:.02em}
  /* var(--paper) at reduced opacity, not a hardcoded white — --ink/--paper are
     the app's only two ground colors and this card must follow them. */
  .dailycard.hero small{color:var(--paper);opacity:.62}
  .dailycard.hero .em{width:26px;height:26px}
  /* The daily keeps its own bottom margin when it is NOT the hero, so the sheet
     never needs a spacer element between it and whatever sits above. */
  .dailycard.under{margin-top:18px}
  .dgrid{font-size:var(--t-callout);line-height:1.5;letter-spacing:.08em;margin:0 0 18px}
  /* The high-score board is the second thing anyone wants, so its ghost is ink
     rather than muted — a ghost among ghosts would hide it. */
  .ghost.strong{color:var(--ink);border:1.5px solid var(--ink);border-radius:var(--r);margin-top:0}
  /* ---- player name ----
     No sign-up, no Join: a generated name is assigned on sight and this row just
     reports it, with 🎲 to reroll. It renders hidden and is only revealed once
     Firebase actually hands a name back, so a blocked backend shows nothing at
     all rather than an identity the board will never receive. */
  /* The 🎲 sits next to the name it rerolls, not shoved against the far edge of
     the sheet — the name stretched to fill and left the button stranded 200px
     away from the only thing it acts on. */
  .namerow{display:flex;gap:8px;align-items:center;margin:14px 0 4px}
  .namerow[hidden]{display:none}
  .namerow .nlabel{font-size:var(--t-caption);letter-spacing:.1em;text-transform:uppercase;color:var(--muted);flex:none}
  .namerow .nname{flex:0 1 auto;font-family:var(--disp);font-weight:700;font-size:var(--t-callout);
    letter-spacing:-.02em;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  .namerow .rl-btn{flex:none}
  /* Test mode is never silent. It sits above every overlay (z-index beats the
     sheet at 80 and the countdown at 70) so there is no state of the app in
     which you are in a dry run and cannot tell. */
  .testbar{position:fixed;top:0;left:0;right:0;z-index:200;background:var(--bad);color:#fff;
    font-family:var(--mono);font-size:11px;letter-spacing:.12em;text-transform:uppercase;
    padding:5px 10px;text-align:center;pointer-events:none}
  body.testing{padding-top:24px}
  /* One rule for every drawn mark: currentColor and a size set by its holder. */
  .ico-svg{display:block;width:100%;height:100%;color:inherit}
  .ico-btn .ico-svg{width:17px;height:17px}
  /* A mark sitting in a line of text: sized off the type around it and dropped
     to the optical centre of lowercase, where an emoji used to sit by itself. */
  .mark-i{display:inline-block;width:1.25em;height:1.25em;vertical-align:-.26em;margin-right:.15em}
  .namenote{font-family:var(--mono);font-size:var(--t-caption);letter-spacing:.06em;color:var(--muted);margin:0 0 4px}
  .lb li.me .who{background:var(--accent);padding:0 5px;margin-left:-5px}
  /* Uppercase at button size wants air, not crush — positive tracking here. */
  .cta{display:block;width:100%;background:var(--ink);color:var(--paper);border-radius:var(--r);padding:18px;
    font-family:var(--disp);font-weight:700;font-size:var(--t-title3);text-transform:uppercase;letter-spacing:.02em;transition:transform .08s}
  .cta:active{transform:translateY(2px)}
  .ghost{display:block;width:100%;margin-top:9px;padding:12px;background:none;color:var(--muted);
    font-family:var(--mono);font-size:var(--t-caption);letter-spacing:.1em;text-transform:uppercase}
  .big{font-family:var(--disp);font-weight:700;font-size:clamp(72px,20vw,150px);line-height:.85;letter-spacing:-.04em;margin:2px 0}
  .big .hl{background:var(--accent);padding:0 .05em}
  .gradeline{font-family:var(--disp);font-weight:700;text-transform:uppercase;letter-spacing:.01em;
    font-size:clamp(26px,6vw,44px);line-height:.95;margin:0 0 10px}
  .gradeline .hl{background:var(--accent);padding:0 .06em}
  .resline{font-family:var(--mono);font-size:var(--t-caption);letter-spacing:.06em;color:var(--muted);margin-bottom:18px;text-transform:uppercase}
  .miss{font-family:var(--mono);font-size:var(--t-caption);letter-spacing:.04em;color:var(--ink);margin:0 0 18px;line-height:1.7}
  .miss .x{background:var(--ink);color:var(--paper);padding:1px 6px;margin:2px 3px 2px 0;display:inline-block}
  .best{display:inline-block;background:var(--accent);color:var(--ink);border:1.5px solid var(--ink);
    font-family:var(--mono);font-size:var(--t-caption);letter-spacing:.1em;text-transform:uppercase;padding:5px 11px;margin-bottom:16px}
  /* ---- the new-best photo ----
     Only ever rendered on a personal best, so it stays a reward rather than
     wallpaper. Framed like every other panel on the sheet (same hairline, same
     radius) so it reads as part of the app and not as a pasted-in screenshot.
     aspect-ratio holds the box open while the file arrives, or the whole sheet
     jumps 300px the moment it decodes. */
  .bestpic{width:100%;aspect-ratio:1024/559;border:1.5px solid var(--ink);border-radius:var(--r);
    overflow:hidden;margin:0 0 18px;background:var(--frame);animation:bestpop .34s ease-out both}
  .bestpic img{display:block;width:100%;height:100%;object-fit:cover}
  /* A quiet fade up, not a springy scale-pop. The photo is already the loudest
     thing on the sheet; bouncing it in on top of that is one flourish too many. */
  @keyframes bestpop{0%{opacity:0;transform:translateY(8px)}100%{opacity:1;transform:none}}
  @media(prefers-reduced-motion:reduce){.bestpic{animation:none}}

  /* ---- classroom: name badge (projector feature — NOT hidden in lecture) ---- */
  .badge{display:none;font-family:var(--disp);font-weight:700;text-transform:uppercase;letter-spacing:-.01em;
    font-size:clamp(28px,6.5vw,60px);line-height:1;background:var(--ink);color:var(--paper);
    padding:.14em .34em;border-radius:5px;max-width:90vw;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  .badge.show{display:inline-block}
  .badge.in{animation:swap .18s ease-out}
  body.lecture .badge{font-size:clamp(44px,8vw,92px)}

  /* cadence picker — three across, reusing .pickrow */
  .pick3{grid-template-columns:1fr 1fr 1fr}

  /* ---- classroom: roster modal ---- */
  .rmodal{position:fixed;inset:0;background:var(--paper);z-index:85;display:grid;place-items:center;padding:24px}
  .rmodal[hidden]{display:none}
  .rsheet{width:100%;max-width:460px}
  .rlist{list-style:none;margin:0 0 14px;padding:0;max-height:48vh;overflow:auto;border:1.5px solid var(--ink);border-radius:var(--r)}
  .rl-empty{padding:16px;color:var(--muted)}
  .rl-row{display:flex;align-items:center;gap:10px;padding:9px 12px;border-bottom:1.5px solid var(--line)}
  .rl-row:last-child{border-bottom:none}
  .rl-i{width:22px;color:var(--muted);flex:none}
  .rl-name{flex:1;font-family:var(--text);font-weight:700;font-size:var(--t-callout);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  .rl-edit{flex:1;min-width:0;border:1.5px solid var(--accent);border-radius:3px;padding:6px 8px;
    font-family:var(--text);font-weight:700;font-size:var(--t-body)}
  .rl-btn{flex:none;width:34px;height:34px;border:1.5px solid var(--ink);border-radius:3px;background:var(--paper);
    font-size:14px;display:grid;place-items:center}
  .rl-btn:active{transform:translateY(1px)}
  .raddrow{display:flex;gap:8px;margin-bottom:12px}
  .radd{flex:1;min-width:0;border:1.5px solid var(--ink);border-radius:3px;padding:12px;
    font-family:var(--text);font-size:var(--t-body)}
  .rbtnadd{flex:none;border:1.5px solid var(--ink);border-radius:3px;padding:0 16px;
    font-family:var(--text);font-weight:700}

  /* ---- classroom: leaderboard ---- */
  .lb{list-style:none;margin:0 0 16px;padding:0;text-align:left;max-height:44vh;overflow:auto}
  .lb li{display:flex;align-items:center;gap:12px;padding:9px 0;border-bottom:1.5px solid var(--line)}
  .lb li:last-child{border-bottom:none}
  .lb .rank{font-family:var(--mono);color:var(--muted);width:24px;flex:none}
  .lb .who{flex:1;font-family:var(--text);font-weight:700;font-size:var(--t-callout);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  .lb .n{font-family:var(--disp);font-weight:700;font-size:var(--t-title2)}
  .lb .a{font-family:var(--mono);font-size:var(--t-caption);color:var(--muted);width:46px;text-align:right;flex:none}
  .lb li:first-child .n{background:var(--accent);padding:0 6px}

  /* ---- lecture mode (camp TV) ---- */
  body.lecture .poster{max-width:1280px}
  body.lecture .square{font-size:clamp(160px,32vw,340px)}
  body.lecture .ans .w{font-size:52px}
  body.lecture .scores .cell b{font-size:48px}
  body.lecture #clock{font-size:38px}
  body.lecture .clockwrap i{font-size:20px}
  body.lecture .ans .k{display:none}
  /* Stacked, so lecture's board is capped off height too — the 62vh it used to
     take when it lived in its own column now has the call and the buttons under
     it and would push them off the TV. */
  body.lecture.board{--bsize:calc(min(76vw,50vh,640px) * var(--bscale,1))}
  body.lecture.board .square{font-size:clamp(64px,8vh,132px)}

  /* Phone: the header is a chip + four icons + a clock and it overflowed by
     55px at 360. Everything in it steps down a size; under 440 the mode chip
     drops its word and keeps the dot, which says the same thing. */
  @media(max-width:600px){
    .poster{padding:13px 14px 12px}
    .answers{gap:10px}
    .ans .w{font-size:var(--t-title1)}
    .ans .k{display:none}
    .toolbar{gap:6px}
    .mark{width:34px;height:34px}
    .ico{width:34px;height:34px;font-size:15px;border-radius:8px}
    .chip{height:34px;padding:0 10px;gap:7px;border-radius:8px;letter-spacing:.06em}
    .chip .dot{width:8px;height:8px}
    #clock{font-size:var(--t-title2)}
    .clockwrap i{font-size:var(--t-caption)}
    .grid2,.pickrow{grid-template-columns:1fr 1fr}
    .scores{gap:18px}
    .scores .cell b{font-size:var(--t-title2)}
  }
  @media(max-width:440px){
    header.top{gap:8px}
    .chip #modeLabel{display:none}
    .chip{padding:0 10px}
  }
  /* Smallest phones: a chip and five icons don't fit at 320. Everything in the
     header takes one more step down rather than any control being dropped —
     the voice and lecture toggles are exactly what a teacher reaches for. */
  @media(max-width:380px){
    .poster{padding:12px 10px 10px}
    header.top{gap:6px}
    .toolbar{gap:5px}
    .mark{width:30px;height:30px}
    .ico{width:30px;height:30px;font-size:14px;border-radius:7px}
    .chip{height:30px;padding:0 8px;border-radius:7px}
    #clock{font-size:var(--t-title3)}
    .clockwrap i{font-size:var(--t-caption)}
  }

  /* Screen-reader-only text: the sparse status line lives here. */
  .vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;
    clip:rect(0 0 0 0);white-space:nowrap;border:0}

  /* One consolidated reduced-motion pass. Two flourishes were already covered
     inline (sqflash, bestpic); everything else that pulses, pops, rises or
     shakes is neutralized here. Color changes stay — they carry the verdict. */
  @media(prefers-reduced-motion:reduce){
    body.low #clock{animation:none}
    .square.in,.square.good,.square.bad{animation:none}
    /* animation:none, not a .01s duration: rise and countpop both END at
       opacity 0, so a near-zero run would leave the element invisible. */
    .pts{animation:none}
    .count b{animation:none}
    .scores .cell.bump b{animation:none}
    .badge.in{animation:none}
    .lives.hurt{animation:none}
    .dailycard:active,.cta:active,.ans:active,.mark:active{transform:none}
  }
