:root {
  --paper: #FAF9F6;
  --paper-line: #E4E1D8;
  --panel-bg: #F3F1EA;
  --ink: #2B2A27;
  --ink-soft: #6B6862;
  --accent: #CC785C;
  --accent-dark: #B35F45;
  --accent-soft: #F3E1D9;
  --error: #C0392B;
  --error-soft: #FBEAE7;
  --given: #2B2A27;
  --user-entry: #2C4A6E;
  --grid-line: #BBB5A6;
  --grid-line-strong: #2B2A27;
  --fog: #BFB8A6;
  --fog-dark: #A79E88;
  --fog-line: #9C927C;
  --pen: #2F6F8F;
  /* Interaction washes, defined per theme. They are painted over the cell,
     so a dark ink wash that reads on cream paper does almost nothing on a
     near-black one. The peer shade used to be a single flat 6% black for
     both themes, which left the cells a selection sees all but invisible
     in the dark theme. */
  --hl-peer: rgba(43, 42, 39, 0.13);
  --hl-selected: rgba(204, 120, 92, 0.50);
  --hl-same: rgba(179, 95, 69, 0.34);
  --hl-conflict: rgba(192, 57, 43, 0.28);
  --line-thin: 1px;
  --line-thick: 2.5px;
  --radius: 10px;
  --serif: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
  --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
}

/* Dark theme. Follows the system unless the player picks a theme in
   settings, which stamps data-theme on <html> and wins either way. */
:root[data-theme="dark"] {
  --paper: #1A1A18;
  --paper-line: #35342F;
  --panel-bg: #232320;
  --ink: #E8E5DD;
  --ink-soft: #9C978C;
  --accent: #CC785C;
  --accent-dark: #E09277;
  --accent-soft: #3A2A23;
  --error: #E2645A;
  --error-soft: #3A2320;
  --given: #E8E5DD;
  --user-entry: #7FB0E8;
  --grid-line: #56544C;
  --grid-line-strong: #C8C4BA;
  --fog: #4A463D;
  --fog-dark: #3A362F;
  --fog-line: #6E695E;
  --pen: #7FC4E8;
  /* Light washes here rather than dark ones -- see the note on :root. */
  --hl-peer: rgba(232, 229, 221, 0.14);
  --hl-selected: rgba(224, 146, 119, 0.52);
  --hl-same: rgba(224, 146, 119, 0.30);
  --hl-conflict: rgba(226, 100, 90, 0.38);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --paper: #1A1A18;
    --paper-line: #35342F;
    --panel-bg: #232320;
    --ink: #E8E5DD;
    --ink-soft: #9C978C;
    --accent: #CC785C;
    --accent-dark: #E09277;
    --accent-soft: #3A2A23;
    --error: #E2645A;
    --error-soft: #3A2320;
    --given: #E8E5DD;
    --user-entry: #7FB0E8;
    --grid-line: #56544C;
    --grid-line-strong: #C8C4BA;
    --fog: #4A463D;
    --fog-dark: #3A362F;
    --fog-line: #6E695E;
    --pen: #7FC4E8;
    --hl-peer: rgba(232, 229, 221, 0.14);
    --hl-selected: rgba(224, 146, 119, 0.52);
    --hl-same: rgba(224, 146, 119, 0.30);
    --hl-conflict: rgba(226, 100, 90, 0.38);
  }
}

* { box-sizing: border-box; }

/* The `hidden` attribute's default UA rule (`[hidden]{display:none}`) has
   the same specificity as any single-class `display` rule below it, and
   author styles win ties — so without this, toggling `.hidden` on an
   element that also matches e.g. `.layout{display:flex}` does nothing
   visually. This keeps `hidden` authoritative everywhere in the app. */
[hidden] { display: none !important; }

html, body {
  margin: 0;
  height: 100%;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--sans);
  -webkit-tap-highlight-color: transparent;
  overscroll-behavior: none;
}

button {
  font-family: inherit;
  cursor: pointer;
}

.app {
  /* A definite height, not a minimum. Everything inside sizes off it --
     the board asks its wrapper how tall it may be, and that answer is only
     a real number if the chain above it is definite; with min-height the
     wrapper's height was indefinite and the board fell back to nothing.
     It also keeps the game on one screen, which is the point: the parts
     that can give way (the rules) scroll within themselves instead.

     Small viewport height, not dynamic: dvh grows and shrinks as a mobile
     browser's address bar hides and reappears, and since everything here
     is measured against this one number, the whole layout resized with it
     and the keypad slid up and down the screen while you were using it.
     svh is the height with the bar showing and never changes, so nothing
     moves. In an installed copy there is no bar and the two are equal. */
  height: 100vh;
  height: 100svh;
  display: flex;
  flex-direction: column;
  padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
}

/* ---------- Header ---------- */
.titlebar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 12px 16px 8px;
  gap: 10px;
}
.titlebar-main {
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-width: 0;   /* so a long name can ellipsize rather than push the timer */
}
.puzzle-name {
  font-size: 19px;
  font-weight: 700;
  color: var(--ink);
  line-height: 1.15;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* The constraint marks reuse the menu's variant icons, shrunk down. They
   are a reminder, not a control: muted, and never wider than the name. */
.puzzle-tags {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 7px;
  color: var(--ink-soft);
}
.puzzle-tags svg {
  width: 15px;
  height: 15px;
  display: block;
}
.timer {
  font-size: 28px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}
.mistakes {
  font-size: 13px;
  font-weight: 600;
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}
.icon-btn {
  background: none;
  border: none;
  color: var(--ink);
  padding: 6px;
  display: flex;
  border-radius: 8px;
}
.icon-btn:active { background: var(--panel-bg); }

/* ---------- Layout ---------- */
.layout {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 0 12px 12px;
  min-height: 0;
}

.puzzle-col {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
}

.board-wrap {
  flex: 1;
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 0;
  /* Establishes a query container so .board can size itself off this
     element's own (definite) inline size even when nested inside the
     fit-content .sandwich-wrap — a plain "%" there would create a
     circular dependency (fit-content parent sized by a percentage
     child) and collapse the board to near-zero. Sized on both axes so a
     short landscape window can bound the board by height as well: against
     the viewport alone it ignored the titlebar and ran off the bottom. */
  container-type: size;
}

.board {
  position: relative;
  /* 100cqh is the wrapper's own height: without it the board kept its
     viewport-derived size while its slot shrank, and overflowed equally
     above and below -- far enough on a phone to sit over the titlebar and
     under the rules. */
  width: min(94vw, 68svh, 100cqh, 640px);
  aspect-ratio: 1 / 1;
  background: var(--paper);
  border: 4px solid var(--grid-line-strong);
  border-radius: 8px;
  overflow: hidden;
}

/* Sandwich sudoku row/column clues live outside the board itself, so
   the board sits in a small grid wrapper with a thin clue strip above
   and to the left. When no sandwich puzzle is loaded the strips
   collapse to 0 and the wrapper hugs the board exactly as before. */
.sandwich-wrap {
  display: grid;
  grid-template-columns: 0px 1fr;
  grid-template-rows: 0px 1fr;
  width: fit-content;
  height: fit-content;
  max-width: 100%;
  max-height: 100%;
}
.sandwich-wrap.has-clues {
  grid-template-columns: 24px 1fr;
  grid-template-rows: 24px 1fr;
  gap: 4px;
}
.sandwich-corner { grid-column: 1; grid-row: 1; }
.sandwich-col-clues {
  grid-column: 2;
  grid-row: 1;
  display: none;
  grid-template-columns: repeat(9, 1fr);
  align-items: end;
  justify-items: center;
  padding-bottom: 2px;
}
.sandwich-row-clues {
  grid-column: 1;
  grid-row: 2;
  display: none;
  grid-template-rows: repeat(9, 1fr);
  align-items: center;
  justify-items: end;
  padding-right: 3px;
}
.sandwich-wrap.has-clues .sandwich-col-clues,
.sandwich-wrap.has-clues .sandwich-row-clues {
  display: grid;
}
.sandwich-wrap .board-anchor {
  grid-column: 2;
  grid-row: 2;
}
.sandwich-clue {
  font-family: var(--sans);
  font-weight: 600;
  font-size: 12px;
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}

/* Little killer clues sit just outside the board on whichever edge/corner
   their diagonal starts from. .board itself clips overflow for its own
   decorations, so the overlay is a plain sibling wrapped by .board-anchor
   (a normal, non-grid position:relative box that sizes to .board exactly
   since it's the only content contributing to layout) rather than a grid
   item -- relying on "grid area as containing block for an absolutely
   positioned grid item" did not behave consistently. */
.board-anchor { position: relative; }
.little-killer-overlay {
  position: absolute;
  inset: 0;
  overflow: visible;
  pointer-events: none;
}
/* Reserves real layout space (not just visual overflow room) so clue
   labels sitting outside the board don't collide with neighboring UI --
   .board-wrap centers its content via flex, so padding here just shrinks
   the centering box rather than requiring a separate grid track. */
.board-wrap.has-little-killer { padding: 28px; }
.little-killer-clue {
  position: absolute;
  transform: translate(-50%, -50%);
  display: flex;
  align-items: center;
  gap: 1px;
  font-family: var(--sans);
  font-weight: 700;
  font-size: 11px;
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.little-killer-clue .lk-arrow { font-size: 10px; line-height: 1; }

.board-grid {
  display: grid;
  grid-template-columns: repeat(9, 1fr);
  grid-template-rows: repeat(9, 1fr);
  width: 100%;
  height: 100%;
  /* Dragging across cells to multi-select shouldn't also scroll/pan the page. */
  touch-action: none;
}

/* Killer cages and kropki dots for variant puzzles — a static SVG drawn
   once per puzzle load. Sits above the cell borders and the 3x3 box
   separators (z-index 2), so a dot on a box boundary isn't sliced in half
   by the thick separator, and still below the digit layer. */
.constraint-overlay {
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
}
.constraint-svg { width: 100%; height: 100%; display: block; }
.cage-line {
  stroke: var(--ink-soft);
  stroke-width: 0.2;
  stroke-dasharray: 0.9 0.7;
  fill: none;
}
.cage-sum {
  font-size: 2.3px;
  font-family: var(--sans);
  font-weight: 600;
  fill: var(--ink);
}
.kropki-dot { stroke-width: 0.35; }
.kropki-dot.kropki-white { fill: var(--paper); stroke: var(--ink); }
.kropki-dot.kropki-black { fill: var(--ink); stroke: var(--ink); }
.xv-bg { fill: var(--paper); }
.xv-label {
  font-size: 2.6px;
  font-family: var(--sans);
  font-weight: 700;
  fill: var(--ink);
  text-anchor: middle;
  dominant-baseline: central;
}

/* Thermo/whispers/renban/palindrome/arrow/diagonal/odd-even decorations:
   one board-wide SVG (see renderDecorations in app.js) stacked above the
   cell borders and the 3x3 box separators, but below the digit layer. Each
   line is therefore one continuous stroke that runs across grid lines
   instead of being chopped into per-cell pieces by them. */
.deco-overlay {
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
}
.deco-svg { width: 100%; height: 100%; display: block; overflow: visible; }

/* Pen marks: your own annotations, drawn above the puzzle's constraint art
   but still under the digits. */
.pen-overlay {
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
}
.pen-svg { width: 100%; height: 100%; display: block; overflow: visible; }
.pen-line {
  stroke: var(--pen);
  stroke-width: 0.95;
  stroke-linecap: round;
  fill: none;
}
.pen-dot { fill: var(--pen); }

/* Live total of whatever is selected — the thing you'd otherwise add up in
   your head on a killer, arrow or sandwich clue. */
.selection-sum {
  margin-top: 8px;
  padding: 7px 12px;
  background: var(--panel-bg);
  border-radius: 10px;
  font-size: 12.5px;
  color: var(--ink-soft);
  text-align: center;
  font-variant-numeric: tabular-nums;
}
.selection-sum strong { color: var(--ink); font-weight: 700; }

/* Digits and pencil marks: the top layer, so a line passing through a cell
   never sits over its digit. Purely visual — all input still goes to the
   cells underneath. */
.board-digits {
  position: absolute;
  inset: 0;
  z-index: 4;
  display: grid;
  grid-template-columns: repeat(9, 1fr);
  grid-template-rows: repeat(9, 1fr);
  pointer-events: none;
}
.dcell {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  container-type: size;
  font-family: var(--serif);
  font-variant-numeric: tabular-nums;
}
/* Constraint lines are slightly translucent so the grid underneath still
   reads through them — you can see which cells a line actually covers
   without the line being washed out. Digits sit in a layer above, so they
   stay fully opaque. */
.line-path {
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-opacity: 0.72;
}
.line-thermo { stroke: #CFC7B8; stroke-width: 1.4; }
.thermo-bulb { fill: #CFC7B8; fill-opacity: 0.72; }
.line-whispers { stroke: #6FA85C; stroke-width: 1.9; }
.line-renban { stroke: #B98FD1; stroke-width: 1.9; }
.line-palindrome { stroke: #A9A296; stroke-width: 1.9; }
.line-whispers-dutch { stroke: #D98A3D; stroke-width: 1.9; }
.line-between { stroke: #9AA3AD; stroke-width: 1.5; }
.between-end { fill: none; stroke: #9AA3AD; stroke-width: 0.55; stroke-opacity: 0.72; }
.line-regionsum { stroke: #5B8FD6; stroke-width: 1.9; }
.line-entropic { stroke: #46A9A2; stroke-width: 1.9; }
.line-modular { stroke: #A9764E; stroke-width: 1.9; }
.line-nabner { stroke: #8C8C8C; stroke-width: 1.9; }
.extra-region { fill: rgba(122, 143, 178, 0.16); }
.extra-region-1 { fill: rgba(176, 134, 90, 0.16); }
.extra-region-2 { fill: rgba(126, 168, 120, 0.16); }
.extra-region-3 { fill: rgba(168, 124, 160, 0.16); }
.quad-circle { fill: var(--paper); stroke: var(--ink-soft); stroke-width: 0.3; }
.quad-label {
  font-size: 2.1px;
  font-family: var(--sans);
  font-weight: 600;
  fill: var(--ink);
  text-anchor: middle;
  dominant-baseline: central;
}
/* Arrows are thin enough to hide almost nothing, so they keep more of
   their weight than the wide lines do. */
.arrow-line { stroke: var(--ink-soft); stroke-width: 0.5; stroke-opacity: 0.88; }
.arrow-head { fill: none; stroke: var(--ink-soft); stroke-width: 0.5; stroke-opacity: 0.88; stroke-linecap: round; stroke-linejoin: round; }
.arrow-circle { fill: none; stroke: var(--ink-soft); stroke-width: 0.6; stroke-opacity: 0.88; }
.diagonal-line { stroke: var(--accent-soft); stroke-width: 3.2; stroke-linecap: butt; stroke-opacity: 0.72; }

/* The grid redrawn over the decorations (see renderDecorations). Widths
   are in screen pixels, not board units, so they match the real borders
   underneath at every board size instead of thickening as it grows. */
.deco-grid line { vector-effect: non-scaling-stroke; }
/* Deliberately the strong grid colour at low alpha rather than the plain
   grid colour: over an empty cell it lands within a shade of the border
   below it, so nothing changes, but against a line it is the opposite end
   of the scale and reads as a notch. The plain colour was too close to
   the decorations to show against them at all. */
.deco-grid-cell { stroke: var(--grid-line-strong); stroke-opacity: 0.38; stroke-width: 1; }
.deco-grid-box { stroke: var(--grid-line-strong); stroke-width: 3; }
.oddeven-odd { fill: var(--panel-bg); stroke: var(--grid-line-strong); stroke-width: 0.3; opacity: 0.75; }
.oddeven-even { fill: var(--panel-bg); stroke: var(--grid-line-strong); stroke-width: 0.3; opacity: 0.75; rx: 0.8; }

.cell {
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
  container-type: size;
  font-family: var(--serif);
  font-variant-numeric: tabular-nums;
  border-right: 1px solid var(--grid-line);
  border-bottom: 1px solid var(--grid-line);
  background: var(--cell-shade, var(--paper));
  user-select: none;
  transition: background 120ms ease;
}
/* Outer edge is drawn once, by .board's own border — cells on the last
   row/column shouldn't add a second line on top of it. */
.cell:nth-child(9n) { border-right: none; }
.cell:nth-child(n+73) { border-bottom: none; }

/* 3x3 box separators are drawn as overlay bars centered at exact
   thirds of the board, rather than individual cell borders — the
   9-way grid split can round column/row widths unevenly, which made
   the old border-based separators a hair off between the two axes.
   An overlay sized and centered the same way on both axes renders
   pixel-identical regardless of that rounding. */
.box-line {
  position: absolute;
  background: var(--grid-line-strong);
  pointer-events: none;
  z-index: 2;
}
.box-line-v { top: 0; bottom: 0; width: 3px; transform: translateX(-50%); }
.box-line-h { left: 0; right: 0; height: 3px; transform: translateY(-50%); }

.dcell .value {
  font-size: clamp(16px, 4.2vmin, 30px);
  font-size: 86cqh;
  line-height: 1;
  /* Numerals have no descenders, so the font's line-box leading sits
     unevenly around the glyph and the digit optically lands a little
     above center; nudge it down to compensate. */
  transform: translateY(4.4%);
  color: var(--given);
}
.dcell.user .value { color: var(--user-entry); }
.dcell.given .value { color: var(--ink); }
.dcell.conflict .value { color: var(--error); }

/* Interaction states are semi-transparent overlays, not solid fills,
   so a shaded cell's color still shows through while selected/peered
   instead of being hidden behind an opaque highlight. */
.cell::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: transparent;
}
.cell.peer::before { background: var(--hl-peer); }
/* Cells holding the digit you tapped. This is the highlight you scan the
   grid with, so it gets a ring as well as a wash: the wash alone was the
   same hue and nearly the same weight as the selected cell, which made
   the two hard to tell apart and neither of them quick to spot. The ring
   is what carries it -- the selected cell stays the solid one, with no
   ring, so "where I am" and "where else this digit is" read differently
   at a glance. */
.cell.same-value::before {
  background: var(--hl-same);
  box-shadow: inset 0 0 0 2px var(--accent-dark);
}
/* The selected cell: the strongest wash, and a ring in the ink colour so
   it still announces itself on a cell that is already shaded or fogged. */
.cell.selected::before {
  background: var(--hl-selected);
  box-shadow: inset 0 0 0 2px var(--ink);
}
.cell.conflict::before { background: var(--hl-conflict); }
.cell.hint-target { outline: 2px solid var(--accent-dark); outline-offset: -2px; }

/* Fog of War: a flat, opaque cover over anything not yet revealed. It
   fully overrides any cell shading underneath, since a cell can't have
   been colour-tagged before it was ever seen. */
.cell.fogged {
  background: var(--fog);
  /* The fog is nearly the same tone as the grid lines, which made the
     cell lattice disappear under it — keep the lines readable so you can
     still count cells and see where the boxes start. */
  border-right-color: var(--fog-line);
  border-bottom-color: var(--fog-line);
}
/* Every interaction state is drawn on ::before, so blanking it under fog
   took the selection with it and left you unable to see which cell you
   were about to type into. The selection comes back: it says where you
   are, not anything about the puzzle. The other three stay hidden,
   because each would give away something still under the fog -- a
   same-digit match or a conflict would both say what an uncovered cell
   holds, and a seen-cell shade can betray a clue you haven't met yet. */
.cell.fogged::before { background: transparent; }
.cell.fogged.selected::before {
  background: var(--hl-selected);
  box-shadow: inset 0 0 0 2px var(--ink);
}

.notes-grid {
  position: absolute;
  inset: 3px;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-template-rows: repeat(3, 1fr);
}
.notes-grid span {
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: clamp(9px, 3.5vmin, 15px);
  font-size: 30cqh;
  line-height: 1;
  color: var(--ink-soft);
  font-family: var(--sans);
}
/* Corner marks live on the edges only — the middle of the cell belongs to
   centre marks and to the final digit. */
.corner-tl { grid-area: 1 / 1; }
.corner-tc { grid-area: 1 / 2; }
.corner-tr { grid-area: 1 / 3; }
.corner-ml { grid-area: 2 / 1; }
.corner-mr { grid-area: 2 / 3; }
.corner-bl { grid-area: 3 / 1; }
.corner-bc { grid-area: 3 / 2; }
.corner-br { grid-area: 3 / 3; }

.center-notes {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: clamp(9px, 4vmin, 17px);
  font-size: 32cqh;
  line-height: 1;
  letter-spacing: 0.5px;
  color: var(--ink-soft);
  font-family: var(--sans);
  font-variant-numeric: tabular-nums;
}

/* ---------- Numpad + mode rail ---------- */
/* One control block: app tools and edit tools down the left, the 3x3
   keypad in the middle with the extra tools beneath it, and the input
   modes down the right. Everything is square and sized off one column
   width, so the whole block scales together and never outgrows a short
   viewport. */
.pad {
  display: flex;
  gap: 6px;
  align-items: flex-start;
  justify-content: center;
  width: 100%;
  max-width: 420px;
  margin: 0 auto;
}
/* The tools stood in two columns either side of the keypad, which left the
   keypad half the width of the block. In one row above it they cost a
   single button's height and hand the rest of that width to the digits. */
.pad-toolbar {
  display: grid;
  grid-template-columns: repeat(8, 1fr);
  gap: 6px;
  width: 100%;
  max-width: 420px;
  margin: 0 auto 6px;
}
.pad-modes {
  display: flex;
  flex-direction: column;
  gap: 6px;
  flex: 1 1 0;
  min-width: 0;
}
.pad-center {
  display: flex;
  flex-direction: column;
  gap: 6px;
  flex: 5 1 0;
  min-width: 0;
}
.pad-numbers {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 6px;
}
.pad-extra {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 6px;
}
/* These sit in the keypad's own columns, so left square they would be as
   tall as a number key for no reason. The mode rail is squat for the same
   reason: its height is what the whole block is measured against. */
.pad-extra > .icon-tool,
.pad-extra > .mode-btn { aspect-ratio: 3 / 1; }
.pad-modes .mode-btn { aspect-ratio: 4 / 3; }
/* Wider than tall on purpose. Square keys grow in both directions, and at
   this width three rows of them pushed the bottom of the keypad off the
   screen -- you had to scroll away from the grid to reach 7, 8 and 9. */
.num-btn {
  aspect-ratio: 3 / 2;
  border-radius: 8px;
  border: none;
  background: var(--panel-bg);
  color: var(--ink);
  font-family: var(--serif);
  font-weight: 600;
  font-size: clamp(16px, 4.5vmin, 22px);
}
.num-btn:active { background: var(--accent-soft); }
.num-btn.exhausted { opacity: 0.3; }
/* The digit currently held for digit-first input. */
.num-btn.locked {
  background: var(--accent);
  color: #fff;
  box-shadow: 0 0 0 2px var(--accent-dark) inset;
}
/* How many of this digit are still unplaced, tucked in the corner. */
.num-btn { position: relative; }
.num-left {
  position: absolute;
  top: 3px;
  right: 5px;
  font-size: 10px;
  font-weight: 600;
  font-family: var(--sans);
  color: var(--ink-soft);
  line-height: 1;
}
.num-btn.color-swatch { position: relative; }
.num-btn.color-swatch.active-swatch::after {
  content: "";
  position: absolute;
  inset: 5px;
  border: 2px solid var(--ink);
  border-radius: 6px;
}

.mode-btn {
  aspect-ratio: 1 / 1;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--panel-bg);
  border: none;
  border-radius: 8px;
  color: var(--ink);
}
.mode-btn:active { background: var(--accent-soft); }
.mode-btn.active { background: var(--accent); color: #fff; }

/* ---------- Control rail (icons + numpad) ---------- */
.panel {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.icon-tool {
  aspect-ratio: 1 / 1;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--panel-bg);
  border: none;
  border-radius: 10px;
  color: var(--ink);
}
.icon-tool:active { background: var(--accent-soft); }
.icon-tool.on { background: var(--accent); color: #fff; }
.icon-tool[aria-pressed="true"] { background: var(--accent); color: #fff; }
.icon-tool.disabled { opacity: 0.35; }

/* ---------- Rules panel ---------- */
/* Shares its slot with the killer calculator (see app.js) -- only one of
   the two is ever visible at a time, so this never adds extra height on
   top of whichever one is showing. */
.rules-panel {
  padding: 10px 14px;
  background: var(--panel-bg);
  border-radius: 12px;
  font-size: 14px;
  line-height: 1.5;
  color: var(--ink);
  /* Stacked between the board and the keypad, a long ruleset would take
     whatever it needed and leave the board the remainder. Cap it and let
     it scroll, so the board keeps a usable share on a phone. */
  max-height: 18svh;
  overflow-y: auto;
}
.rules-panel-title {
  font-weight: 700;
  color: var(--accent-dark);
  display: block;
  margin-bottom: 4px;
}
.rules-panel ul {
  margin: 0;
  padding: 0;
  list-style: none;
}
.rules-panel li {
  position: relative;
  padding-left: 14px;
}
.rules-panel li + li { margin-top: 5px; }
.rules-panel-source {
  display: inline-block;
  margin-top: 6px;
  color: var(--accent-dark);
  font-weight: 600;
  text-decoration: underline;
}
.rules-panel li::before {
  content: "";
  position: absolute;
  left: 2px;
  /* Centred on a 14px/1.5 line -- retune with the font size. */
  top: 9px;
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: var(--ink-soft);
}

/* ---------- Killer cage calculator ---------- */
.killer-calc {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 12px 14px;
  background: var(--panel-bg);
  border-radius: 12px;
  /* Capped and scrollable, like the rules. Stacked above the keypad the
     board is the only flexible thing in the column, so anything that
     appears here comes straight out of it -- uncapped, the calculator
     left 77px of board. */
  min-height: 0;
  max-height: 21svh;
  overflow-y: auto;
}
.killer-calc-controls {
  display: flex;
  gap: 10px;
  justify-content: center;
  flex-wrap: wrap;
}
.calc-field {
  display: flex;
  align-items: center;
  gap: 8px;
}
.calc-field-label {
  font-size: 12.5px;
  font-weight: 600;
  color: var(--ink-soft);
}
.stepper {
  display: flex;
  align-items: center;
  gap: 2px;
  background: var(--paper);
  border-radius: 8px;
  padding: 2px;
}
.stepper-btn {
  width: 28px;
  height: 28px;
  border: none;
  border-radius: 6px;
  background: transparent;
  color: var(--ink);
  font-size: 17px;
  font-weight: 700;
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
}
.stepper-btn:active { background: var(--accent-soft); }
.stepper-value {
  min-width: 26px;
  text-align: center;
  font-size: 15px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
/* Digit filter: each digit cycles neutral -> required -> ruled out, so you
   can narrow the combination list to what the grid actually allows. */
.calc-filter {
  display: flex;
  align-items: center;
  gap: 8px;
  justify-content: center;
  flex-wrap: wrap;
}
.calc-filter-digits { display: flex; gap: 4px; flex-wrap: wrap; justify-content: center; }
.calc-digit {
  width: 27px;
  height: 27px;
  border-radius: 7px;
  border: 1px solid var(--paper-line);
  background: var(--paper);
  color: var(--ink);
  font-size: 13px;
  font-weight: 600;
  font-family: var(--sans);
  font-variant-numeric: tabular-nums;
  position: relative;
}
.calc-digit.include {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}
.calc-digit.exclude {
  color: var(--error);
  border-color: var(--error);
  text-decoration: line-through;
  background: var(--error-soft);
}
.calc-filter-clear {
  border: none;
  background: none;
  color: var(--accent-dark);
  font-size: 12px;
  font-weight: 600;
  text-decoration: underline;
  padding: 4px 2px;
}

.killer-calc-hint {
  margin: 0;
  font-size: 11.5px;
  color: var(--ink-soft);
  text-align: center;
}
.killer-calc-combos {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  justify-content: center;
}
.combo-chip {
  border: none;
  border-radius: 8px;
  background: var(--paper);
  color: var(--ink);
  padding: 7px 10px;
  font-size: 14px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.5px;
}
.combo-chip:active { background: var(--accent-soft); }
.combo-chip.crossed-out {
  text-decoration: line-through;
  color: var(--ink-soft);
  opacity: 0.5;
}
.killer-calc-empty {
  font-size: 12.5px;
  color: var(--ink-soft);
  text-align: center;
  padding: 4px 0;
}

.hint-output {
  padding: 10px 14px;
  background: var(--panel-bg);
  border-radius: 12px;
  font-size: 13px;
  line-height: 1.5;
  color: var(--ink);
  display: none;
  /* As the calculator: capped, so a long hint cannot eat the board. */
  min-height: 0;
  max-height: 12svh;
  overflow-y: auto;
}
.hint-output.show { display: block; }
.hint-output .hint-technique {
  font-weight: 700;
  color: var(--accent-dark);
  display: block;
  margin-bottom: 3px;
}
.hint-reveal-link {
  display: inline-block;
  margin-top: 6px;
  background: none;
  border: none;
  padding: 0;
  color: var(--accent-dark);
  font-weight: 600;
  font-size: 13px;
  text-decoration: underline;
}

.chip {
  padding: 12px 4px;
  border-radius: 10px;
  border: none;
  background: var(--panel-bg);
  color: var(--ink-soft);
  font-size: 13px;
  font-weight: 500;
}
.chip.active {
  background: var(--accent);
  color: #fff;
}

.btn {
  width: 100%;
  padding: 13px 14px;
  border-radius: 10px;
  border: none;
  background: var(--panel-bg);
  color: var(--ink);
  font-size: 14.5px;
  font-weight: 600;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
}
.btn.primary {
  background: var(--accent);
  color: #fff;
}
.btn.small { width: auto; padding: 7px 12px; font-size: 13px; }

/* ---------- Main menu ---------- */
.menu-screen {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
}
.menu-scroll {
  display: flex;
  flex-direction: column;
  gap: 18px;
  padding: 8px 18px calc(24px + env(safe-area-inset-bottom));
  max-width: 560px;
  width: 100%;
  margin: 0 auto;
}
.menu-header { padding: 10px 2px 0; }
.menu-header h1 {
  font-family: var(--serif);
  font-size: 26px;
  margin: 0 0 4px;
  color: var(--ink);
}
.menu-tagline {
  margin: 0;
  font-size: 13.5px;
  color: var(--ink-soft);
}

.menu-continue {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  background: var(--accent);
  color: #fff;
  border: none;
  border-radius: 14px;
  padding: 14px 16px;
  text-align: left;
}
.menu-continue-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.menu-continue-label {
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  opacity: 0.85;
}
.menu-continue-title {
  font-size: 15px;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.menu-section-title {
  margin: 0 0 10px 2px;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: var(--ink-soft);
}

.menu-card {
  display: flex;
  gap: 12px;
  width: 100%;
  background: var(--panel-bg);
  border: none;
  border-radius: 14px;
  padding: 14px;
  text-align: left;
  color: var(--ink);
}
.menu-card-icon {
  flex-shrink: 0;
  width: 44px;
  height: 44px;
  border-radius: 10px;
  background: var(--paper);
  color: var(--accent-dark);
  display: flex;
  align-items: center;
  justify-content: center;
}
.menu-card-body { flex: 1; min-width: 0; }
.menu-card-title-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
}
.menu-card-title { font-size: 15.5px; font-weight: 600; }
.menu-card-count {
  flex-shrink: 0;
  font-size: 11.5px;
  font-weight: 600;
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}
.menu-card-count.all-solved { color: var(--accent-dark); }
.menu-card-blurb {
  margin: 3px 0 0;
  font-size: 12.5px;
  color: var(--ink-soft);
  line-height: 1.35;
}

.menu-diff-row {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 6px;
  margin-top: 10px;
}

.menu-card-list { display: flex; flex-direction: column; gap: 10px; }
.menu-variant-card { cursor: pointer; }
.menu-variant-card:active { background: var(--accent-soft); }

.menu-subheader { display: flex; align-items: center; gap: 6px; padding: 10px 0 0; }
.menu-back { flex-shrink: 0; }
.menu-subheader-text h1 {
  font-family: var(--serif);
  font-size: 21px;
  margin: 0 0 2px;
}

.menu-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; }
.menu-header-actions { display: flex; gap: 6px; flex-shrink: 0; }

.menu-search-row { margin-top: 12px; }
.menu-search {
  width: 100%;
  padding: 10px 12px;
  border-radius: 10px;
  border: 1px solid var(--paper-line);
  background: var(--panel-bg);
  color: var(--ink);
  font-size: 14px;
  font-family: var(--sans);
}
.menu-search::placeholder { color: var(--ink-soft); }

/* ---------- Settings & stats ---------- */
.settings-group { margin-top: 18px; }
.settings-title {
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ink-soft);
  margin: 0 0 8px;
}
.settings-row { margin-bottom: 10px; }
.settings-label { display: block; font-size: 14px; margin-bottom: 6px; }
.segmented { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; }
.segmented .chip { padding: 9px 4px; }
.settings-toggle {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 11px 12px;
  margin-bottom: 6px;
  background: var(--panel-bg);
  border-radius: 10px;
  font-size: 13.5px;
  line-height: 1.35;
}
.settings-toggle input { width: 18px; height: 18px; accent-color: var(--accent); flex-shrink: 0; }
.settings-note { font-size: 12px; color: var(--ink-soft); line-height: 1.4; margin: 4px 2px 0; }

.stats-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; margin-bottom: 14px; }
.stat-card { background: var(--panel-bg); border-radius: 12px; padding: 12px 14px; }
.stat-value { font-size: 22px; font-weight: 700; font-variant-numeric: tabular-nums; }
.stat-label { font-size: 11.5px; color: var(--ink-soft); margin-top: 2px; }
.stats-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.stats-table th, .stats-table td { text-align: left; padding: 7px 6px; border-bottom: 1px solid var(--paper-line); }
.stats-table th { font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--ink-soft); font-weight: 600; }
.stats-table td.num { text-align: right; font-variant-numeric: tabular-nums; }

.menu-puzzle-fav {
  flex-shrink: 0;
  background: none;
  border: none;
  padding: 4px;
  color: var(--paper-line);
  line-height: 0;
}
.menu-puzzle-fav.on { color: var(--accent); }
.menu-puzzle-best { font-size: 11px; color: var(--ink-soft); font-variant-numeric: tabular-nums; }

.menu-sort-row {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 6px;
  margin: 12px 0 10px;
}
.menu-sort-row .chip { padding: 8px 4px; }
.menu-puzzle-list { display: flex; flex-direction: column; gap: 8px; }
.menu-puzzle-empty {
  margin: 12px 0;
  font-size: 13px;
  color: var(--ink-soft);
  text-align: center;
}
.menu-puzzle-row {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  background: var(--panel-bg);
  border: none;
  border-radius: 12px;
  padding: 12px 14px;
  text-align: left;
  color: var(--ink);
}
.menu-puzzle-row:active { background: var(--accent-soft); }
.menu-puzzle-check {
  flex-shrink: 0;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  border: 1.5px solid var(--paper-line);
  display: flex;
  align-items: center;
  justify-content: center;
  color: #fff;
}
.menu-puzzle-row.solved .menu-puzzle-check {
  background: var(--accent);
  border-color: var(--accent);
}
.menu-puzzle-info { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.menu-puzzle-title-row { display: flex; align-items: baseline; gap: 6px; flex-wrap: wrap; }
.menu-puzzle-title { font-size: 14.5px; font-weight: 600; }
.menu-puzzle-combo {
  font-size: 10.5px;
  font-weight: 700;
  color: var(--accent-dark);
  background: var(--accent-soft);
  padding: 2px 6px;
  border-radius: 20px;
  white-space: nowrap;
}
.menu-puzzle-stars { display: inline-flex; align-items: center; gap: 1px; }
.menu-puzzle-stars .star { color: var(--paper-line); line-height: 0; }
.menu-puzzle-stars .star.filled { color: var(--accent); }
.menu-puzzle-stars .star svg { display: block; fill: currentColor; }
.menu-puzzle-blurb { font-size: 12px; color: var(--ink-soft); line-height: 1.3; }

/* ---------- Win overlay ---------- */
.overlay {
  position: fixed;
  inset: 0;
  background: rgba(31, 46, 61, 0.55);
  display: none;
  align-items: center;
  justify-content: center;
  z-index: 50;
  padding: 20px;
}
.overlay.show { display: flex; }
.overlay-card {
  background: var(--paper);
  border-radius: 16px;
  padding: 28px 26px;
  text-align: center;
  max-width: 320px;
  width: 100%;
}
.overlay-eyebrow {
  color: var(--accent-dark);
  font-size: 13px;
  margin: 0 0 6px;
  letter-spacing: 0.4px;
}
.overlay-card h2 {
  font-family: var(--serif);
  font-size: 40px;
  margin: 0 0 4px;
}
.overlay-card p#winMistakes {
  color: var(--ink-soft);
  margin: 0 0 18px;
  font-size: 14px;
}

/* ---------- Install banner ---------- */
.install-banner {
  position: fixed;
  left: 14px;
  right: 14px;
  bottom: calc(14px + env(safe-area-inset-bottom));
  background: var(--ink);
  color: var(--paper);
  border-radius: 12px;
  padding: 12px 12px 12px 16px;
  display: none;
  align-items: center;
  gap: 10px;
  font-size: 13px;
  z-index: 40;
  box-shadow: 0 8px 24px rgba(0,0,0,0.25);
}
.install-banner.show { display: flex; }
.install-banner span { flex: 1; line-height: 1.4; }
.install-banner .btn.small { background: var(--accent); border-color: var(--accent); color: #fff; }
.install-banner .icon-btn.small { color: var(--paper); padding: 4px; }

/* ============================================================
   Wide layout — Galaxy Z Fold unfolded (and tablets/desktop)
   Roughly square-to-landscape wide screens.
   ============================================================ */
@media (min-width: 720px) and (min-aspect-ratio: 1/1) {
  .layout {
    flex-direction: row;
    align-items: stretch;
  }
  .puzzle-col { flex: 1.6; }
  .board { width: min(100cqw, 100cqh, 760px); }

  .panel {
    flex: 1;
    /* Only bites on screens wider than a folding phone, where the column
       share alone would leave the panel much wider than it needs; unfolded
       it is the 1 : 1.6 split against the board that decides, and the
       board should keep the larger half. */
    max-width: 400px;
    gap: 20px;
    overflow: hidden;
  }
  .rules-panel {
    flex: 0 1 auto;
    min-height: 0;
    overflow-y: auto;
    /* Beside the board there is real height to use, so the portrait cap
       would only make the panel needlessly short. Flex decides here. */
    max-height: none;
  }
  /* The controls sit at the foot of the column and stay there. Everything
     above them changes height while you play -- the rules card, the
     calculator that replaces it, the hint line, the selection sum -- and
     with the column centred the keypad moved every time one of them did;
     opening the calculator shifted it most. The gap goes above the tools,
     so what grows eats the empty space instead of pushing the keypad. */
  .pad-toolbar { margin-top: auto; }
  .input-row { max-width: 320px; }
  .mobile-numpad {
    max-width: 230px;
    gap: 8px;
  }
  .num-btn { font-size: clamp(20px, 3vw, 28px); border-radius: 12px; }
  .mode-btn svg { width: 18px; height: 18px; }
}

/* Very narrow cover-screen fallback */
@media (max-width: 380px) {
  .timer { font-size: 15px; }
  .puzzle-name { font-size: 16px; }
  .puzzle-tags svg { width: 13px; height: 13px; }
  .num-btn { font-size: 15px; }
}

@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; animation: none !important; }
}
