/* ============================================================
   numrist
   Newspaper puzzle-book styling: warm paper, Fraunces display,
   IBM Plex for body and data.
   ============================================================ */

:root{
  --paper:#EDE6D6;
  --card:#F5F0E4;
  --ink:#22201B;
  --ink-soft:#55503F;
  --line:#D9D0BA;
  --rule:#3A362E;
  --cageink:#6E9A94;
  --add:#3E7D66;
  --add-text:#38705C; /* darkened further to clear 4.5:1 where --add/--ok is used as small text (op-add, stat/board readouts) */
  --sub:#B2472A;
  --mul:#B3811F;      /* darkened just enough to clear 3:1 for the selection/focus outline it draws */
  --mul-text:#835F17; /* darkened further to clear 4.5:1 where --mul is used as small text (op-mul, My Board) */
  --div:#8C5A8C;
  --ok:#3E7D66;
  --bad:#B2472A;
  --danger:#AD4329;   /* destructive action; on-accent text clears AA on this */
  --arctic:#3E71A0;   /* matt arctic blue — My Board */
  --arctic-deep:#30587D;
  --rank:#90622E;     /* bronze — Rankings, distinct from --mul's brighter gold */
  --rank-deep:#7E5729;
  --account:#5B5A9E;  /* purple — My Account, its own tone rather than reusing --div/--note */
  --account-deep:#49497F;
  --safe:#3E7D66;     /* the reassuring half of a confirmation */
  --note:#5A4462;
  --on-accent:#F7F4EC; /* fixed light text for saturated accent buttons (danger/arctic/rank/account) that don't invert in dark mode */
  --fill-wash:color-mix(in srgb,var(--arctic) 10%,transparent); /* player-filled cage-cell tint */
  --shadow-sm:0 1px 3px color-mix(in srgb,var(--ink) 12%,transparent); /* pad cells, lifted action buttons — dark shadow in light mode, soft glow in dark */
  --shadow-md:0 2px 5px color-mix(in srgb,var(--ink) 22%,transparent); /* primary CTA — heavier lift */
  --disabled-bg:color-mix(in srgb,var(--paper) 88%,var(--ink) 12%); /* inert button fill, distinct from --paper at rest */
  --rule-soft:color-mix(in srgb,var(--rule) 80%,var(--paper)); /* board frame + box-grid lines — same rule, blended down so the heavy structural lines read as calm rather than stark */
  --board-radius:10px; /* outer board frame only — the interior box/cell grid stays square, sudoku logic needs straight lines */

  --n:9;
  --notecols:3;
  --board-max:560px;
}

/* Only reached with JS disabled — the toggle stamps data-theme on <html> for
   everyone else, defaulting to light regardless of this (see initTheme in
   ui.js and the inline script in <head> that applies a stored choice before
   first paint). Kept as a no-JS fallback, not the source of truth. */
@media (prefers-color-scheme:dark){
  :root{
    --paper:#1C1A15;
    --card:#262219;
    --ink:#F0E9D8;
    --ink-soft:#C9C0A6;
    --line:#3A362B;
    --rule:#D8CCAC;
    --cageink:#6FA89D;
    --add:#6FA893;
    --add-text:#6FA893;
    --sub:#D98A6B;
    --mul:#D9B15C;
    --mul-text:#D9B15C;
    --div:#B48CC2;
    --ok:#6FA893;
    --bad:#D98A6B;
    --danger:#9C5332;
    --arctic:#3C6D9A;
    --arctic-deep:#31597E;
    --rank:#93652F;
    --rank-deep:#795327;
    --account:#5A599C;
    --account-deep:#4A4980;
    --safe:#6FA893;
    --note:#C793D6;
    --on-accent:#FBF7ED;
    --fill-wash:color-mix(in srgb,var(--arctic) 14%,transparent);
  }
}

/* The manual toggle's actual source of truth — higher specificity than the
   bare :root above, so it overrides the OS preference in both directions. */
:root[data-theme="dark"]{
  --paper:#1C1A15;
  --card:#262219;
  --ink:#F0E9D8;
  --ink-soft:#C9C0A6;
  --line:#3A362B;
  --rule:#D8CCAC;
  --cageink:#6FA89D;
  --add:#6FA893;
  --add-text:#6FA893;
  --sub:#D98A6B;
  --mul:#D9B15C;
  --mul-text:#D9B15C;
  --div:#B48CC2;
  --ok:#6FA893;
  --bad:#D98A6B;
  --danger:#9C5332;
  --arctic:#3C6D9A;
  --arctic-deep:#31597E;
  --rank:#93652F;
  --rank-deep:#795327;
  --account:#5A599C;
  --account-deep:#4A4980;
  --safe:#6FA893;
  --note:#C793D6;
  --on-accent:#FBF7ED;
  --fill-wash:color-mix(in srgb,var(--arctic) 14%,transparent);
}
:root[data-theme="light"]{
  --paper:#EDE6D6;
  --card:#F5F0E4;
  --ink:#22201B;
  --ink-soft:#55503F;
  --line:#D9D0BA;
  --rule:#3A362E;
  --cageink:#6E9A94;
  --add:#3E7D66;
  --add-text:#38705C;
  --sub:#B2472A;
  --mul:#B3811F;
  --mul-text:#835F17;
  --div:#8C5A8C;
  --ok:#3E7D66;
  --bad:#B2472A;
  --danger:#AD4329;
  --arctic:#3E71A0;
  --arctic-deep:#30587D;
  --rank:#90622E;
  --rank-deep:#7E5729;
  --account:#5B5A9E;
  --account-deep:#49497F;
  --safe:#3E7D66;
  --note:#5A4462;
  --on-accent:#F7F4EC;
  --fill-wash:color-mix(in srgb,var(--arctic) 10%,transparent);
}

*{box-sizing:border-box;}
html,body{margin:0;padding:0;}

html{-webkit-text-size-adjust:100%;}

body{
  background:var(--paper);
  background-image:radial-gradient(circle at 1px 1px,rgba(0,0,0,0.035) 1px,transparent 0);
  background-size:14px 14px;
  color:var(--ink);
  font-family:'IBM Plex Sans',system-ui,-apple-system,'Segoe UI',sans-serif;
  min-height:100vh;
  min-height:100dvh;
  padding:0 0 48px;
  -webkit-tap-highlight-color:transparent;
}
/* the grain is a dark dot on light paper; a dark dot is invisible on dark
   paper, so dark mode gets a light one instead */
@media (prefers-color-scheme:dark){
  body{background-image:radial-gradient(circle at 1px 1px,rgba(255,255,255,0.035) 1px,transparent 0);}
}
:root[data-theme="dark"] body{background-image:radial-gradient(circle at 1px 1px,rgba(255,255,255,0.035) 1px,transparent 0);}
:root[data-theme="light"] body{background-image:radial-gradient(circle at 1px 1px,rgba(0,0,0,0.035) 1px,transparent 0);}

button{font:inherit;color:inherit;touch-action:manipulation;}

.wrap{
  max-width:760px;margin:0 auto;
  padding:0 16px;
  padding-left:max(16px,env(safe-area-inset-left));
  padding-right:max(16px,env(safe-area-inset-right));
}

.sr-only{
  position:absolute;width:1px;height:1px;padding:0;margin:-1px;
  overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0;
}
.skip{
  position:absolute;left:-9999px;top:0;z-index:100;
  background:var(--rule);color:var(--card);padding:10px 14px;
  font-size:0.85rem;text-decoration:none;
}
.skip:focus{left:8px;top:8px;}

/* ---------------------------------------------------------- top bar */
.topbar{
  display:grid;
  grid-template-columns:1fr auto 1fr;
  align-items:center;
  gap:10px;
  border-bottom:3px solid var(--rule);
  padding:14px 0 11px;
  margin-bottom:14px;
}
.brand{
  font-family:'Fraunces',Georgia,serif;font-weight:600;
  font-size:clamp(1.35rem,4vw,2.3rem);line-height:0.95;
  letter-spacing:-0.01em;text-align:center;white-space:nowrap;
  margin:0;display:flex;align-items:center;justify-content:center;gap:0.16em;
  min-width:0;
}
.brand-dot{color:var(--mul);}
.brand-mark{width:0.82em;height:0.82em;flex-shrink:0;}
/* overflow:hidden is the backstop: bar-l/bar-r sit in the grid's two 1fr
   side columns either side of the centered brand (min-width:0 already lets
   those tracks shrink below content size), so anything that still doesn't
   fit must clip at its own edge rather than bleed sideways into the logo —
   see .clock below for the one child actually liable to overflow. */
.bar-l{justify-self:start;min-width:0;display:flex;align-items:center;gap:8px;overflow:hidden;}
.bar-r{justify-self:end;display:flex;align-items:center;gap:8px;min-width:0;overflow:hidden;}
/* Under ~430px (most current Android phones, incl. the common ~360-412
   CSS-px bucket), bar-r alone (pause + clock + more) already has three
   things to fit into the same 1fr share bar-l's two get — tight enough
   that even a perfectly normal "00:00" clock leaves .pausebtn measurably
   overlapping .brand without this. Shaving a few px off the wordmark and
   the tightest controls closes that gap; nothing above this width, where
   there was already room to spare, is touched. */
@media (max-width:430px){
  .brand-mark{display:none;}
  .brand{font-size:1.15rem;}
  .pausebtn{width:26px;height:26px;}
  .pausebtn svg{width:16px;height:16px;}
  .bar-l{gap:5px;}
  .bar-r{gap:5px;}
}
.clock{
  font-family:'IBM Plex Mono',ui-monospace,monospace;
  font-size:clamp(0.9rem,2.6vw,1.05rem);font-weight:600;
  letter-spacing:0.02em;font-variant-numeric:tabular-nums;
  /* A session clock is normally 5 characters ("00:00"); this just keeps a
     freak long value (a very long uninterrupted sitting, or a stale/buggy
     elapsed time) from pushing the pause button into the logo instead of
     truncating in its own corner. */
  max-width:7ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
.iconbtn{
  display:flex;align-items:center;gap:7px;
  font-weight:600;font-size:0.78rem;
  padding:8px 11px;cursor:pointer;border-radius:3px;
  border:1.5px solid var(--rule);background:var(--card);
  max-width:100%;
}
.iconbtn:hover{background:color-mix(in srgb,var(--mul) 18%,transparent);}
.iconbtn .lbl{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.glyph3{display:flex;flex-direction:column;gap:2.5px;width:15px;flex-shrink:0;}
.glyph3 i{display:block;height:2px;background:var(--ink);border-radius:1px;}
.glyph3.steps i:nth-child(1){width:100%;}
.glyph3.steps i:nth-child(2){width:70%;}
.glyph3.steps i:nth-child(3){width:40%;}

.theme-toggle{
  flex-shrink:0;padding:2px;border-radius:999px;cursor:pointer;
  background:none;border:none;
}
.theme-toggle-track{
  position:relative;display:block;width:42px;height:24px;
  background:var(--line);border-radius:999px;
  transition:background 0.2s;
}
.theme-toggle-thumb{
  position:absolute;top:2px;left:2px;width:20px;height:20px;
  border-radius:50%;background:var(--card);
  box-shadow:0 1px 2px color-mix(in srgb,var(--ink) 35%,transparent);
  display:flex;align-items:center;justify-content:center;
  transition:transform 0.2s;
}
.theme-toggle-thumb svg{
  position:absolute;width:13px;height:13px;
  transition:opacity 0.15s;
}
.ti-sun{fill:none;stroke:var(--mul);stroke-width:2;stroke-linecap:round;opacity:1;}
.ti-moon{opacity:0;}
.ti-moon path{fill:var(--ink-soft);}
.theme-toggle[aria-checked="true"] .theme-toggle-track{background:var(--rule);}
.theme-toggle[aria-checked="true"] .theme-toggle-thumb{transform:translateX(18px);}
.theme-toggle[aria-checked="true"] .ti-sun{opacity:0;}
.theme-toggle[aria-checked="true"] .ti-moon{opacity:1;}
@media (prefers-reduced-motion:reduce){
  .theme-toggle-track,.theme-toggle-thumb,.theme-toggle-thumb svg{transition:none;}
}


/* Pause, signed-in only (see b-pause's click handler) — the glyph itself
   stays visible to a guest so the perk is discoverable, not hidden. Idle
   state borrows --mul (same warm gold already doing this job for the sun
   icon next to it); the resume triangle borrows --ok, the app's existing
   "correct/go" green, since that is exactly what tapping it does. */
.pausebtn{
  flex-shrink:0;width:30px;height:30px;padding:0;
  border:none;background:none;border-radius:50%;cursor:pointer;
  position:relative;display:flex;align-items:center;justify-content:center;
}
.pausebtn:hover{background:color-mix(in srgb,var(--rule) 10%,transparent);}
.pausebtn svg{position:absolute;width:18px;height:18px;transition:opacity 0.15s ease;}
.pausebtn .pi-pause{fill:var(--mul);opacity:1;}
.pausebtn .pi-play{fill:var(--ok);opacity:0;}
.pausebtn.paused .pi-pause{opacity:0;}
.pausebtn.paused .pi-play{opacity:1;}
@media (prefers-reduced-motion:reduce){
  .pausebtn svg{transition:none;}
}

/* Hidden unless a signed-in player's connection is actually worth
   mentioning (see ui.js's connectionMattersHere) — a guest's local play
   never depends on reaching the server, so this stays out of their way. */
.net-status{
  display:flex;align-items:center;justify-content:center;
  width:24px;height:24px;flex-shrink:0;
}
/* `display:flex` on a class selector otherwise beats the UA stylesheet's
   [hidden] rule the same way .bigbtn[hidden] does further down. */
.net-status[hidden]{display:none;}
.net-status svg{
  width:19px;height:19px;fill:none;stroke:currentColor;
  stroke-width:1.6;stroke-linecap:round;
}
.net-status.lost{color:var(--bad);}
.net-status.restored{color:var(--ok);animation:net-pulse 700ms ease-in-out 2;}
@keyframes net-pulse{
  0%,100%{opacity:1;}
  50%{opacity:0.4;}
}
@media (prefers-reduced-motion:reduce){
  .net-status.restored{animation:none;}
}

@media (max-width:520px){
  .iconbtn .lbl{display:none;}
  .topbar{gap:6px;}
  /* Options and New puzzle are both fixed to the viewport now, one in each
     bottom corner, so together they claim a constant bottom slice of
     screen regardless of scroll position — on a narrow phone .functs/.acts
     can render close enough to either corner to sit under one of them.
     Reserving each button's footprint as matching left/right padding
     keeps their content out of the way no matter how tall the viewport is
     or how a row wraps, rather than chasing exact pixel offsets that only
     happen to work on one device. None of .functs/.acts's own buttons
     shrink to make room (unlike .pad, see below) — the widest thing here
     is "Check my work" at ~122px, comfortably narrower than even a 320px
     phone's content width once both sides are reserved, so the padding
     can be symmetric and both rows stay exactly centered under the board
     instead of drifting toward one FAB. Checked against a 320-412 wide by
     560-900 tall sweep at every grid size (N=4/6/9): neither row's
     buttons ever render under either FAB, at any height. */
  .functs,.acts{padding-left:64px;padding-right:64px;}
}

/* .pad does NOT get the same treatment above, on purpose. Its digit
   buttons compute their width as a percentage of the row's own content
   box (see .pad button) precisely so N always fits one line — but that
   means the same 64px reservation that costs .functs/.acts nothing would
   have come straight out of the digits' own size, and at N=9 there simply
   isn't 128px of width to spare: it would crush every one of 320-412px
   phones' buttons down to 14-21px, for every player, to guard against a
   collision that a live sweep showed only actually happens in a narrow
   band — 360-412px wide *and* 620px tall or shorter. That combination is
   not a real portrait phone (even the shortest mainstream one, iPhone SE,
   is 667px); it is what heavy browser/OS chrome eating the viewport looks
   like. So the reservation is scoped to just that band, trading a smaller
   pad on an already-unusual viewport for full-size, unpadded digits
   everywhere else — rather than shrinking every player's number pad to
   guard a case most players' devices cannot even reach. */
@media (max-width:520px) and (max-height:650px){
  .pad{padding-left:64px;padding-right:64px;}
}

/* ------------------------------------------------------ progress strip */
.strip{
  display:flex;flex-wrap:nowrap;
  border-top:1px solid var(--line);
  border-bottom:1px solid var(--line);
  margin-bottom:16px;
}
.stat{
  flex:1 1 0;min-width:0;
  padding:9px 6px;text-align:center;
  border-right:1px solid var(--line);
}
.stat:last-child{border-right:none;}
.stat .lab{
  display:block;font-family:'IBM Plex Mono',ui-monospace,monospace;
  font-size:0.58rem;letter-spacing:0.14em;text-transform:uppercase;
  color:var(--ink-soft);margin-bottom:3px;
}
.stat .val{
  display:block;font-family:'IBM Plex Mono',ui-monospace,monospace;
  font-size:clamp(0.82rem,2.6vw,1rem);font-weight:700;
  font-variant-numeric:tabular-nums;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
.stat .val.small{font-size:clamp(0.7rem,2.2vw,0.82rem);}
.stat .val.good{color:var(--add-text);}
/* Reusable ok/bad text colour, e.g. inline within translated copy — a class
   rather than an inline style so it survives style-src CSP. */
.clr-ok{color:var(--add-text);}
.clr-bad{color:var(--bad);}
/* the one stat that only ever appears on a real win deserves to look like
   an earned thing, not just another readout — same bronze as Rankings */
.stat.score{background:color-mix(in srgb,var(--rank) 16%,transparent);}
.stat.score .lab{color:var(--rank-deep);}
.stat.score .val{color:var(--rank-deep);}

/* -------------------------------------------------------------- board */
.stage{display:flex;flex-direction:column;align-items:center;gap:13px;position:relative;}

/* A manual pause covers the play area only — not a .veil, deliberately: the
   player just chose this, so it needs no "are you sure" ceremony the way
   the unattended idle-timeout pause does. Tapping the now-green triangle in
   the top bar is the only way out, so the message points back to it. */
.pause-cover{
  position:absolute;inset:0;z-index:15;border-radius:8px;
  display:flex;align-items:center;justify-content:center;text-align:center;
  background:color-mix(in srgb,var(--paper) 88%,transparent);
  backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);
}
.pause-cover[hidden]{display:none;}
.pause-cover-inner{max-width:280px;padding:20px;}
.pause-cover-inner h2{font-family:'Fraunces',Georgia,serif;font-size:1.3rem;margin:0 0 8px;font-weight:600;}
.pause-cover-inner p{margin:0;font-size:0.86rem;line-height:1.5;color:var(--ink-soft);}

.frame{
  display:grid;
  grid-template-columns:1fr 14px;
  grid-template-rows:1fr 14px;
  gap:5px;
  width:min(100%,var(--board-max));
}
.board{
  position:relative;grid-column:1;grid-row:1;
  display:grid;
  grid-template-columns:repeat(var(--n),1fr);
  grid-template-rows:repeat(var(--n),1fr);
  aspect-ratio:1/1;
  background:var(--card);
  border:2.5px solid var(--rule-soft);
  border-radius:var(--board-radius);
  overflow:hidden;
  box-shadow:6px 6px 0 color-mix(in srgb,var(--rule) 7%,transparent);
}
.rail{display:grid;}
.rail-rows{grid-column:2;grid-row:1;grid-template-rows:repeat(var(--n),1fr);}
.rail-cols{grid-column:1;grid-row:2;grid-template-columns:repeat(var(--n),1fr);}
.pip{display:flex;align-items:center;justify-content:center;}
.pip i{
  display:block;width:6px;height:6px;border-radius:50%;
  background:var(--line);
  transition:background 200ms ease,transform 200ms ease;
}
.pip.done i{background:var(--ok);transform:scale(1.5);}

.cell{
  position:relative;display:flex;align-items:center;justify-content:center;
  cursor:pointer;user-select:none;-webkit-user-select:none;
  border:1px solid color-mix(in srgb,var(--rule) 13%,transparent);
  background-color:transparent;
  transition:background-color 140ms ease;
  min-width:0;min-height:0;
}
/* The Sudoku box grid: solid, deliberately calmer than the raw --rule tone
   (see --rule-soft). Cage outlines never touch these lines — they live in
   the .ink layer, inset inside the cells. */
.cell.box-l{border-left:2.5px solid var(--rule-soft);}
.cell.box-t{border-top:2.5px solid var(--rule-soft);}
.cell.box-r{border-right:2.5px solid var(--rule-soft);}
.cell.box-b{border-bottom:2.5px solid var(--rule-soft);}

.cell.cage-ok{background-color:color-mix(in srgb,var(--ok) 30%,transparent);}
.cell.cage-bad{background-color:color-mix(in srgb,var(--bad) 30%,transparent);}
.cell.peer{background-image:linear-gradient(color-mix(in srgb,var(--rule) 7.5%,transparent),color-mix(in srgb,var(--rule) 7.5%,transparent));}
.cell.incage{background-image:linear-gradient(color-mix(in srgb,var(--mul) 20%,transparent),color-mix(in srgb,var(--mul) 20%,transparent));}
.cell.peer.incage{background-image:linear-gradient(color-mix(in srgb,var(--mul) 20%,transparent),color-mix(in srgb,var(--mul) 20%,transparent));}
.cell.sel{box-shadow:inset 0 0 0 3px var(--mul);}

.ink{
  position:absolute;inset:0;width:100%;height:100%;
  pointer-events:none;z-index:1;overflow:visible;
}
.ink path{fill:none;stroke:var(--cageink);transition:stroke 140ms ease;}
.ink path.here{stroke:var(--mul);stroke-dasharray:none !important;}

.digit{
  font-family:'Fraunces',Georgia,serif;font-weight:500;
  font-size:calc(min(var(--board-max),90vw) / var(--n) * 0.5);
  line-height:1;color:var(--ink);position:relative;z-index:2;
}
.cell.given .digit{font-weight:600;color:var(--ink);}
.cell.given{cursor:default;}
/* the lock marker lives on the cell, not the digit, so it survives even
   when the given cell is empty of visible ink (it never is, but keeps the
   marker's positioning independent of .digit's own box) */
.cell.given::after{
  content:"";position:absolute;z-index:4;pointer-events:none;
  top:calc(min(var(--board-max),90vw) / var(--n) * 0.08);
  right:calc(min(var(--board-max),90vw) / var(--n) * 0.08);
  width:calc(min(var(--board-max),90vw) / var(--n) * 0.13);
  height:calc(min(var(--board-max),90vw) / var(--n) * 0.13);
  border-radius:50%;border:1.5px solid var(--ink-soft);
  background:radial-gradient(circle,var(--ink) 32%,transparent 34%);
}
/* player-filled cells: regular weight, arctic ink, faint wash — independent
   of the given/wrong/dup layers below, which still win on .digit's color
   since they're declared after this and share the same specificity */
.cell:not(.given) .digit{font-weight:400;color:var(--arctic);}
.cell:not(.given):has(.digit:not(:empty))::after{
  content:"";position:absolute;inset:0;z-index:0;pointer-events:none;
  background:var(--fill-wash);
}
.cell.dup .digit{color:var(--bad);font-weight:700;}
.cell.wrong .digit{color:var(--bad);}
.cell.wrong::before{
  content:"";position:absolute;inset:4px;
  border:2px dashed var(--bad);border-radius:2px;z-index:1;
}
.cell.twin .digit{
  text-decoration:underline;text-decoration-color:var(--ok);
  text-decoration-thickness:2px;text-underline-offset:3px;
}

.cage-tag{
  position:absolute;top:1px;left:2px;z-index:4;
  font-family:'IBM Plex Mono',ui-monospace,monospace;font-weight:700;
  font-size:calc(min(var(--board-max),90vw) / var(--n) * 0.235);
  line-height:1;letter-spacing:-0.02em;pointer-events:none;
  padding:1px 3px;border-radius:2px;background:color-mix(in srgb,var(--card) 82%,transparent);
}
.op-add{color:var(--add-text);}
.op-sub{color:var(--sub);}
.op-mul{color:var(--mul-text);}
.op-div{color:var(--div);}
/* --note, not --ink-soft: a singleton cage is a freebie annotation, not an
   unresolved challenge, and it used to share --ink-soft with op-hidden —
   the same neutral grey on "here is the answer" and "this is unknown" cage
   tags read as related at a glance, which is what actually made the
   Adept_single-cell_bug screenshot confusing (the cages themselves were
   both correct). --note already clears contrast at an even smaller size
   (the pad's remaining-count label), so it needed no new token. */
.op-eq{color:var(--note);}
.op-hidden{color:var(--ink-soft);}

.notes{
  position:absolute;inset:0;z-index:2;display:grid;
  grid-template-columns:repeat(var(--notecols),1fr);
  padding:calc(min(var(--board-max),90vw) / var(--n) * 0.26) 3px 3px;
}
.notes b{
  font-family:'IBM Plex Mono',ui-monospace,monospace;font-weight:600;
  font-size:calc(min(var(--board-max),90vw) / var(--n) * 0.2);
  color:var(--note);
  display:flex;align-items:center;justify-content:center;line-height:1;
}
.notes b.dead{opacity:0.26;text-decoration:line-through;}

/* solve animation: a quiet diagonal settle across the finished grid */
.board.solved .cell{
  animation:cellsettle 620ms cubic-bezier(.22,.61,.36,1) both;
  animation-delay:var(--d,0ms);
}
@keyframes cellsettle{
  0%{transform:scale(1);}
  30%{transform:scale(0.93);}
  65%{transform:scale(1.035);}
  100%{transform:scale(1);}
}

/* ------------------------------------------------------------- pads */
/* nowrap, not wrap: digits 1..N are one entity and belong on one line no
   matter the screen or the grid size (N=4/6/9) — see each button's width
   formula below for how that is actually guaranteed, not just requested.
   Clear Cell used to live here too and was the reason N=9 could strand a
   lone "9" on its own row on a narrow phone; it now lives in .functs. */
.pad{display:flex;gap:7px;flex-wrap:nowrap;justify-content:center;width:min(100%,var(--board-max));}
.pad button{
  font-family:'Fraunces',Georgia,serif;font-weight:600;
  /* N buttons plus (N-1) 7px gaps, minus a 2px rounding buffer, can never
     exceed the row's own width — so this never wraps, by construction,
     regardless of viewport or N. Capped at 44px so a small grid (N=4)
     does not get oversized buttons just because there is room to spare. */
  width:min(44px,calc((100% - 7px * (var(--n) - 1) - 2px) / var(--n)));
  height:min(44px,calc((100% - 7px * (var(--n) - 1) - 2px) / var(--n)));
  /* Can't reuse the width formula above for font-size — "%" means the
     inherited font size there, not the container's width. Falls back to
     the board's own cell-size approximation instead (see .digit); a
     slightly-off font size is cosmetic, unlike the button's own width,
     which has to be exact or the wrap guarantee above breaks. */
  font-size:min(1.625rem,calc(min(var(--board-max),90vw) / var(--n) * 0.5));
  border:1px solid color-mix(in srgb,var(--rule) 13%,transparent);
  background:var(--card);box-shadow:var(--shadow-sm);
  border-radius:8px;cursor:pointer;position:relative;
  transition:transform 70ms ease,background 120ms ease;
}
.pad button:hover{background:color-mix(in srgb,var(--mul) 20%,transparent);}
.pad button:active{transform:scale(0.93);}
/* --note, not --ink-soft: same "this is a helper annotation, not the digit
   itself" language as pencil-mark notes inside the cells. */
.pad button .left{
  position:absolute;bottom:2px;right:5px;
  font-family:'IBM Plex Mono',ui-monospace,monospace;
  font-size:0.625rem;font-weight:600;color:var(--note);
}
.pad button.spent{opacity:0.4;}

/* Functional actions on the grid — Clear Cell, Undo, Redo, Hint — as their
   own entity: a row of icon-only square buttons, distinct from the digit
   pad above and Notes/Check below (extra margin-top on both keeps that
   separation visible without drawing an actual rule between them). Same
   card+shadow language as the number pad, since these read as siblings of
   it, just a different job. */
.functs{display:flex;gap:10px;flex-wrap:wrap;justify-content:center;width:min(100%,var(--board-max));margin-top:12px;}
.functs button{
  position:relative;width:44px;height:44px;
  display:flex;align-items:center;justify-content:center;
  border:1px solid color-mix(in srgb,var(--rule) 13%,transparent);
  background:var(--card);box-shadow:var(--shadow-sm);
  border-radius:8px;cursor:pointer;color:var(--ink);
  transition:transform 70ms ease,background 120ms ease;
}
.functs button:hover:not(:disabled){background:color-mix(in srgb,var(--mul) 20%,transparent);}
.functs button:active:not(:disabled){transform:scale(0.93);}
.functs button svg{width:21px;height:21px;stroke:currentColor;fill:none;}
/* The one thing on this row that can't be a static icon. Same corner-badge
   idea as the pad's own remaining-count digits, just circular and always
   shown rather than hidden at zero — "no hints left" is meaningful here,
   not just an empty state. */
.functs button .badge{
  position:absolute;top:-5px;right:-5px;
  min-width:16px;height:16px;padding:0 3px;border-radius:999px;
  background:var(--mul);color:var(--card);
  font-family:'IBM Plex Mono',ui-monospace,monospace;font-size:0.62rem;font-weight:700;
  display:flex;align-items:center;justify-content:center;line-height:1;
}

/* fixed tray — only shown while a hidden cage is selected. Same card+shadow
   chrome as the digit pad (.pad button) on purpose, so choosing an operator
   reads as the same kind of action as entering a digit, not a separate
   control language. The .op-add/.op-sub/.op-mul/.op-div class each button
   carries (set in render.js's mountOpPad, from config.js's OP_CLASS) is
   what actually colors it — the same classes and colors a cage's own tag
   already uses for that operator, defined above with the rest of the
   cage-tag rules. */
.op-pad{display:flex;gap:8px;flex-wrap:wrap;justify-content:center;width:min(100%,var(--board-max));min-height:44px;}
.op-pad[hidden]{display:none;}
.op-pad button{
  font-family:'IBM Plex Mono',ui-monospace,monospace;font-weight:700;font-size:1.05rem;
  width:44px;height:44px;border-radius:8px;
  border:1px solid color-mix(in srgb,var(--rule) 13%,transparent);
  background:var(--card);box-shadow:var(--shadow-sm);cursor:pointer;
  transition:transform 70ms ease,background 120ms ease;
}
.op-pad button:hover{background:color-mix(in srgb,var(--mul) 20%,transparent);}
.op-pad button:active{transform:scale(0.93);}
.op-pad button.committed{background:var(--ink);color:var(--card);border-color:var(--ink);}
.op-pad button.eliminated{opacity:0.4;text-decoration:line-through;}
.op-pad button:disabled{opacity:0.32;cursor:default;}

.acts{display:flex;gap:8px;width:min(100%,var(--board-max));justify-content:center;flex-wrap:wrap;margin-top:12px;}
.acts button{
  font-weight:600;font-size:0.79rem;
  padding:9px 14px;border:1.5px solid var(--rule);
  background:transparent;border-radius:8px;cursor:pointer;
}
.acts button:hover:not(:disabled){background:color-mix(in srgb,var(--rule) 6%,transparent);}

/* Check my work — a neutral accent, not --add/--ok, so it never implies a
   correct/incorrect verdict before the player has actually checked. */
.acts button.accent{
  border:1.5px solid var(--arctic);color:var(--arctic);
  background:color-mix(in srgb,var(--arctic) 10%,transparent);
}
.acts button.accent:hover:not(:disabled){background:color-mix(in srgb,var(--arctic) 18%,transparent);}

.acts button.lit{background:color-mix(in srgb,var(--div) 20%,transparent);border-color:var(--div);color:var(--div);}
/* Inert, not just faint — background swap reads as "off" even to someone
   who can't tell a 55%-opacity button from a 100%-opacity one at a glance.
   .functs button sets its icon to stroke:currentColor specifically so this
   color swap dims the glyph too, without a separate icon-specific rule. */
.acts button:disabled,.pad button:disabled,.functs button:disabled{
  background:var(--disabled-bg);color:var(--ink-soft);
  opacity:0.55;box-shadow:none;border:none;cursor:default;
}

.flash{
  min-height:20px;margin:0;padding:0 8px;
  font-family:'IBM Plex Mono',ui-monospace,monospace;font-size:0.75rem;
  color:var(--ink-soft);text-align:center;
}
.flash.warn{color:var(--bad);}

.banner{
  display:none;margin:0;
  background:var(--ok);color:var(--card);
  padding:13px 18px;border:2px solid var(--rule);
  font-family:'Fraunces',Georgia,serif;font-size:1rem;font-weight:600;
  text-align:center;width:min(100%,var(--board-max));
}
.banner.show{display:block;animation:bannerdrop 460ms cubic-bezier(.22,.61,.36,1) both;animation-delay:120ms;}
@keyframes bannerdrop{
  from{transform:translateY(-8px);opacity:0;}
  to{transform:none;opacity:1;}
}

/* ---------------------------------------------------------- drawers */
.scrim{
  position:fixed;inset:0;background:rgba(34,32,27,0.5);
  opacity:0;pointer-events:none;transition:opacity 240ms ease;z-index:40;
}
.scrim.show{opacity:1;pointer-events:auto;}
.drawer{
  position:fixed;top:0;bottom:0;
  width:min(340px,86vw);
  background:var(--card);z-index:45;
  display:flex;flex-direction:column;
  transition:transform 280ms cubic-bezier(.22,.61,.36,1);
  overflow-y:auto;-webkit-overflow-scrolling:touch;
}
.drawer.left{left:0;border-right:3px solid var(--rule);transform:translateX(-102%);}
.drawer.right{right:0;border-left:3px solid var(--rule);transform:translateX(102%);}
.drawer.open{transform:translateX(0);}
.dhead{
  display:flex;justify-content:space-between;align-items:center;gap:10px;
  padding:16px 18px 12px;border-bottom:2px solid var(--rule);
  position:sticky;top:0;background:var(--card);z-index:2;
}
.dhead h2{font-family:'Fraunces',Georgia,serif;font-size:1.25rem;margin:0;font-weight:600;}
.dclose{
  border:1.5px solid var(--rule);background:transparent;cursor:pointer;
  font-family:'IBM Plex Mono',ui-monospace,monospace;font-size:0.85rem;
  line-height:1;padding:6px 9px;border-radius:3px;
}
.dclose:hover{background:color-mix(in srgb,var(--rule) 8%,transparent);}
.dbody{padding:14px 18px 26px;}

/* --------------------------------------- floating corner buttons & sheet */
/* Options and New puzzle are deliberate twins — one .fab base, a .left/
   .right modifier for which corner, so "symmetric" is one shared selector
   instead of two numbers a future edit could quietly drift apart. Docked
   like chat-widget corner buttons, not touching the edge — the app is used
   installed to a home screen (see manifest's standalone display), so a
   pair of floating controls anchored to the viewport corners is native
   here rather than a departure from anything else on the page. Kept at
   44px, the smallest Apple's HIG still calls a comfortable tap target —
   smaller than that just to shrink the .functs/.acts reservation further
   wasn't worth the tradeoff (see the padding-left/right comment there;
   the reservation is symmetric so it already covers both corners). */
.fab{
  position:fixed;z-index:41;
  bottom:max(16px,calc(10px + env(safe-area-inset-bottom)));
  width:44px;height:44px;border-radius:50%;
  display:flex;align-items:center;justify-content:center;
  background:var(--card);border:1.5px solid var(--rule);
  box-shadow:var(--shadow-md);cursor:pointer;
}
.fab.left{left:max(12px,calc(8px + env(safe-area-inset-left)));}
.fab.right{right:max(12px,calc(8px + env(safe-area-inset-right)));}
.fab:hover{background:color-mix(in srgb,var(--mul) 18%,transparent);}
.fab svg{
  width:19px;height:19px;fill:var(--ink);stroke:var(--ink);
  stroke-width:1.7;stroke-linecap:round;
}
.fab[aria-expanded="true"]{background:var(--rule);}
.fab[aria-expanded="true"] svg{fill:var(--card);stroke:var(--card);}

/* A small anchored card, not the full-height .drawer treatment — three
   session actions do not need that much chrome. */
.sheet{
  position:fixed;z-index:45;
  left:max(20px,calc(12px + env(safe-area-inset-left)));
  bottom:max(78px,calc(70px + env(safe-area-inset-bottom)));
  width:min(240px,72vw);
  background:var(--card);border:1.5px solid var(--rule);border-radius:8px;
  box-shadow:var(--shadow-md);
  display:flex;flex-direction:column;
  transform:translateY(10px) scale(0.97);opacity:0;pointer-events:none;
  transition:transform 180ms cubic-bezier(.22,.61,.36,1),opacity 180ms ease;
}
.sheet.open{transform:none;opacity:1;pointer-events:auto;}
.sheet .dhead{padding:10px 12px 8px;position:static;}
.sheet .dhead h2{font-size:0.95rem;}
.sheet-body{padding:8px;display:flex;flex-direction:column;gap:8px;}
.sheet-body button{
  width:100%;text-align:left;font-weight:600;font-size:0.82rem;
  padding:10px 12px;border-radius:6px;
  border:1.5px solid var(--rule);background:var(--paper);box-shadow:var(--shadow-sm);
  cursor:pointer;
}
.sheet-body button:hover:not(:disabled){background:color-mix(in srgb,var(--rule) 8%,var(--paper));}
.sheet-body button:disabled{
  color:var(--ink-soft);background:var(--disabled-bg);border-color:transparent;
  box-shadow:none;opacity:0.55;cursor:default;
}
/* Dim/Highlight/Show-remaining — same dot-plus-label on/off language they
   had when they lived on the main screen, just full width like every other
   row in this sheet instead of a centered pill. */
.sheet-body button.toggle{
  display:flex;align-items:center;gap:8px;
  font-family:'IBM Plex Mono',ui-monospace,monospace;font-size:0.76rem;
  border-color:var(--line);background:transparent;color:var(--ink-soft);
  box-shadow:none;
}
.sheet-body button.toggle[hidden]{display:none;}
.sheet-body button.toggle:hover{border-color:var(--rule);color:var(--ink);background:color-mix(in srgb,var(--rule) 8%,transparent);}
.sheet-body button.toggle .dot{
  width:9px;height:9px;border-radius:50%;box-sizing:border-box;
  background:none;border:1.5px solid var(--ink-soft);flex-shrink:0;
}
.sheet-body button.toggle.on{border-color:var(--add);color:var(--ink);background:color-mix(in srgb,var(--add) 12%,transparent);}
.sheet-body button.toggle.on .dot{background:var(--add);border-color:var(--add);}
@media (prefers-reduced-motion:reduce){
  .sheet{transition:none;}
}

.bigbtn{
  display:flex;align-items:center;gap:12px;
  width:100%;text-align:left;cursor:pointer;
  border:2px solid var(--arctic-deep);background:var(--arctic);color:var(--on-accent);
  border-radius:3px;padding:12px 14px;margin-bottom:20px;
  transition:background 140ms ease;
}
/* `display:flex` on a class selector otherwise beats the UA stylesheet's
   [hidden] rule, since author styles always win over UA styles regardless
   of specificity — so a hidden .bigbtn would render anyway without this. */
.bigbtn[hidden]{display:none;}
.bigbtn:hover{background:var(--arctic-deep);}
.bigbtn.rank{border-color:var(--rank-deep);background:var(--rank);}
.bigbtn.rank:hover{background:var(--rank-deep);}
.bigbtn.account{border-color:var(--account-deep);background:var(--account);}
.bigbtn.account:hover{background:var(--account-deep);}
.bigbtn-i{width:26px;height:26px;flex-shrink:0;opacity:0.95;}
.bigbtn-txt{min-width:0;}
.bigbtn-t{display:block;font-family:'Fraunces',Georgia,serif;font-size:1.1rem;font-weight:600;}
.bigbtn-s{display:block;font-size:0.73rem;opacity:0.85;margin-top:2px;line-height:1.35;}

.lvl{
  width:100%;text-align:left;cursor:pointer;
  border:1.5px solid var(--line);background:transparent;
  border-radius:3px;padding:11px 12px;margin-bottom:9px;
}
.lvl:hover{border-color:var(--rule);background:color-mix(in srgb,var(--mul) 10%,transparent);}
.lvl.on{border-color:var(--rule);background:var(--rule);}
.lvl.on .lname,.lvl.on .ldesc,.lvl.on .lmeta{color:var(--card);}
.lvl.on .lmeta{opacity:0.75;}
.lname{display:flex;justify-content:space-between;align-items:baseline;gap:8px;font-weight:700;font-size:0.92rem;}
.lname span{font-family:'IBM Plex Mono',ui-monospace,monospace;font-size:0.7rem;font-weight:600;color:var(--ink-soft);}
.lvl.on .lname span{color:color-mix(in srgb,var(--card) 80%,transparent);}
.ldesc{font-size:0.78rem;line-height:1.45;color:var(--ink-soft);margin:5px 0 0;}
.lmeta{font-family:'IBM Plex Mono',ui-monospace,monospace;font-size:0.66rem;color:var(--ink-soft);margin-top:6px;letter-spacing:0.03em;}

/* "try a harder level" points here — a slow gold pulse, not an alert */
@keyframes lvl-glow{
  0%,100%{box-shadow:0 0 0 2px var(--mul),0 0 12px 1px color-mix(in srgb,var(--mul) 65%,transparent);}
  50%{box-shadow:0 0 0 2px var(--mul),0 0 3px 0 color-mix(in srgb,var(--mul) 25%,transparent);}
}
.lvl.suggest{border-color:var(--mul);animation:lvl-glow 1.6s ease-in-out infinite;}
@media (prefers-reduced-motion:reduce){
  .lvl.suggest{animation:none;box-shadow:0 0 0 2px var(--mul);}
}

.confirm{
  border:1.5px solid var(--danger);background:color-mix(in srgb,var(--danger) 9%,transparent);
  border-radius:3px;padding:11px 12px;margin:-3px 0 11px;
}
.confirm p{margin:0 0 9px;font-size:0.78rem;line-height:1.45;}
.confirm .row{display:flex;gap:7px;}
.confirm button{
  flex:1;font-weight:600;font-size:0.76rem;
  padding:8px 9px;border-radius:3px;cursor:pointer;border:1.5px solid var(--rule);
}
.confirm .yes{background:var(--danger);border-color:var(--danger);color:var(--on-accent);}
.confirm .yes:hover{filter:brightness(0.85);}
.confirm .no{background:transparent;}

.sect{margin-bottom:22px;}
.sect:last-child{margin-bottom:0;}
.sect h3{
  font-family:'IBM Plex Mono',ui-monospace,monospace;font-size:0.66rem;
  letter-spacing:0.16em;text-transform:uppercase;color:var(--ink-soft);
  margin:0 0 9px;padding-bottom:6px;border-bottom:1px solid var(--line);
}
.sect p{margin:0 0 9px;font-size:0.83rem;line-height:1.55;}
.sect p:last-child{margin-bottom:0;}
.lines{display:flex;flex-direction:column;gap:11px;}
.lrow{display:flex;gap:10px;align-items:flex-start;font-size:0.79rem;line-height:1.45;}
.lsvg{width:42px;height:27px;flex-shrink:0;margin-top:1px;}
.legend{display:flex;flex-direction:column;gap:7px;}
.legend .row{display:flex;align-items:center;gap:9px;font-family:'IBM Plex Mono',ui-monospace,monospace;font-size:0.76rem;}
.legend .chip{width:22px;text-align:center;font-weight:700;flex-shrink:0;}

/* ----------------------------------------------------------- modals */
.veil{
  position:fixed;inset:0;background:rgba(34,32,27,0.62);
  display:none;align-items:center;justify-content:center;
  padding:20px;z-index:50;
}
.veil.show{display:flex;}
.modal{
  background:var(--card);border:3px solid var(--rule);
  max-width:540px;width:100%;padding:22px 24px 18px;
  box-shadow:10px 10px 0 color-mix(in srgb,var(--ink) 25%,transparent);
  max-height:88vh;max-height:88dvh;overflow-y:auto;
}
.modal h2{font-family:'Fraunces',Georgia,serif;font-size:1.45rem;margin:0 0 3px;font-weight:600;}
.modal .sub{
  font-family:'IBM Plex Mono',ui-monospace,monospace;font-size:0.68rem;
  letter-spacing:0.13em;text-transform:uppercase;color:var(--ink-soft);
  margin:0 0 15px;
}
.modal .go{
  margin-top:15px;width:100%;padding:11px;
  font-weight:700;font-size:0.88rem;
  background:var(--rule);color:var(--card);
  border:none;border-radius:3px;cursor:pointer;
}
.modal .go:hover{background:var(--ink);}

/* ------------------------------------------------- confirmation ask */
.modal.ask{max-width:430px;}

/* ------------------------------------------------------------- win */
.modal.win{max-width:430px;text-align:center;}
.modal.win .sub{margin-bottom:16px;}

/* --------------------------------------------------------- tutorial */
/* .modal.tutorial has no fixed position of its own — centred by the .veil's
   flex rules like any other modal (illustration/plain steps), until
   tutorial-ui.js gives it an inline position:fixed/top/left for a spotlight
   step, which lifts it out of that centring so it can float beside the
   real button it's pointing at. */
/* z-index above #tut-spot's 51: they're siblings inside the same fixed
   .veil, and #tut-spot's whole job is an oversized shadow that dims the
   entire viewport outside its target hole — without this the card itself,
   sitting anywhere other than inside that hole, would get dimmed too. */
.modal.tutorial{max-width:460px;position:relative;z-index:52;}

.tut-skip{
  position:absolute;top:14px;right:16px;background:none;border:none;cursor:pointer;padding:4px;
  font-family:'IBM Plex Mono',ui-monospace,monospace;font-size:0.7rem;
  color:var(--ink-soft);text-decoration:underline;text-underline-offset:2px;
}
.tut-skip:hover{color:var(--ink);}

.tut-stage{min-height:150px;padding-right:44px;}
.tut-stage h3{font-family:'Fraunces',Georgia,serif;font-size:1.2rem;margin:0 0 8px;font-weight:600;}
.tut-stage p{margin:0;font-size:0.87rem;line-height:1.6;color:var(--ink);}

.tut-progress{
  font-family:'IBM Plex Mono',ui-monospace,monospace;font-size:0.64rem;
  color:var(--ink-soft);letter-spacing:0.06em;margin:12px 0 0;
}

.tut-nav{display:flex;align-items:center;gap:10px;margin-top:10px;}
.tut-dots{display:flex;gap:5px;margin-right:auto;flex-wrap:wrap;}
.tut-dots i{
  display:block;width:6px;height:6px;border-radius:50%;background:var(--line);
  transition:background 200ms ease,transform 200ms ease;
}
.tut-dots i.done{background:var(--rule);}
.tut-dots i.on{background:var(--mul);transform:scale(1.5);}

.tut-prev,.tut-next{
  font-weight:700;font-size:0.82rem;padding:9px 14px;border-radius:3px;cursor:pointer;
  border:1.5px solid var(--rule);background:transparent;color:var(--ink);white-space:nowrap;
}
.tut-prev:disabled{opacity:0.35;cursor:default;}
.tut-prev:hover:not(:disabled){background:color-mix(in srgb,var(--rule) 8%,transparent);}
.tut-next{background:var(--rule);color:var(--card);border-color:var(--rule);}
.tut-next:hover{background:var(--ink);}

/* Illustration steps get a small self-contained mock grid rather than the
   real board — it stays correct no matter which tier or mid-game state the
   player actually has open, and it can show exactly the example the copy
   is describing (row/column/box, a 7+ cage, a hidden op) every time. */
.tut-grid{
  display:grid;border:2.5px solid var(--rule);width:max-content;background:var(--paper);
  margin:4px auto 14px;
}
.tut-grid .c{
  position:relative;border:1px solid color-mix(in srgb,var(--rule) 18%,transparent);
  display:flex;align-items:center;justify-content:center;
  font-family:'Fraunces',Georgia,serif;font-weight:600;font-size:1.15rem;color:var(--ink);
  width:42px;height:42px;
}
.tut-grid .c small{
  position:absolute;top:2px;left:3px;
  font-family:'IBM Plex Mono',ui-monospace,monospace;font-size:0.55rem;font-weight:700;
}
.tut-grid .c.cage-ok{background-color:color-mix(in srgb,var(--ok) 30%,transparent);}
.tut-grid .c.cage-bad{background-color:color-mix(in srgb,var(--bad) 30%,transparent);}
/* .sweep tints a row/column cell; .boxed rings a box cell — kept as two
   effects on purpose so a cell that is both (the shared corner) reads as
   visibly two things at once, same as it does on the real board. One-shot
   reveals with a stagger via --d, not loops: the story is "watch it build
   up", not an ongoing pulse (that's reserved for the spotlight ring). */
.tut-grid .c.sweep{background-color:color-mix(in srgb,var(--mul) 22%,transparent);}
.tut-grid .c.boxed{box-shadow:inset 0 0 0 3px var(--rule);}
.tut-grid .c .fill{display:inline-block;opacity:0;transform:scale(0.4);color:var(--arctic);font-weight:400;}

@media (prefers-reduced-motion:no-preference){
  .tut-grid .c .fill{animation:tut-pop 240ms ease forwards;animation-delay:var(--d,0ms);}
  .tut-grid .c.cage-ok{animation:tut-flip-ok 240ms ease forwards;animation-delay:var(--d,0ms);}
  .tut-grid .c.cage-bad{animation:tut-flip-bad 240ms ease forwards;animation-delay:var(--d,0ms);}
  .tut-grid .c.sweep{animation:tut-flip-mul 220ms ease forwards;animation-delay:var(--d,0ms);}
  .tut-grid .c.boxed{
    box-shadow:inset 0 0 0 0 var(--rule);
    animation:tut-box-pop 220ms ease forwards;animation-delay:var(--d,0ms);
  }
}
@media (prefers-reduced-motion:reduce){
  .tut-grid .c .fill{opacity:1;transform:none;}
}
@keyframes tut-pop{from{opacity:0;transform:scale(0.4);}to{opacity:1;transform:scale(1);}}
@keyframes tut-flip-ok{from{background-color:transparent;}to{background-color:color-mix(in srgb,var(--ok) 30%,transparent);}}
@keyframes tut-flip-bad{from{background-color:transparent;}to{background-color:color-mix(in srgb,var(--bad) 30%,transparent);}}
@keyframes tut-flip-mul{from{background-color:transparent;}to{background-color:color-mix(in srgb,var(--mul) 22%,transparent);}}
@keyframes tut-box-pop{from{box-shadow:inset 0 0 0 0 var(--rule);}to{box-shadow:inset 0 0 0 3px var(--rule);}}

/* Hidden-operator and notes illustrations reuse the real op-pad/cell chrome
   at a smaller size, so "the round buttons" in the copy are recognisably
   the same buttons the player will actually meet mid-grid. */
.tut-mini-op{display:flex;gap:6px;justify-content:center;margin:6px 0 4px;}
.tut-mini-op button{
  font-family:'IBM Plex Mono',ui-monospace,monospace;font-weight:700;font-size:0.9rem;
  width:34px;height:34px;border-radius:7px;cursor:default;
  border:1px solid color-mix(in srgb,var(--rule) 13%,transparent);background:var(--card);
}
.tut-mini-op button.committed{background:var(--ink);color:var(--card);border-color:var(--ink);}

/* Two small grids side by side — the "green means right, red needs another
   look" pairing in the colors step. */
.tut-pair{display:flex;gap:16px;justify-content:center;}

.tut-mini-cell{
  width:56px;height:56px;margin:4px auto 14px;border:2.5px solid var(--rule);
  background:var(--paper);position:relative;display:flex;align-items:center;justify-content:center;
}
.tut-mini-cell .digit{
  font-family:'Fraunces',Georgia,serif;font-weight:500;font-size:1.5rem;color:var(--arctic);
  opacity:0;
}
.tut-mini-notes{
  position:absolute;inset:4px;display:grid;grid-template-columns:repeat(3,1fr);
  font-family:'IBM Plex Mono',ui-monospace,monospace;font-size:0.6rem;color:var(--ink-soft);
  opacity:0;transition:opacity 300ms ease;
}
.tut-mini-notes span{display:flex;align-items:center;justify-content:center;}
@media (prefers-reduced-motion:no-preference){
  .tut-mini-notes{animation:tut-pop 240ms ease forwards;animation-delay:200ms;}
  .tut-mini-cell .digit{animation:tut-pop 240ms ease forwards;animation-delay:1100ms;}
}
@media (prefers-reduced-motion:reduce){
  .tut-mini-notes{opacity:1;}
  .tut-mini-cell .digit{opacity:1;}
}

/* Spotlight steps: #tut-spot is a live-tracked proxy over the real target,
   not a hole cut into this veil. Its own oversized box-shadow does all the
   dimming, so the target shows through crisply in the one screen region the
   veil itself goes transparent for (.spotlighting) — while the veil, still
   a full-viewport element above the real chrome, keeps catching every click
   so nothing underneath is actually reachable mid-tour. */
.veil.spotlighting{background:transparent;}
.tut-spot{
  position:fixed;z-index:51;pointer-events:none;border-radius:10px;
  box-shadow:0 0 0 9999px rgba(34,32,27,0.62),0 0 0 3px var(--mul);
}
@media (prefers-reduced-motion:no-preference){
  .tut-spot{transition:top 260ms ease,left 260ms ease,width 260ms ease,height 260ms ease;}
  .tut-spot:not([hidden]){animation:tut-ring 1.6s ease-in-out infinite;}
}
@keyframes tut-ring{
  0%,100%{box-shadow:0 0 0 9999px rgba(34,32,27,0.62),0 0 0 3px var(--mul);}
  50%{box-shadow:0 0 0 9999px rgba(34,32,27,0.62),0 0 0 6px color-mix(in srgb,var(--mul) 55%,transparent);}
}

@media (max-width:480px){
  .modal.tutorial{max-width:100%;}
  .tut-grid .c{width:38px;height:38px;}
}
.winstats{
  display:flex;justify-content:center;gap:10px;margin:0 0 20px;flex-wrap:wrap;
}
.wstat{
  flex:1 1 100px;padding:9px 6px;border:1.5px solid var(--line);border-radius:3px;
}
.wstat .lab{
  display:block;font-family:'IBM Plex Mono',ui-monospace,monospace;
  font-size:0.58rem;letter-spacing:0.14em;text-transform:uppercase;
  color:var(--ink-soft);margin-bottom:3px;
}
.wstat .val{
  display:block;font-family:'IBM Plex Mono',ui-monospace,monospace;
  font-size:1.05rem;font-weight:700;font-variant-numeric:tabular-nums;
}
.wstat.score{background:color-mix(in srgb,var(--rank) 16%,transparent);border-color:var(--rank-deep);}
.wstat.score .lab,.wstat.score .val{color:var(--rank-deep);}
.asktext{font-size:0.88rem;line-height:1.6;margin:0 0 18px;color:var(--ink);}
.askrow{display:flex;gap:10px;flex-wrap:wrap;}
.askbtn{
  flex:1 1 160px;min-height:44px;padding:12px 14px;border-radius:4px;cursor:pointer;
  font-weight:700;font-size:0.85rem;border:2px solid transparent;
}
/* danger/arctic stay saturated in both themes rather than inverting, so
   their text is the fixed --on-accent, not --card (which does invert) */
.askbtn.danger{background:var(--danger);border-color:var(--danger);color:var(--on-accent);}
.askbtn.danger:hover{filter:brightness(0.85);}
/* --safe is the same green as --ok/--add, which does invert with the
   theme, so it correctly pairs with --card, same as .banner does */
.askbtn.safe{background:var(--safe);border-color:var(--safe);color:var(--card);}
.askbtn.safe:hover{filter:brightness(0.85);}

/* ------------------------------------------------------------ pager */
.pager{
  display:flex;align-items:center;justify-content:space-between;gap:10px;
  margin-top:14px;padding-top:12px;border-top:1px solid var(--line);
}
.pager:empty{display:none;}
.pager button{
  border:1.5px solid var(--rule);background:transparent;border-radius:3px;
  padding:7px 12px;cursor:pointer;font-weight:600;font-size:0.76rem;
}
.pager button:hover:not(:disabled){background:color-mix(in srgb,var(--rule) 7%,transparent);}
.pager button:disabled{opacity:0.32;cursor:default;}
.pager .where{
  font-family:'IBM Plex Mono',ui-monospace,monospace;
  font-size:0.7rem;color:var(--ink-soft);letter-spacing:0.04em;
  text-align:center;flex:1;
}

/* -------------------------------------------------------- my board */
.tablewrap{overflow-x:auto;-webkit-overflow-scrolling:touch;}
.rec{width:100%;border-collapse:collapse;font-family:'IBM Plex Mono',ui-monospace,monospace;}
.rec th{
  font-size:0.6rem;letter-spacing:0.14em;text-transform:uppercase;
  color:var(--ink-soft);font-weight:600;text-align:left;
  padding:0 8px 7px 0;border-bottom:2px solid var(--rule);white-space:nowrap;
}
.rec th.r,.rec td.r{text-align:right;padding-right:0;}
.rec td{padding:12px 8px 12px 0;border-bottom:1px solid var(--line);font-size:0.82rem;white-space:nowrap;}
.rec tr:last-child td{border-bottom:none;}
.rec .no{color:var(--ink-soft);font-size:0.72rem;}
.rec .lv{font-family:'IBM Plex Sans',sans-serif;font-weight:600;}
.rec .tm{font-weight:600;font-variant-numeric:tabular-nums;}
.rec .done{color:var(--add-text);font-weight:700;}
.rec .undone{color:var(--bad);font-weight:700;}
.rec .live{color:var(--mul-text);font-weight:700;}
.rec tr.me{background:color-mix(in srgb,var(--rank) 12%,transparent);}
.rec tr.me td{font-weight:700;}

#lb-tiers{margin-bottom:16px;}
.recsum{
  display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap;
  margin-top:14px;padding-top:11px;border-top:2px solid var(--rule);
  font-family:'IBM Plex Mono',ui-monospace,monospace;font-size:0.74rem;color:var(--ink-soft);
}
.recsum b{color:var(--ink);}
.recempty{font-size:0.85rem;color:var(--ink-soft);line-height:1.55;margin:6px 0 0;}

footer{
  text-align:center;font-family:'IBM Plex Mono',ui-monospace,monospace;
  font-size:0.66rem;color:var(--ink-soft);margin-top:26px;
  letter-spacing:0.03em;padding:0 16px;
}
.legalnav{margin-top:8px;}
.legalnav a{color:inherit;text-decoration:underline;text-underline-offset:2px;margin:0 6px;}
/* coarse-pointer devices have no arrow keys, N, or backspace to speak of */
@media (pointer:coarse){
  .kbd-only{display:none;}
}

/* --------------------------------------------------------- a11y bits */
:focus-visible{outline:3px solid var(--mul);outline-offset:2px;}

@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{
    animation-duration:0.01ms !important;
    animation-iteration-count:1 !important;
    transition-duration:0.01ms !important;
  }
}

@media (prefers-contrast:more){
  :root{--line:#8A8069;--ink-soft:#3A362E;--cageink:#3F6F68;}
}
@media (prefers-contrast:more) and (prefers-color-scheme:dark){
  :root{--line:#8C8264;--ink-soft:#D6CBAE;--cageink:#8FC4B8;}
}

/* very small phones: give the board every pixel available */
@media (max-width:400px){
  .wrap{padding-left:10px;padding-right:10px;}
  .acts button{padding:8px 11px;font-size:0.75rem;}
}

/* ============================================================
   Welcome, language picker and sign-in
   ============================================================ */

.modal.welcome,.modal.auth{max-width:440px;}

.langpick{border:1.5px solid var(--line);border-radius:3px;padding:12px 14px;margin:0 0 16px;}
.langpick legend{
  font-family:'IBM Plex Mono',ui-monospace,monospace;font-size:0.62rem;
  letter-spacing:0.14em;text-transform:uppercase;color:var(--ink-soft);padding:0 6px;
}
.langrow{display:flex;gap:8px;flex-wrap:wrap;}
/* same reasoning as .bigbtn[hidden] elsewhere: a class-based display always
   beats the browser's own [hidden] rule. Without this, switching Rankings
   to Overall mode would leave the tier tabs sitting there visibly. */
.langrow[hidden]{display:none;}
.langbtn{
  flex:1 1 120px;padding:10px 12px;cursor:pointer;border-radius:3px;
  border:1.5px solid var(--line);background:transparent;font-weight:600;font-size:0.84rem;
}
.langbtn:hover{border-color:var(--rule);}
.langbtn.on{background:var(--rule);border-color:var(--rule);color:var(--card);}

/* ---------------------------------------------------- rankings mode */
.lbmode{
  display:flex;border:1.5px solid var(--rule);border-radius:3px;
  overflow:hidden;margin-bottom:14px;
}
.lbmodebtn{
  flex:1 1 50%;padding:9px 10px;cursor:pointer;border:none;
  border-right:1.5px solid var(--rule);background:transparent;
  font-weight:600;font-size:0.82rem;color:var(--ink);
}
.lbmodebtn:last-child{border-right:none;}
.lbmodebtn.on{background:var(--rule);color:var(--card);}
.lbmodebtn:hover:not(.on){background:color-mix(in srgb,var(--rule) 8%,transparent);}

/* the sign-in call to action reuses .safe's green rather than a fixed color
   of its own — same reasoning as .askbtn.safe elsewhere: it inverts
   correctly in dark mode instead of carrying fixed colors. .ink was tried
   here once and collided with the unrelated cage-outline .ink SVG layer
   (position:absolute;inset:0), which silently stretched the button to
   cover the whole viewport — do not reuse "ink" as a button modifier. */
.askbtn.plain{background:transparent;border-width:1.5px;border-color:var(--ink);color:var(--ink);}
.askbtn.plain:hover{background:color-mix(in srgb,var(--ink) 7%,transparent);}

.authstep[hidden]{display:none;}
.fieldlab{
  display:block;font-family:'IBM Plex Mono',ui-monospace,monospace;
  font-size:0.62rem;letter-spacing:0.13em;text-transform:uppercase;
  color:var(--ink-soft);margin:14px 0 5px;
}
.field{
  width:100%;padding:11px 12px;font:inherit;font-size:1rem;
  border:1.5px solid var(--rule);border-radius:3px;background:var(--paper);color:var(--ink);
}
.field:focus{outline:3px solid var(--mul);outline-offset:1px;}
/* Safari zooms the whole page on focus if an input's font-size is under
   16px; 1rem above already clears that, this just guards a shrunk root. */
@media screen and (max-width:400px){.field{font-size:max(1rem,16px);}}
/* the autofilled-yellow can't be turned off directly in webkit; push the
   real background back in via a same-color inset shadow instead */
.field:-webkit-autofill,
.field:-webkit-autofill:hover,
.field:-webkit-autofill:focus{
  -webkit-text-fill-color:var(--ink);
  box-shadow:0 0 0 1000px var(--paper) inset;
  transition:background-color 9999s ease-in-out 0s;
}
.field.code{
  font-family:'IBM Plex Mono',ui-monospace,monospace;
  font-size:1.5rem;letter-spacing:0.4em;text-align:center;
}
.fieldhelp{font-size:0.75rem;line-height:1.5;color:var(--ink-soft);margin:6px 0 0;}
.fielderr{font-size:0.78rem;line-height:1.5;color:var(--bad);margin:8px 0 0;min-height:1em;font-weight:600;}
.fielderr:empty{margin:0;min-height:0;}
.fieldok{font-size:0.78rem;line-height:1.5;color:var(--add-text);margin:8px 0 0;min-height:1em;font-weight:600;}
.fieldok:empty{margin:0;min-height:0;}

.consent{
  display:flex;align-items:flex-start;gap:9px;cursor:pointer;
  font-size:0.82rem;line-height:1.5;color:var(--ink-soft);
  margin:16px 0 4px;
}
.consent input{
  accent-color:var(--ok);width:16px;height:16px;flex-shrink:0;
  margin-top:2px;cursor:pointer;
}

.authlinks{display:flex;justify-content:space-between;gap:10px;margin-top:12px;flex-wrap:wrap;}
.linkbtn{
  background:none;border:none;padding:4px 2px;cursor:pointer;
  font-size:0.76rem;font-weight:600;color:var(--ink-soft);
  text-decoration:underline;text-underline-offset:3px;
}
.linkbtn:hover{color:var(--ink);}
.linkbtn.centred{display:block;margin:14px auto 0;}

.acct{
  display:flex;justify-content:space-between;align-items:center;gap:10px;
  border:1.5px solid var(--line);border-radius:3px;
  padding:9px 11px;margin-bottom:14px;font-size:0.78rem;
}
.acct-who{color:var(--ink-soft);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.acct.in .acct-who{color:var(--ink);font-weight:600;}

