/* Notan Zoom lab. Notan's mono palette and type; the board is the page. */
@font-face { font-family: "Plex Mono"; font-weight: 500; font-display: swap; src: url("./fonts/IBMPlexMono-500.woff2") format("woff2"); }
@font-face { font-family: "Plex Sans"; font-weight: 400; font-display: swap; src: url("./fonts/IBMPlexSans-400.woff2") format("woff2"); }

:root {
  color-scheme: light;
  --bg: #ffffff;
  --ink: #000000;
  --text: #111111;
  --sub: #474747;
  --done: #737373;
  --line: #d4d4d4;
  --frame: #1c1c1c;
  --mark: #3a3a3a;
  --band: rgba(0, 0, 0, 0.05);
  --held: #6f6f6f;
  --tile: #c9c9c9;
  --tile-hi: #b5b5b5;
  --sunk: #f4f4f4;
  --scorch: rgba(44, 26, 10, 0.32);
  --smoke: rgba(60, 60, 60, 0.4);
  --well: #b3b3b3;
  --wall: #a3a3a3;
  --void: #9c9c9c;
  --scrim: rgba(0, 0, 0, 0.28);
  --star-core: #050505; --star-rim: #f0a640; --star-glow: rgba(240, 166, 64, 0.5); --star-halo: rgba(0, 0, 0, 0.13); --star-dust: #2e2e2e;
  --blast: #ffffff; --blast-edge: rgba(90, 110, 150, 0.55);
  --lift: 0 1px 0 rgba(0, 0, 0, 0.3), 0 7px 12px -6px rgba(0, 0, 0, 0.6);
  --press: inset 0 1px 3px rgba(0, 0, 0, 0.45);
  --wellsh: inset 0 2px 8px rgba(0, 0, 0, 0.22);
  --shadow: rgba(0, 0, 0, 0.45);
  --tile-drop: 0 6px 10px rgba(0, 0, 0, 0.18);
  --rise: -1px;
  --mono: "Plex Mono", "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  --sans: "Plex Sans", "IBM Plex Sans", system-ui, -apple-system, "Segoe UI", sans-serif;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;
    --bg: #000000; --ink: #ffffff; --text: #f2f2f2; --sub: #b8b8b8; --done: #8a8a8a;
    --line: #2f2f2f; --frame: #e4e4e4; --mark: #c4c4c4; --band: rgba(255, 255, 255, 0.08); --held: #8a8a8a;
  --tile: #3d3d3d; --tile-hi: #505050; --sunk: #141414; --scorch: rgba(255, 104, 24, 0.27); --smoke: rgba(200, 200, 200, 0.3); --well: #1b1b1b; --wall: #1c1c1c; --void: #000000; --scrim: rgba(0, 0, 0, 0.66);
  --star-core: #000000; --star-rim: #ffcf86; --star-glow: rgba(255, 196, 110, 0.55); --star-halo: rgba(255, 255, 255, 0.09); --star-dust: #e2e2e2;
  --blast: #fff4dc; --blast-edge: rgba(255, 150, 60, 0.6);
  --lift: 0 0 0 1px rgba(255, 255, 255, 0.12);
  --press: inset 0 1px 3px rgba(0, 0, 0, 0.75);
  --wellsh: inset 0 0 0 1px rgba(255, 255, 255, 0.07), inset 0 3px 10px rgba(0, 0, 0, 0.9);
  --shadow: rgba(255, 255, 255, 0.28);
  --tile-drop: 0 6px 12px rgba(255, 255, 255, 0.08);
  }
}
:root[data-theme="dark"] {
  color-scheme: dark;
  --bg: #000000; --ink: #ffffff; --text: #f2f2f2; --sub: #b8b8b8; --done: #8a8a8a;
  --line: #2f2f2f; --frame: #e4e4e4; --mark: #c4c4c4; --band: rgba(255, 255, 255, 0.08); --held: #8a8a8a;
  --tile: #3d3d3d; --tile-hi: #505050; --sunk: #141414; --scorch: rgba(255, 104, 24, 0.27); --smoke: rgba(200, 200, 200, 0.3); --well: #1b1b1b; --wall: #1c1c1c; --void: #000000; --scrim: rgba(0, 0, 0, 0.66);
  --star-core: #000000; --star-rim: #ffcf86; --star-glow: rgba(255, 196, 110, 0.55); --star-halo: rgba(255, 255, 255, 0.09); --star-dust: #e2e2e2;
  --blast: #fff4dc; --blast-edge: rgba(255, 150, 60, 0.6);
  --lift: 0 0 0 1px rgba(255, 255, 255, 0.12);
  --press: inset 0 1px 3px rgba(0, 0, 0, 0.75);
  --wellsh: inset 0 0 0 1px rgba(255, 255, 255, 0.07), inset 0 3px 10px rgba(0, 0, 0, 0.9);
  --shadow: rgba(255, 255, 255, 0.28);
  --tile-drop: 0 6px 12px rgba(255, 255, 255, 0.08);
}

*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; height: 100%; overflow: hidden; background: var(--bg); color: var(--text); font-family: var(--sans); -webkit-tap-highlight-color: transparent; }
/* No page zoom. touch-action is intersected across every ancestor, so
   pan-x pan-y on the root stops double-tap and pinch zoom page-wide (the
   readout, gaps between buttons, the space around a board or plate); the
   board's own none still wins inside it. Unlike the other games
   (docs/double-tap-zoom-20261011.md), pinch is off too: a pinch here could
   leave the board filling the screen with no way back. iOS Safari also needs
   the gesture events canceled in zoom.js. */
html, body { touch-action: pan-x pan-y; }
button { font: inherit; color: inherit; }
.sr-only { position: absolute !important; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }

.app {
  height: 100%;
  display: flex;
  flex-direction: column;
  padding-left: max(16px, env(safe-area-inset-left, 0px));
  padding-right: max(16px, env(safe-area-inset-right, 0px));
  padding-block: max(8px, env(safe-area-inset-top, 0px)) max(8px, env(safe-area-inset-bottom, 0px));
}
.top, .bar, .keys { position: relative; z-index: 3; background: var(--bg); }
.top { display: flex; flex-direction: column; align-items: center; gap: 4px; }
/* The one line at the top names the board and opens the menu. */
.menu-btn {
  appearance: none; border: 0; border-radius: 0; background: none; cursor: pointer; color: var(--sub);
  display: inline-flex; align-items: center; gap: 8px; min-height: 44px; padding: 0 12px; touch-action: manipulation;
}
.menu-btn svg { flex: none; opacity: 0.7; }
@media (hover: hover) and (pointer: fine) { .menu-btn:hover { color: var(--text); } .menu-btn:hover svg { opacity: 1; } }
:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
.blurb { margin: 0; max-width: 46ch; text-align: center; font-size: 13px; line-height: 1.4; color: var(--sub); text-wrap: balance; min-height: 2.8em; }
.readout { margin: 0; font: 500 12px/1.2 var(--mono); color: var(--sub); font-variant-numeric: tabular-nums; letter-spacing: 0.04em; }
/* A Mines readout ("board 40 · mines · 10×10 · 0/3 mines") wrapped at 320 px wide. */
@media (max-width: 359px) { .readout { font-size: 11px; letter-spacing: 0; } .menu-btn { padding: 0 6px; } }

.stage { flex: 1; min-height: 0; position: relative; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px; overflow: hidden; }
/* While the camera moves, the play area's top and bottom edges go soft. */
.keys { margin: 0; text-align: center; font-size: 11px; color: var(--sub); text-wrap: balance; }
/* a short message after a press that changes nothing on the board */
.keys.note { color: var(--text); }

/* ---------- the menu: Notan's sheet ---------- */
dialog.sheet {
  width: min(30rem, calc(100vw - 16px)); max-width: none;
  max-height: calc(100dvh - 16px - env(safe-area-inset-top) - env(safe-area-inset-bottom));
  height: fit-content; padding: 0; margin: auto; border: 1px solid var(--frame); border-radius: 0;
  background: var(--bg); color: var(--text); overflow: hidden;
}
dialog.sheet[open] { display: block; }
dialog.sheet::backdrop { background: var(--scrim); }
.sheet-in { max-height: calc(100dvh - 18px - env(safe-area-inset-top) - env(safe-area-inset-bottom)); display: flex; flex-direction: column; min-height: 0; font: 400 15px/1.45 var(--sans); }
.sheet-head { display: flex; flex: none; align-items: center; justify-content: space-between; gap: 12px; padding: 6px 8px 6px 16px; border-bottom: 1px solid var(--line); }
.sheet-head form { margin: 0; }
.sheet h2 { margin: 0; font: 500 15px/1.3 var(--mono); }
.sheet h3 { margin: 0 0 8px; font: 500 13px/1.3 var(--mono); color: var(--sub); }
.sheet-x { display: grid; place-items: center; width: 44px; height: 44px; padding: 0; border: 0; background: none; color: var(--text); cursor: pointer; }
.sheet-x svg { width: 22px; height: 22px; }
.sheet-body { min-height: 0; overflow: auto; overscroll-behavior: contain; padding: 16px 20px 20px; }
.sheet section { border-top: 1px solid var(--line); margin-top: 14px; padding-top: 12px; }
.lv-meta { margin: 0 0 8px; font: 500 12px/1.3 var(--mono); color: var(--sub); }
.lv-grid { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(44px, 1fr)); gap: 4px; }
.lv, .other {
  appearance: none; border: 1px solid transparent; border-radius: 0; background: none; color: var(--sub);
  font: 500 14px/1.2 var(--mono); cursor: pointer; touch-action: manipulation;
}
.lv { position: relative; width: 100%; min-height: 44px; padding: 0; font-variant-numeric: tabular-nums; }
.lv.done { color: var(--ink); }
/* a twist (merge, patch or a bonus) wears a small square under its number */
.lv.twist::after { content: ""; position: absolute; left: 50%; bottom: 6px; width: 4px; height: 4px; margin-left: -2px; background: currentColor; opacity: 0.55; }
.lv[aria-current="true"], .other[aria-current="true"] { border-color: var(--frame); color: var(--ink); }
.others { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: 1fr 1fr; gap: 4px; }
.other { display: flex; flex-direction: column; width: 100%; height: 100%; min-height: 44px; padding: 8px 10px; gap: 2px; text-align: left; }
.other-name { color: var(--text); }
.other-blurb { font: 400 12px/1.35 var(--sans); }
@media (hover: hover) and (pointer: fine) { .lv:hover, .other:hover { background: var(--band); } }
.setting { display: flex; align-items: center; gap: 10px; min-height: 44px; cursor: pointer; font-size: 14px; }
.setting input { accent-color: var(--ink); width: 18px; height: 18px; margin: 0; cursor: pointer; }
@media (max-width: 480px) { .sheet-body { padding: 12px 14px 16px; } }

/* ---------- the bar ----------
 * Icons only. Each label stays in the button as its accessible name and
 * the title gives the tooltip. The icons say how far each one reaches:
 * Clear board is one board with a restart arrow, New game is the nested
 * boards of the whole sequence with a plus. New game asks first, in the
 * bar, when there is progress to lose. */
.bar { padding-top: 4px; }
/* .tb sets display: flex, which would otherwise beat [hidden] */
.bar [hidden] { display: none !important; }
.bar-main { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(44px, 64px); justify-content: center; min-height: 48px; }
.bar-main .tb { position: relative; justify-content: center; }
.bar-main .tb-label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.rb-icon { display: flex; }
.tb {
  appearance: none; border: 0; border-radius: 0; background: none; cursor: pointer;
  display: flex; flex-direction: column; align-items: center; justify-content: flex-start; gap: 2px;
  min-width: 44px; min-height: 44px; padding: 5px 2px 3px;
  font: 500 11px/1.15 var(--mono); letter-spacing: 0.02em; color: var(--sub);
  touch-action: manipulation; -webkit-tap-highlight-color: transparent;
}
.tb svg { width: 24px; height: 24px; flex: none; }
.tb-label { max-width: 100%; text-align: center; text-wrap: balance; }
@media (hover: hover) and (pointer: fine) { .tb:not([aria-disabled="true"]):hover { color: var(--text); background: var(--band); } }
.tb:not([aria-disabled="true"]):active { color: var(--text); background: var(--band); }
.tb[aria-disabled="true"] { color: var(--done); cursor: default; }
#tool-btn[data-tool="mark"] { color: var(--text); box-shadow: inset 0 -2px 0 var(--ink); }
#tool-btn[data-tool="mark"] .m-fill, #tool-btn[data-tool="fill"] .m-dot { display: none; }
.bar .tb:focus-visible { outline: 2px solid var(--ink); outline-offset: -2px; }
.bar-ask { display: grid; grid-template-columns: minmax(0, 1fr) 56px 56px; gap: 4px; align-items: stretch; min-height: 48px; }
.ask-text { margin: 0; align-self: center; padding-inline-start: 4px; font: 400 13px/1.3 var(--sans); color: var(--text); text-wrap: balance; }
/* Keep playing, the safe choice, is drawn stronger; New game stays plain. */
.bar-ask .tb { color: var(--text); }
#ask-no { box-shadow: inset 0 0 0 1.5px var(--ink); }
.bar .bar-ask .tb:focus-visible { outline-offset: 2px; }
/* room between the two choices, so a near miss lands on neither */
.bar-ask .tb + .tb { margin-inline-start: 8px; }

/* ---------- board ----------
 * No grid lines. Every square is a tile on the page, inset from its cell:
 * open squares are faint tiles flush with the page, your ink is raised
 * and casts a shadow, a square marked empty sinks, and the picture
 * carried over from the last solve sits recessed in a well, a layer
 * below. Each zoom pushes the past one layer down. */
.pz { display: grid; position: relative; user-select: none; -webkit-user-select: none; -webkit-touch-callout: none; touch-action: none; }
.clue { display: flex; font: 500 var(--f)/1.15 var(--mono); font-variant-numeric: tabular-nums; color: var(--text); }
.clue span { transition: color 0.25s; }
.clue span.done { color: var(--done); }
.cc { flex-direction: column; justify-content: flex-end; align-items: center; padding-bottom: calc(var(--f) * 0.45); }
.rc { justify-content: flex-end; align-items: center; gap: 0.5em; padding-right: calc(var(--f) * 0.6); }

.pz-bg { position: absolute; z-index: 0; background: var(--bg); border-radius: calc(var(--r) * 1.6); transition: background-color var(--fade, 0.8s) ease, box-shadow var(--fade, 0.8s) ease; pointer-events: none; }
.pz:focus, .pz:focus-visible { outline: none; }
/* The board and the Patch tiles draw a focus ring only with "Focus outline" on.
   Without it a keyboard player still sees the square cursor on the board
   (shown as soon as the board takes keyboard focus) and a lifted tile on the plate. */
:root[data-focus-ring] .pz:focus-visible { outline: 2px solid var(--ink); outline-offset: 6px; border-radius: 4px; }
.cell { position: relative; z-index: 1; cursor: pointer; }
.cell::before {
  content: "";
  position: absolute;
  inset: var(--in);
  border-radius: var(--r);
  background: var(--tile);
  transition: background-color 0.18s ease, box-shadow 0.18s ease, transform 0.18s ease;
}
/* groups of five get a slightly wider seam instead of a heavier line */
.cell.g5r::before { right: calc(var(--in) + var(--seam)); }
.cell.g5l::before { left: calc(var(--in) + var(--seam)); }
.cell.g5b::before { bottom: calc(var(--in) + var(--seam)); }
.cell.g5t::before { top: calc(var(--in) + var(--seam)); }
@media (hover: hover) { .cell:not(.given):not(.ink):not(.hit):hover::before { background: var(--tile-hi); } }
.cell.ink::before { background: var(--ink); box-shadow: var(--lift); transform: translateY(var(--rise)); }
.cell.mark::before { background: var(--sunk); box-shadow: var(--press); }
.cell.mark::after { content: ""; position: absolute; left: 50%; top: 50%; width: 6px; height: 6px; margin: -3px 0 0 -3px; border-radius: 50%; background: var(--mark); }
.cell.given { cursor: default; }
/* Locked squares, carried over from the last solve or given as a nudge,
 * are pressed into the page: gray, no lift. Locked empty squares leave a
 * gap in the well. */
.cell.given::before { background: transparent; }
.cell.given.ink::before { background: var(--held); box-shadow: var(--press); transform: none; }
/* A locked filled square in a row or column run that your own ink joins lights
   up to ink, with a thin gray inner ring (a crisp edge: the soft pressed
   shadow blurred on white in dark mode), so the run reads as one block. */
.cell.given.ink.joined::before { background: var(--ink); box-shadow: inset 0 0 0 1.5px var(--held); }
.cell .thumb { position: absolute; left: var(--in); top: var(--in); border-radius: var(--r); }
/* Pixel's middle square is an ordinary locked filled square (gray, or ink
   when your run joins it) with the last board pressed into it, faintly. */
.region { position: absolute; z-index: 0; border-radius: calc(var(--r) * 1.6); background: var(--well); box-shadow: var(--wellsh); pointer-events: none; }
/* The hole lies over its own locked squares and takes no clicks. */
/* The hole is a round well under the squares (its own squares draw nothing),
   turning slowly the chain's way round. */
.hole { position: absolute; z-index: 0; pointer-events: none; animation: swirl-cw 90s linear infinite; }
.hole.ccw { animation-name: swirl-ccw; }
/* Gravity's star lies under the squares (its own draws nothing), so the
   squares round it stay crisp, and turns faster the heavier it is. */
.star { position: absolute; z-index: 0; pointer-events: none; animation: swirl-cw var(--spin, 40s) linear infinite; }
.star.ccw { animation-name: swirl-ccw; }
.cell.star-sq::before, .cell.star-sq::after { opacity: 0; }
.star.gulp { animation: gulp 1.4s cubic-bezier(.45,0,.55,1), swirl-cw var(--spin, 40s) linear infinite; }
.star.ccw.gulp { animation: gulp 1.4s cubic-bezier(.45,0,.55,1), swirl-ccw var(--spin, 40s) linear infinite; }
@keyframes gulp { 0% { scale: 1; } 45% { scale: 1.14; filter: brightness(1.2); } 100% { scale: 1; } }
@keyframes swirl-cw { to { transform: rotate(360deg); } }
@keyframes swirl-ccw { to { transform: rotate(-360deg); } }
/* Squares near it lean in, shrink and turn, in a slow wave round the hole. */
/* Only the drawing moves (the tile and its mark), never the square's box,
   so a tap lands where the square sits. */
.cell.pull::before, .cell.pull::after { animation: pull 5s ease-in-out var(--dl, 0s) infinite alternate; }
@keyframes pull {
  from { transform: translate(calc(var(--dx) * var(--a0)), calc(var(--dy) * var(--a0))) rotate(var(--r0)) scale(var(--s0)); }
  to { transform: translate(calc(var(--dx) * var(--a1)), calc(var(--dy) * var(--a1))) rotate(var(--r1)) scale(var(--s1)); }
}
.cursor { position: absolute; z-index: 2; width: var(--cell); height: var(--cell); border-radius: calc(var(--r) + 2px); box-shadow: 0 0 0 2px var(--ink); pointer-events: none; display: none; }
.cursor.on { display: block; }
/* Solved: the board sinks into one well and only the picture stays, which
 * is exactly how it will sit in the next board when the camera lands. */
.pz.solved .pz-bg { background: var(--well); box-shadow: var(--wellsh); }
.pz.solved .region { opacity: 0; transition: opacity var(--fade, 0.8s) ease; }
.pz.solved .cell:not(.ink):not(.given)::before { opacity: 0; transition: opacity var(--fade, 0.8s) ease; }
.pz.solved .cell.mark::after, .pz.solved .cell.given:not(.ink)::after { opacity: 0; transition: opacity var(--fade, 0.8s) ease; }
.pz.solved .clue span { color: var(--done); }
.pz.solved .cursor { display: none; }

/* ---------- mines ----------
 * A found mine: a sunk tile like a mark, a dashed outline and a cross, so
 * it reads without hue (and the cross, drawn in currentColor, survives
 * forced colors). A pick you are holding shows an inset ring before it
 * counts. A lost board outlines the answer squares you had not filled. */
.cell.hit { cursor: default; }
.cell.hit::before { background: radial-gradient(circle at 40% 60%, var(--scorch) 0 26%, transparent 64%), radial-gradient(circle at 64% 38%, var(--scorch) 0 16%, transparent 56%), radial-gradient(circle, var(--scorch) 0 8%, transparent 78%), var(--sunk); box-shadow: var(--press); outline: 1.5px dashed var(--ink); outline-offset: -2px; }
/* A mine going off (zoom.js boom): the one place the look gets loud.
 * --u is the square's size; --tx/--ty are directions in squares. */
.boom { position: absolute; width: 0; height: 0; z-index: 6; pointer-events: none; }
.boom i { position: absolute; left: 0; top: 0; display: block; }
.boom .flash { width: calc(var(--u) * 3); height: calc(var(--u) * 3); margin: calc(var(--u) * -1.5); border-radius: 50%;
  background: radial-gradient(circle, #fff 0 14%, #ffe2a0 26%, rgba(255, 150, 50, 0.6) 44%, transparent 68%);
  animation: boom-flash 0.26s ease-out 0.12s both; }
.boom .shard { width: calc(var(--u) * 0.24); height: calc(var(--u) * 0.18); margin: calc(var(--u) * -0.09) 0 0 calc(var(--u) * -0.12);
  background: var(--held); clip-path: polygon(0 10%, 100% 0, 85% 75%, 25% 100%); animation: boom-shard 0.66s cubic-bezier(.15,.6,.3,1) both; }
.boom .spark { width: 3px; height: 3px; margin: -1.5px; border-radius: 50%; background: #ffc061; box-shadow: 0 0 6px 1px rgba(255, 150, 50, 0.85);
  animation: boom-spark 0.52s ease-out var(--dl) both; }
.boom .tri { width: calc(var(--u) - 2 * var(--in)); height: calc(var(--u) - 2 * var(--in)); margin: calc((var(--u) - 2 * var(--in)) * -0.5); border-radius: var(--r);
  background: linear-gradient(160deg, var(--tile-hi), var(--held)); animation: boom-tri 0.95s cubic-bezier(.2,.7,.35,1) 0.16s both; }
@keyframes boom-tri { 0% { transform: none; opacity: 1; } 70% { opacity: 1; }
  100% { transform: translate(calc(var(--tx) * var(--u)), calc(var(--ty) * var(--u) + var(--u) * 0.5)) rotate(var(--rot)) scale(0.4); opacity: 0; } }
.blast { position: absolute; left: 0; top: 0; z-index: 7; pointer-events: none; }
.boom .smoke { width: var(--u); height: var(--u); margin: calc(var(--u) * -0.5); border-radius: 50%;
  background: radial-gradient(circle, var(--smoke), transparent 66%); animation: boom-smoke 1.15s ease-out var(--dl) both; }
@keyframes boom-flash { 0% { opacity: 0; transform: scale(0.3); } 22% { opacity: 1; } 100% { opacity: 0; transform: scale(1.15); } }
@keyframes boom-shard { 0% { transform: none; opacity: 1; } 75% { opacity: 1; }
  100% { transform: translate(calc(var(--tx) * var(--u) * 1.7), calc(var(--ty) * var(--u) * 1.7 + var(--u) * 0.5)) rotate(var(--rot)); opacity: 0; } }
@keyframes boom-spark { 0% { transform: none; opacity: 1; } 100% { transform: translate(calc(var(--tx) * var(--u) * 2.1), calc(var(--ty) * var(--u) * 2.1 + var(--u) * 0.3)); opacity: 0; } }
@keyframes boom-smoke { 0% { transform: scale(0.4); opacity: 0; } 25% { opacity: 1; } 100% { transform: translate(calc(var(--tx) * var(--u)), calc(var(--u) * -0.55)) scale(1.4); opacity: 0; } }
.cell .x { position: absolute; z-index: 1; left: 50%; top: 50%; width: 46%; height: 46%; transform: translate(-50%, -50%); color: var(--ink); pointer-events: none; }
.cell.hit.now .x { animation: pop 0.18s ease-out both; }
@keyframes pop { from { transform: translate(-50%, -50%) scale(0.6); } to { transform: translate(-50%, -50%) scale(1); } }
.pz.solved .cell.hit .x { opacity: 0; transition: opacity 0.4s ease; }
.cell.pend::before { box-shadow: inset 0 0 0 2px var(--ink); background: var(--tile-hi); transform: none; }
.cell.pm::after { content: ""; position: absolute; left: 50%; top: 50%; width: 6px; height: 6px; margin: -3px 0 0 -3px; border-radius: 50%; background: var(--mark); }
.pz.lost .cell { cursor: default; }
.pz.lost .cell:not(.ink):not(.given):not(.hit):not(.answer)::before { opacity: 0.35; }
.pz.lost .cell.answer::before { background: transparent; box-shadow: inset 0 0 0 2px var(--ink); transform: none; }
.pz.lost .cell.mark:not(.answer)::after { opacity: 0; }
.lost-cap { display: flex; flex-direction: column; align-items: center; gap: 8px; animation: rise 0.5s ease 0.3s both; }
.lost-cap .tb { flex-direction: row; gap: 6px; padding: 0 14px; font-size: 13px; color: var(--text); box-shadow: inset 0 0 0 1px var(--line); }
.lost-cap .tb:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }

/* Camera moves. The old view is lifted out and rides above the new one;
 * its numbers go first so only the picture travels. The new view's fresh
 * squares and numbers come up while the camera is still moving. */
/* No will-change: a layer promoted ahead of time is painted once at its
   resting size and then stretched, so a board flying in at 2 to 5 times
   size looked soft until it landed. While the camera moves, shadows are
   dropped (scaled up they smear) and come back as it lands. */
.stage.zooming .cell::before { box-shadow: none; }
.leaving { z-index: 2; pointer-events: none; }
.leaving .clue { opacity: 0; transition: opacity var(--fade, 0.8s) ease; }
/* The old board's own backing goes with its numbers: it would land as a
   second, smaller well inside the new board's (a merged board covers two
   and a half squares of a three-square seed). */
/* The board's backing travels with the picture, so it never floats alone. */
.leaving .cursor { display: none; }
/* A first board fades in whole. A board that a camera move brings in is
   hidden ("arriving") while the old one flies in, then fades in as the old
   picture fades out: one crossfade, no per-square animation or shadow paint. */
.pz.enter:not(.arriving) { animation: rise var(--fade, 0.8s) ease both; }
.pz .clue, .pz .cell, .pz .region, .pz .hole, .pz .star { transition: opacity var(--fade, 0.8s) ease; }
.pz.arriving .clue, .pz.arriving .cell, .pz.arriving .region, .pz.arriving .hole, .pz.arriving .star { opacity: 0; }
.pz.arriving-seed .cell.given, .pz.arriving-seed .region { opacity: 0; }
.pz.collapsing .star, .pz.collapsing .hole { opacity: 1; transition: none; }
@keyframes rise { from { opacity: 0; } to { opacity: 1; } }

/* ---------- patch world: a tilted plate over a shaft of older layers ---------- */
.scene { position: relative; flex: none; perspective: 1700px; perspective-origin: 50% 50%; transition: perspective 0.48s cubic-bezier(.4,0,.2,1); }
/* Level, the scene is seen straight on with no perspective, so the shaft
 * collapses and the layer below fills the window exactly; tilting brings
 * the perspective back with it. */
.scene.flat { perspective: 40000px; }
.scene.instant { transition: none; }
.base { position: absolute; inset: 0; pointer-events: none; }
.scene.enter .base { animation: rise 0.8s ease 0.3s both; }
.base canvas { display: block; }
.tilt, .stack { position: absolute; inset: 0; transform-style: preserve-3d; }
/* Only the tiles take clicks: the plate behind them is coplanar, and in 3D
 * hit-testing it can win a tie and swallow a click on the lower row. */
.tilt { pointer-events: none; }
.tile { pointer-events: auto; }
.tilt { transform: rotateX(var(--tilt)); transition: transform 0.48s cubic-bezier(.4,0,.2,1); }
.scene.flat .tilt { transform: none; }
.scene.instant .tilt { transition: none; }
.layer { position: absolute; pointer-events: none; translate: calc(var(--px, 0) * var(--d, 1) * -3px) calc(var(--py, 0) * var(--d, 1) * -3px); transition: translate 0.35s ease-out; }
.layer canvas { position: absolute; inset: 0; }
.layer .fresh { animation: fade-out 0.9s ease 1.3s both; }
@keyframes fade-out { from { opacity: 1; } to { opacity: 0; } }
.wall { position: absolute; pointer-events: none; background: var(--wall); backface-visibility: visible; transition: opacity 0.48s ease; }
/* Level, the walls are edge-on: hide them, and let them grow in with the tilt. */
.scene.flat .wall { opacity: 0; }
.scene.instant .wall { transition: none; }
.wall.top { background: linear-gradient(to bottom, var(--wall), var(--well)); }
.wall.left, .wall.right { background: linear-gradient(to right, var(--wall), var(--well)); filter: brightness(0.94); }
.wall.bottom { filter: brightness(0.85); }
/* The plate is one flat plane (base and tiles together), so nothing in it
 * can lose a depth tie to anything else. */
.world { position: absolute; inset: 0; display: grid; grid-template-columns: repeat(3, 1fr); grid-template-rows: repeat(3, 1fr); gap: var(--wgap); transform-style: flat; }
.tile { appearance: none; border: 0; padding: 0; background: none; cursor: pointer; display: block; position: relative; border-radius: var(--wr); transition: transform 0.2s ease, box-shadow 0.2s ease; }
.tile canvas { display: block; }
.tile.center { cursor: default; pointer-events: none; }
.tile.done { cursor: default; }
@media (hover: hover) { .tile:not(:disabled):hover { transform: translateY(-3px); box-shadow: var(--tile-drop); } }
.tile:focus-visible { outline: none; transform: translateY(-3px); box-shadow: var(--tile-drop); }
:root[data-focus-ring] .tile:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
@media (forced-colors: active) { .tile:focus-visible { outline: 2px solid Highlight; } }
.scene.enter .tile:not(.center) { animation: rise 0.8s ease 0.6s both; }
.world-cap { margin: 0; font: 500 12px/1 var(--mono); color: var(--sub); animation: rise 0.5s ease 0.3s both; }
/* the plate's caption waits for the camera, or it draws over the move */
.stage.zooming .world-cap { visibility: hidden; }

@media (prefers-reduced-motion: reduce) {
  .pz.enter, .pz .clue, .pz .cell, .pz .region, .pz .hole, .hole, .scene.enter .tile, .scene.enter .base, .layer .fresh, .world-cap, .lost-cap, .cell.hit.now .x { animation: none; }
  .layer .fresh { opacity: 0; }
  .boom { display: none; }
  .star { animation: none; }
  .blast { display: none; }
  .cell.pull::before, .cell.pull::after { animation: none; transform: translate(calc(var(--dx) * var(--a1)), calc(var(--dy) * var(--a1))) rotate(var(--r1)) scale(var(--s1)); }
  .tilt, .wall { transition: none; }
  .cell::before, .tile { transition: none; }
}

/* 320 to 360: six columns need 52px each. Borrow 12px of each 16px gutter
   (4px still clear of the edge) and drop letter-spacing, so "Filling" fits
   on one line. */
@media (max-width: 360px) {
  .bar { margin-inline: -12px; }
  .tb { padding-inline: 0; letter-spacing: 0; }
}
/* desktop and tablet: icon beside label, one row */
@media (min-width: 768px) and (min-height: 481px) {
  .bar-main { display: flex; justify-content: center; gap: 4px; min-height: 44px; }
  .tb { flex-direction: row; justify-content: center; align-items: center; gap: 6px; padding: 0 12px; font-size: 13px; line-height: 1; }
  .tb-label { white-space: nowrap; }
  #back-btn, #back-btn[hidden] + #new-btn { margin-inline-start: 16px; }
  .bar-ask { display: flex; justify-content: center; align-items: center; gap: 8px; min-height: 44px; }
  .ask-text { max-width: 40ch; }
  .bar-ask .tb + .tb { margin-inline-start: 24px; }
}
/* short landscape phones: a compact one-row bar, so the board keeps its height */
@media (max-height: 480px) and (min-width: 560px) {
  .bar-main { display: flex; justify-content: center; gap: 2px; min-height: 44px; }
  .tb { flex-direction: row; justify-content: center; align-items: center; gap: 4px; padding: 0 6px; line-height: 1; }
  .tb svg { width: 20px; height: 20px; }
  .tb-label { white-space: nowrap; }
  .bar-ask { display: flex; justify-content: center; align-items: center; gap: 6px; min-height: 44px; }
  .ask-text { max-width: 26ch; }
  .bar-ask .tb + .tb { margin-inline-start: 24px; }
}
/* Forced colors (Windows High Contrast) drop backgrounds and shadows, and
 * every square here is drawn with them. So the board draws itself in system
 * colors: open squares outlined, your ink solid, a mark a dot, a locked
 * filled square hatched with a double edge, a locked empty square a dash,
 * a found mine a dashed square with a cross. */
@media (forced-colors: active) {
  .boom { display: none; }
  #tool-btn[data-tool="mark"] { box-shadow: none; border-bottom: 2px solid CanvasText; }
  #ask-no { box-shadow: none; border: 2px solid ButtonText; }
  .tb[aria-disabled="true"] { color: GrayText; }
  .pz .cell { forced-color-adjust: none; }
  .pz .cell::before { background: Canvas; border: 1px solid CanvasText; box-shadow: none; transform: none; }
  .pz .cell.ink::before { background: CanvasText; }
  .pz .cell.mark::before { background: Canvas; }
  .pz .cell.mark::after, .pz .cell.pm::after { background: CanvasText; }
  .pz .cell.given::before { border-color: transparent; }
  .pz .cell.given.ink::before { background: repeating-linear-gradient(45deg, CanvasText 0 1.5px, Canvas 1.5px 4px); border: 3px double CanvasText; }
  /* only here does a locked empty square get a dash: system colors draw no well */
  .pz .cell.given:not(.ink)::after { content: ""; position: absolute; left: 50%; top: 50%; width: 32%; height: 2px; transform: translate(-50%, -50%); border-radius: 1px; background: CanvasText; }
  .pz .cell.hit::before { background: Canvas; outline: 1.5px dashed CanvasText; outline-offset: -3px; border-color: transparent; }
  .pz .cell .x { color: CanvasText; }
  .pz .cell.pend::before { background: Canvas; border: 3px solid Highlight; }
  .pz.lost .cell.answer::before { background: Canvas; border: 3px solid CanvasText; }
  .pz.solved .cell:not(.ink):not(.given)::before { border-color: transparent; }
  .cell.has-thumb::after { content: ""; position: absolute; z-index: 1; inset: var(--in); border-radius: var(--r); border: 2px solid CanvasText; }
  .cursor { box-shadow: none; outline: 2px solid Highlight; }
  .clue span.done { forced-color-adjust: none; color: GrayText; }
  .region { box-shadow: none; border: 1px solid GrayText; }
}
