/* mobile.css — the mobile-first PuzzleVerse shell. Same "Newsprint" identity
   as the desktop (tokens.css), laid out for thumbs: full-height column,
   bottom action bar in reach, bottom sheets instead of dropdowns/menus,
   44px+ targets, 16px inputs (no iOS focus zoom), safe-area aware.

   tokens.css is NOT @imported here: an @import is only discovered after this
   file has been fetched and parsed, which delays every colour token and every
   @font-face by a round trip and makes the fonts swap in visibly late. Both
   shells link it themselves, ahead of this file. */

/* ==========================================================================
   The mobile shell wears the shared "Newsprint" identity straight from
   tokens.css: warm paper, dark ink, a rust accent, sharp 3px corners and
   Newsreader for the headings. (It used to override all of that with a flat
   grey/green "modern mobile game" palette — that override is gone.)

   The ONLY token set here is the one tokens.css has no reason to know about:
   the engine's reference background.
   ========================================================================== */
:root {
  /* The sheet: every board is the same printed paper the paper puzzles
     (Maze/Crossword) already draw on — `--canvas-paper` — so an engine game
     and a printed sheet are visibly the same stock. It is always the LIGHT
     reference: the engine derives the dark board by lightness-flipping it, so
     it must NOT be overridden under [data-theme="dark"]. Kept as a literal so
     the JS side reads a colour, not a var() reference. */
  --engine-ref-bg: #faf7f0;
}

* { box-sizing: border-box; }
html, body { height: 100%; margin: 0; }
body {
  font-family: var(--serif);
  color: var(--ink);
  background: var(--paper-bg);
  font-size: 15px;
  -webkit-font-smoothing: antialiased;
  touch-action: manipulation;          /* no double-tap zoom on chrome */
  overscroll-behavior: none;
}
button { font-family: var(--sans); color: inherit; }
a { color: inherit; text-decoration: none; }
::selection { background: var(--accent-soft); }

#app {
  height: 100vh;
  height: 100dvh;                      /* tracks mobile browser chrome */
  display: flex; flex-direction: column;
  overflow: hidden;
}
/* The rotate gate, now for PHONES IN LANDSCAPE ONLY.
   It used to fire on any touch device in landscape (`pointer: coarse`), which
   also blanked every tablet — an iPad in landscape is a perfectly good place to
   play, and it now gets the wide two-column layout at the bottom of this file.
   The `max-height` is what distinguishes the two: a phone on its side is short
   (a 926x428 iPhone), a tablet is not (1024x768). Below that height there is no
   room for a board AND its controls, so the gate stays. */
.rotate-gate { display: none; }
@media (orientation: landscape) and (pointer: coarse) and (max-height: 500px) {
  .rotate-gate {
    position: fixed; inset: 0; z-index: 200;
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: 14px; padding: 24px; text-align: center;
    background: var(--paper-bg); color: var(--ink);
    font-family: var(--sans); font-size: 15px;
  }
  .rotate-gate .rotate-ico { font-size: 44px; color: var(--accent); }
  #app { display: none; }
}

/* When the OS keyboard is up, fit the visual viewport instead. */
body.kb-open #app { height: var(--vvh, 100dvh); }
body.kb-open .bottombar, body.kb-open .keyrow { display: none; }

/* ---- Top bar (shared: home header rows, play top bar) ------------------- */
.topbar {
  flex: 0 0 auto;
  display: flex; align-items: center; gap: 4px;
  padding: calc(6px + env(safe-area-inset-top)) 8px 6px;
  background: var(--surface);
  border-bottom: 1px solid var(--rule-strong);
}
.topbar .tb-title {
  flex: 0 1 auto; min-width: 0;
  font-family: var(--serif); font-size: 18px; font-weight: 700;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  /* The serif's cap-height sits high in its line box, so centring the box
     leaves the word riding above the icons beside it. Nudge the glyphs, not
     the box, so the layout is untouched. */
  transform: translateY(4px);
}
/* Nav actions fused into the header: icon-only cells centred between the
   title and the help button by the flanking flex spacers. */
.topbar .tb-spacer { flex: 1 1 auto; }
.hdr-actions {
  flex: 0 0 auto;
  display: flex; align-items: center; gap: 0;
}
.hb-cell {
  flex: 0 0 auto;
  min-width: 40px; min-height: 44px;
  display: inline-flex; align-items: center; justify-content: center;
  background: none; border: none; border-radius: var(--radius);
  cursor: pointer; color: var(--ink);
  -webkit-tap-highlight-color: transparent;
}
.hb-cell .ico { font-size: 20px; line-height: 1; }
.hb-cell:disabled { opacity: 0.35; cursor: default; }
.hb-cell:not(:disabled):active { background: var(--surface-2); }
.hb-cell.primary .ico { color: var(--accent); }
.hb-cell.active { background: var(--accent-soft); }
.hb-cell.active .ico { color: var(--accent); }
.icon-btn {
  flex: 0 0 auto;
  min-width: 44px; min-height: 44px;
  display: inline-flex; align-items: center; justify-content: center;
  background: none; border: none; border-radius: var(--radius);
  font-size: 20px; cursor: pointer; color: var(--ink);
  -webkit-tap-highlight-color: transparent;
}
.icon-btn:active { background: var(--surface-2); }
/* In the fused play header, back/help are tighter to leave the title room. */
.topbar .icon-btn { min-width: 40px; }
/* Eight cells (back · undo redo restart new settings more · help) outgrow a
   small phone. Narrow the cells before the title starts losing letters; the
   44px touch height is what matters and it is untouched. */
@media (max-width: 400px) {
  .topbar .icon-btn, .topbar .hb-cell { min-width: 36px; }
}

/* ---- Home ---------------------------------------------------------------- */
.home { display: flex; flex-direction: column; height: 100%; min-height: 0; }
.home-head {
  flex: 0 0 auto;
  background: var(--surface);
  border-bottom: 1px solid var(--rule-strong);
  padding: calc(8px + env(safe-area-inset-top)) 12px 10px;
}
.brand-row { display: flex; align-items: center; gap: 8px; min-height: 40px; }
.brand-row .wordmark {
  font-family: var(--serif); font-weight: 700; font-size: 20px; letter-spacing: 0.01em;
  transform: translateY(2px);   /* same optical centring as .tb-title */
}
.brand-row .spacer { flex: 1; }
.home-search {
  width: 100%; margin-top: 8px;
  font-family: var(--sans); font-size: 16px; color: var(--ink);
  background: var(--paper-bg); border: 1px solid var(--rule-strong);
  border-radius: var(--radius); padding: 10px 12px;
  min-height: 44px; -webkit-appearance: none; appearance: none;
}
.home-search:focus { outline: none; border-color: var(--accent); }
.home-list {
  flex: 1 1 auto; min-height: 0;
  overflow-y: auto; -webkit-overflow-scrolling: touch;
  padding-bottom: env(safe-area-inset-bottom);
}
.cat-label {
  font-family: var(--sans); font-size: 11.5px; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.09em; color: var(--muted);
  padding: 16px 16px 6px;
}
/* Collapsible category section (accordion) */
.cat-header {
  display: flex; align-items: center; gap: 9px; width: 100%;
  min-height: 50px; padding: 12px 16px;
  background: none; border: none; border-top: 1px solid var(--rule);
  text-align: left; cursor: pointer; -webkit-tap-highlight-color: transparent;
  font-family: var(--sans); font-size: 11.5px; font-weight: 700;
  letter-spacing: 0.09em; text-transform: uppercase; color: var(--muted);
}
.cat-section:first-child .cat-header { border-top: none; }
.cat-header:active { background: var(--surface-2); }
.cat-chev { font-size: 11px; line-height: 1; }
.cat-section.collapsed .cat-chev { transform: rotate(-90deg); }
.cat-h-label { flex: 1 1 auto; }
.cat-h-count { color: var(--muted); font-weight: 400; font-variant-numeric: tabular-nums; }
.cat-section.collapsed .cat-rows { display: none; }
.row {
  display: flex; align-items: center; gap: 12px;
  width: 100%; min-height: 56px; padding: 8px 16px;
  background: none; border: none; border-bottom: 1px solid var(--rule);
  text-align: left; cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.row:active { background: var(--surface-2); }
.row .swatch {
  flex: 0 0 auto; width: 13px; height: 13px;
  border-radius: 3px; background: var(--sw, var(--sw-other));
}
/* Name over blurb. A flex column rather than a <br>, so the blurb is a block
   that can wrap — which is what the wide-screen cards need. */
.row .row-text { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }
.row .row-name { font-family: var(--serif); font-size: 16.5px; font-weight: 700; }
.row .row-blurb {
  font-family: var(--sans); font-size: 13px; color: var(--muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  margin-top: 1px;
}
.home-empty { padding: 32px 16px; color: var(--muted); font-family: var(--sans); }

/* ---- Credits (#/credits) --------------------------------------------------- */
.credits-m { display: flex; flex-direction: column; height: 100%; min-height: 0; }
.credits-body {
  flex: 1 1 auto; min-height: 0; overflow-y: auto;
  padding: 8px 20px calc(28px + env(safe-area-inset-bottom));
  -webkit-overflow-scrolling: touch;
}
.credits-sec { max-width: 62ch; margin: 0 auto 26px; }
.credits-sec h2 {
  margin: 20px 0 8px;
  font-family: var(--serif); font-size: 17px; font-weight: 700; color: var(--ink);
}
.credits-sec p {
  margin: 0 0 10px;
  font-family: var(--sans); font-size: 14px; line-height: 1.6; color: var(--muted);
}
.credits-sec strong { color: var(--ink); font-weight: 600; }
.credits-sec a {
  color: var(--accent); text-decoration: underline; text-underline-offset: 2px;
}
.credits-sec a:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 2px;
}

/* Engine/catalog failed to load (app.js mountError). */
.load-error {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 12px; height: 100%; padding: 32px 24px; text-align: center;
}
.load-error h2 {
  margin: 0; font-family: var(--serif); font-size: 20px; font-weight: 700; color: var(--ink);
}
.load-error p {
  margin: 0; max-width: 34ch; font-family: var(--sans); font-size: 14px;
  line-height: 1.5; color: var(--muted);
}
.load-error .err-btn {
  min-height: 44px; padding: 0 20px; border: 1px solid var(--rule-strong);
  border-radius: 8px; background: var(--surface-2); color: var(--ink);
  font-family: var(--sans); font-size: 15px; cursor: pointer;
}
.load-error .err-btn:first-of-type { border-color: var(--accent); color: var(--accent); }

/* -- Boot skeleton (index.html) --
   The home chrome, painted from static markup while app.js's module graph and
   the catalog manifest are still in flight; mountHome() replaces it. It reuses
   .home/.home-head/.brand-row so the header it paints IS the real header — only
   the rows are placeholders, and they are inert grey bars rather than fake text,
   which would itself be a flash. On a deep link to a play screen there is no
   home to skeleton, so prepaint.js marks the boot and it stays hidden. */
[data-boot="play"] #boot-skeleton { display: none; }
.skel-theme, .skel-search, .skel-row { pointer-events: none; }
/* The same glyph mountHome() picks, so the button doesn't pop in on mount. */
.skel-theme::before { content: "☾"; }
[data-theme="dark"] .skel-theme::before { content: "☀"; }
.skel-search { min-height: 44px; }
.skel-row {
  height: 56px; border-bottom: 1px solid var(--rule);
  background:
    linear-gradient(var(--surface-2), var(--surface-2)) 16px 21px / 13px 13px no-repeat,
    linear-gradient(var(--surface-2), var(--surface-2)) 41px 16px / 38% 11px no-repeat,
    linear-gradient(var(--rule), var(--rule)) 41px 33px / 62% 9px no-repeat;
  opacity: 0.7;
}
.home-foot {
  padding: 14px 16px calc(14px + env(safe-area-inset-bottom));
  font-family: var(--sans); font-size: 13px; color: var(--muted);
  text-align: center;
}
/* A quiet sign-off under the puzzle list: who the puzzles are by, and a link to
   the full attribution (#/credits). */
.home-foot { font-size: 12px; line-height: 1.6; border-top: 1px solid var(--rule); margin-top: 8px; }
.home-foot p { margin: 0; }
.home-foot a { text-decoration: underline; text-underline-offset: 2px; color: inherit; }
.home-foot a:hover { color: var(--ink); }
.home-foot a:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 2px; }

/* ---- Play screens ---------------------------------------------------------- */
.play-m { display: flex; flex-direction: column; height: 100%; min-height: 0; }

/* The control widgets (piece tray / number pad / key row / mode bar) live in a
   rail so a wide screen can put them BESIDE the board. On a phone the rail is
   not a box at all: `display: contents` dissolves it, and .play-m lays its
   children out exactly as it did when they were direct children. The phone is
   untouched; only the wide block at the bottom of this file uses the wrapper. */
.control-rail { display: contents; }
.stage-m {
  flex: 1 1 auto; min-height: 0; position: relative;
  display: flex; align-items: center; justify-content: center;
  padding: 8px; background: var(--paper-bg);
}
.engine-host {
  width: 100%; height: 100%;
  display: flex; align-items: center; justify-content: center;
}
.engine-canvas,
.engine-board {
  outline: none; touch-action: none;
  box-shadow: 0 0 0 1px var(--rule-strong);
  border-radius: var(--radius-sm);
}

/* DOM-rendered board: an <svg> whose user units ARE engine pixels (see
   engine.js attachDom). Its width/height are set from C's board size, so it
   must not be stretched by the flex host. */
.engine-board {
  display: block; flex: 0 0 auto;
  background: var(--canvas-paper);
  -webkit-user-select: none; user-select: none;
  -webkit-touch-callout: none;
}

.status-m {
  flex: 0 0 auto;
  min-height: 26px; padding: 4px 12px calc(4px + env(safe-area-inset-bottom, 0px));
  display: flex; align-items: center; justify-content: center; gap: 8px;
  font-family: var(--sans); font-size: 12.5px; color: var(--ink-soft);
  border-top: 1px solid var(--rule);
  background: var(--surface);
}
.status-m:empty { display: none; }
.status-m .err { color: var(--accent-press); }

/* ---- end of game -------------------------------------------------------
   Ending a game flashes the board twice (parity with Untangle's completion
   flash), latches a badge — green "Solved", red "Game Over!" — and locks the
   board: `.won` kills pointer input for the whole stage, which is what locks
   the paper puzzles; the engine games are also locked in C (frontend.c
   board_locked, for a lost game as well as a solved one), so keys and toolbar
   values stop too. Undo/Redo/Restart/New live in the header, outside the
   stage. */
.stage-m.won { pointer-events: none; }
/* The paper board scrolls, so lock the canvas rather than the whole area. */
.canvas-area.won canvas { pointer-events: none; }
.win-flash {
  position: absolute; inset: 0; pointer-events: none;
  background: var(--win); opacity: 0; border-radius: var(--radius);
}
.win-flash.go { animation: win-flash 0.62s ease-out both; }
@keyframes win-flash {
  0%, 100% { opacity: 0; }
  10% { opacity: 0.28; }
  30% { opacity: 0; }
  50% { opacity: 0.28; }
  70% { opacity: 0; }
}
.win-badge {
  position: absolute; left: 50%; top: 12px; z-index: 3;
  transform: translateX(-50%) translateY(-6px) scale(0.94);
  padding: 5px 14px; border-radius: 999px;
  font-family: var(--sans); font-weight: 700; font-size: 13px;
  color: #fff; background: var(--win);
  opacity: 0; pointer-events: none;
  transition: opacity 0.2s ease 0.45s, transform 0.2s ease 0.45s;
}
.win-badge.show { opacity: 1; transform: translateX(-50%) translateY(0) scale(1); }
/* Losing wears the same badge and flash in red. */
.win-flash.lost { background: var(--lose); }
.win-badge.lost { background: var(--lose); }
@media (prefers-reduced-motion: reduce) {
  .win-flash.go { animation: none; }
  .win-badge { transition: none; }
}

/* ---- Control strips: key row, number pad, piece tray ----------------------
   All three wear the mode-bar's look (see .bottombar / .mode-bar below): no
   button chrome, just edge-to-edge square segments. The 1px gaps let the
   strip's own background show through as hairline dividers, which survives
   wrapping (a digit row of 9+ keys) without per-cell border bookkeeping; the
   safe-area inset is a surface-coloured bottom border so it stays paper, not
   rule. */
.keyrow, .number-pad, .piece-tray {
  flex: 0 0 auto;
  background: var(--rule);
  border-top: 1px solid var(--rule-strong);
  border-bottom: env(safe-area-inset-bottom, 0px) solid var(--surface);
  display: flex; flex-direction: column; gap: 1px;
}
.keyrow { flex-direction: row; flex-wrap: wrap; }
body.kb-open .number-pad { display: none; }
/* Belt and braces: a row with no keys takes no space (see keypad.js). */
.keyrow:empty { display: none; }
.np-digits, .np-actions, .pt-pieces, .pt-actions {
  display: flex; flex-wrap: wrap; gap: 1px;
}

/* One segment, in every strip. */
.keyrow button, .number-pad .np-key, .number-pad .np-erase,
.number-pad .np-notes, .piece-tray .piece, .piece-tray .pencil-chip {
  flex: 1 1 8%; min-width: 34px; min-height: 56px;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 2px; padding: 6px 4px;
  font-family: var(--sans); font-size: 19px; font-weight: 600;
  color: var(--ink); background: var(--surface);
  border: none; border-radius: 0;
  cursor: pointer; -webkit-tap-highlight-color: transparent;
  touch-action: manipulation; white-space: nowrap;
  user-select: none; -webkit-user-select: none;
}
.keyrow button:active, .number-pad .np-key:active,
.number-pad .np-erase:active, .piece-tray .piece:active {
  background: var(--surface-2);
}
/* The selected tool — an armed digit, the active piece, Notes/Pencil on — reads
   the same accent-soft way in every strip, matching the mode bar. */
.number-pad .np-key.armed, .number-pad .np-erase.armed,
.number-pad .np-notes.on, .piece-tray .piece.active, .pencil-chip.on {
  background: var(--accent-soft); color: var(--accent);
}
.piece-tray .piece.active .p-ico, .piece-tray .piece.active .p-lbl {
  color: var(--accent);
}
/* Actions (Clear / Notes / Pencil) are utilities: smaller, softer text. */
.number-pad .np-erase, .number-pad .np-notes, .piece-tray .pencil-chip {
  font-size: 13.5px; color: var(--ink-soft);
}
/* The standardized Erase tool sits with the pieces but reads as a utility. */
.piece-tray .pt-erase .p-ico { color: var(--ink-soft); font-size: 20px; }
.piece-tray .p-ico { font-size: 22px; line-height: 1; }
.piece-tray .p-art { width: 34px; height: 34px; display: block; }
.piece-tray .p-lbl {
  font-family: var(--sans); font-size: 10.5px; font-weight: 600; color: var(--ink-soft);
}

/* ---- Row-drag (sixteen/netslide) ------------------------------------------ */

.rowdrag-arrow {
  position: absolute; left: 50%; top: 50%; z-index: 6;
  transform: translate(-50%, -50%);
  pointer-events: none;
  font-family: var(--sans); font-size: 30px; font-weight: 700;
  color: var(--accent);
  background: var(--surface);
  border: 1px solid var(--rule-strong); border-radius: 12px;
  padding: 6px 16px;
}
.rowdrag-arrow.blocked { color: var(--muted); }

.coach-toast {
  position: absolute; left: 50%; top: 14px; z-index: 7;
  transform: translateX(-50%);
  max-width: 86%;
  pointer-events: none;
  font-family: var(--sans); font-size: 13.5px; color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--accent); border-radius: 12px;
  padding: 9px 14px;
}
.coach-toast.bye { opacity: 0; }

/* Bottom action bar — five equal cells in thumb reach */
.bottombar {
  flex: 0 0 auto;
  display: flex;
  background: var(--surface);
  border-top: 1px solid var(--rule-strong);
  padding-bottom: env(safe-area-inset-bottom);
}
.bb-cell {
  flex: 1 1 0; min-height: 56px;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 2px; background: none; border: none; cursor: pointer;
  color: var(--ink); -webkit-tap-highlight-color: transparent;
  padding: 6px 2px;
}
.bb-cell .ico { font-size: 19px; line-height: 1; }
.bb-cell .lbl {
  font-family: var(--sans); font-size: 10.5px; font-weight: 600;
  color: var(--ink-soft); white-space: nowrap;
}
.bb-cell:disabled { opacity: 0.35; cursor: default; }
.bb-cell:not(:disabled):active { background: var(--surface-2); }
.bb-cell.primary .ico, .bb-cell.primary .lbl { color: var(--accent); }
.bb-cell.active { background: var(--accent-soft); }
.bb-cell.active .ico, .bb-cell.active .lbl { color: var(--accent); }

/* Segmented mode bar (placing games): the .bottombar shell, with a divider so
   the segments read as a group. */
.mode-bar .bb-cell + .bb-cell { border-left: 1px solid var(--rule); }

/* ---- Bottom sheet ----------------------------------------------------------- */
.sheet-backdrop {
  position: fixed; inset: 0; z-index: 90;
  background: rgba(0, 0, 0, 0.45);
}
.sheet {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 91;
  max-height: 78dvh;
  /* Every labelled control in a sheet — config rows, maker fields, dropdown
     triggers — sits in this one column, so their edges line up. It shrinks
     on a narrow phone rather than pushing the row wider than the screen. */
  --field-col: clamp(150px, 46%, 210px);
  display: flex; flex-direction: column;
  background: var(--surface); color: var(--ink);
  border-top: 1px solid var(--rule-strong);
  border-radius: 14px 14px 0 0;
  padding-bottom: env(safe-area-inset-bottom);
}
.sheet-grab {
  flex: 0 0 auto; width: 38px; height: 4px; border-radius: 2px;
  background: var(--rule-strong); margin: 8px auto 4px;
}
.sheet-title {
  flex: 0 0 auto;
  font-family: var(--serif); font-size: 17px; font-weight: 700;
  padding: 6px 18px 8px;
}
.sheet-body {
  flex: 1 1 auto; min-height: 0;
  overflow-y: auto; -webkit-overflow-scrolling: touch;
  padding: 0 10px 12px;
}

/* Sheet row primitives */
.sh-action {
  display: flex; align-items: center; gap: 10px;
  width: 100%; min-height: 48px; padding: 6px 10px;
  background: none; border: none; border-radius: var(--radius);
  font-family: var(--sans); font-size: 15.5px; color: var(--ink);
  text-align: left; cursor: pointer; -webkit-tap-highlight-color: transparent;
}
.sh-action:active { background: var(--surface-2); }
.sh-action:disabled { opacity: 0.4; }
.sh-action.primary { color: var(--accent); font-weight: 600; }
.sh-radio { composes: none; }
.sh-radio .mark {
  flex: 0 0 auto; width: 18px; height: 18px; border-radius: 50%;
  border: 2px solid var(--rule-strong);
}
.sh-radio[aria-checked="true"] .mark {
  border-color: var(--accent);
  background: radial-gradient(circle, var(--accent) 0 40%, transparent 45%);
}
.sh-toggle input { accent-color: var(--accent); width: 20px; height: 20px; }
.sh-sep { height: 1px; background: var(--rule); margin: 6px 10px; }
.sh-note {
  font-family: var(--sans); font-size: 13.5px; line-height: 1.55; color: var(--ink-soft);
  padding: 4px 10px 8px;
}
.sh-note .seed { user-select: all; -webkit-user-select: all; }
.sh-head {
  font-family: var(--sans); font-size: 11px; font-weight: 700;
  letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted);
  padding: 12px 10px 2px;
}

/* Help sheet: side-by-side Controls | Rules */
.help-grid {
  display: grid; grid-template-columns: 1fr 1fr; gap: 0 14px;
  align-items: start; padding: 0 0 4px;
}
@media (max-width: 360px) { .help-grid { grid-template-columns: 1fr; } }
.help-col { min-width: 0; }
.help-tip { font-style: italic; }
.ctl-list {
  list-style: none; margin: 0; padding: 4px 10px 8px;
  font-family: var(--sans); font-size: 13px; line-height: 1.5; color: var(--ink-soft);
}
.ctl-list li { margin-bottom: 10px; }
.kbd {
  display: inline-block;
  font-family: var(--sans); font-size: 11.5px; font-weight: 700;
  padding: 1px 7px; margin-right: 2px;
  background: var(--paper-bg); color: var(--ink);
  border: 1px solid var(--rule-strong); border-bottom-width: 2px;
  border-radius: 6px;
}

/* Config form inside a sheet (shared markup from js/config-form.js) */
.sheet .cfg-row {
  display: grid; grid-template-columns: minmax(0, 1fr) var(--field-col);
  align-items: center;
  gap: 12px; min-height: 48px; padding: 2px 10px;
  font-family: var(--sans); font-size: 15px; color: var(--ink-soft);
}
/* A checkbox row is a checkbox + its label text, not a labelled control: it
   opts out of the two-column grid and hugs the left edge like .maker-check. */
.sheet .cfg-row:has(.cfg-check) {
  display: flex; justify-content: flex-start; gap: 10px;
}
.sheet .cfg-input {
  font-family: var(--sans); font-size: 16px; color: var(--ink);
  background: var(--paper-bg); border: 1px solid var(--rule-strong);
  border-radius: var(--radius-sm); padding: 9px 10px;
  width: 100%; min-width: 0; min-height: 44px;
}
.sheet .cfg-input:focus { outline: none; border-color: var(--accent); }
.sheet .cfg-check { accent-color: var(--accent); width: 20px; height: 20px; flex: 0 0 auto; }
.sheet .cfg-row.seed-row { border-top: 1px dashed var(--rule); margin-top: 6px; padding-top: 8px; }
.sheet .cfg-error {
  color: var(--accent-press); font-family: var(--sans);
  font-size: 13.5px; padding: 6px 10px;
}
.sheet-actions {
  flex: 0 0 auto;
  display: flex; gap: 10px; justify-content: flex-end;
  padding: 10px 14px 12px;
  border-top: 1px solid var(--rule);
}
.sheet-actions .btn-ok:only-child { flex: 1 1 auto; } /* lone Create spans */
.btn-ok, .btn-cancel {
  font-family: var(--sans); font-size: 15px; font-weight: 600;
  min-height: 44px; padding: 0 20px; border-radius: var(--radius);
  cursor: pointer; -webkit-tap-highlight-color: transparent;
}
.btn-ok { background: var(--accent); color: #fff; border: 1px solid var(--accent); }
.btn-ok:active { background: var(--accent-press); }
.btn-cancel { background: var(--paper-bg); color: var(--ink); border: 1px solid var(--rule-strong); }
.btn-cancel:active { background: var(--surface-2); }

/* ---- Themed dropdowns + menus (js/dropdown.js) ---------------------------
   The trigger is styled as a field, so a "choices" row in a config sheet reads
   as a sibling of the text inputs beside it. The panel is portaled to <body>
   and positioned (fixed) by the component, so these rules only give it its
   surface — never its coordinates. */
.dd { position: relative; display: inline-flex; }
/* In a form row the trigger IS the control: it fills --field-col like the
   inputs beside it instead of carrying a width of its own. */
.sheet .maker-field > .dd, .sheet .cfg-row > .dd { width: 100%; }
.sheet .dd-btn { min-width: 0; }
.dd-btn {
  display: inline-flex; align-items: center; justify-content: space-between; gap: 8px;
  width: 100%; min-width: 170px; min-height: 44px; padding: 9px 10px;
  font-family: var(--sans); font-size: 16px; color: var(--ink); text-align: left;
  background: var(--paper-bg);
  border: 1px solid var(--rule-strong); border-radius: var(--radius-sm);
  cursor: pointer; -webkit-tap-highlight-color: transparent;
}
.dd-btn:active { background: var(--surface-2); }
.dd-btn[aria-expanded="true"] { border-color: var(--accent); }
.dd-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dd-caret { flex: 0 0 auto; font-size: 11px; color: var(--muted); }

.dd-pop {
  display: none;
  position: fixed; z-index: 130;          /* over the sheet (91) that hosts it */
  min-width: 170px; max-width: calc(100vw - 16px);
  overflow-y: auto; -webkit-overflow-scrolling: touch;
  padding: 4px;
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--rule-strong); border-radius: var(--radius);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.28);
}
.dd-pop.open { display: block; }
.dd-opt {
  display: flex; align-items: center; gap: 8px;
  min-height: 44px; padding: 8px 10px;
  font-family: var(--sans); font-size: 15px; color: var(--ink);
  border-radius: var(--radius-sm);
  cursor: pointer; -webkit-tap-highlight-color: transparent;
}
.dd-opt:active { background: var(--surface-2); }
.dd-opt:focus-visible { outline: none; background: var(--surface-2); }
/* The chosen option carries the collection's selection wash, like a picked cell. */
.dd-opt[aria-selected="true"] {
  background: var(--accent-soft); color: var(--accent); font-weight: 600;
}
.dd-opt[aria-selected="true"]::after { content: "✓"; margin-left: auto; }
.dd-opt.primary { color: var(--accent); font-weight: 600; }
.dd-check[aria-checked="true"]::after { content: "✓"; margin-left: auto; color: var(--accent); }
.dd-sep { height: 1px; background: var(--rule); margin: 4px 6px; }

/* ---- Paper puzzles ---------------------------------------------------------- */
.canvas-area {
  flex: 1 1 auto; min-height: 0; position: relative;
  overflow: auto; -webkit-overflow-scrolling: touch;
  background: var(--paper-bg);
  display: flex; align-items: safe center; justify-content: safe center;
  padding: 10px;
}
#board, .ws-grid, .canvas-area canvas {
  background: var(--canvas-paper);
  border: 1px solid var(--rule-strong); border-radius: var(--radius);
  outline: none; touch-action: none;
  max-width: none;
  margin: auto;
}
/* Maker panel fields living inside the Setup sheet: label left, control in the
   shared --field-col on the right, so a panel of mixed fields reads as one
   two-column form rather than a stack of independently sized rows. */
.sheet .maker-field {
  display: grid; grid-template-columns: minmax(0, 1fr) var(--field-col);
  align-items: center; gap: 12px;
  min-height: 48px; padding: 2px 10px;
  font-family: var(--sans); font-size: 15px; color: var(--ink-soft);
}
.sheet .maker-check {
  display: flex; align-items: center; justify-content: flex-start; gap: 10px;
  min-height: 48px; padding: 2px 10px;
  font-family: var(--sans); font-size: 15px; color: var(--ink-soft);
}
.sheet .maker-check input { accent-color: var(--accent); width: 20px; height: 20px; }
.sheet .maker-field input, .sheet .maker-field textarea {
  font-family: var(--sans); font-size: 16px; color: var(--ink);
  background: var(--paper-bg); border: 1px solid var(--rule-strong);
  border-radius: var(--radius-sm); padding: 9px 10px;
  min-height: 44px; width: 100%; min-width: 0;
}
.sheet .maker-field textarea { min-height: 90px; }
.sheet .maker-field input:focus, .sheet .maker-field textarea:focus {
  outline: none; border-color: var(--accent);
}
/* A row of side-by-side controls. Fields in it share the width equally
   (flex-basis 0, not auto — otherwise "Height" being a wider word than "Width"
   would steal from its own input) and stack their label above the box, which is
   the only way a pair of number boxes comes out exactly the same size. Rows
   holding a lone button or a couple of checkboxes keep their natural widths. */
.sheet .maker-row { display: flex; align-items: flex-end; gap: 10px; padding: 2px 10px; }
.sheet .maker-row .maker-field {
  flex: 1 1 0; min-width: 0;
  grid-template-columns: minmax(0, 1fr);
  align-items: stretch; gap: 4px; min-height: 0; padding: 0;
}
.sheet .maker-row .maker-field > span { font-size: 13px; }
.sheet .maker-seed-current, .sheet .maker-help, .sheet .maker-hint {
  font-family: var(--sans); font-size: 13px; color: var(--muted);
  padding: 4px 10px 8px;
}
.sheet .maker-seed-current { font-variant-numeric: tabular-nums; }
/* Wide fields (word lists / phrases) give the control the full width */
.sheet .maker-field.wide, .sheet .maker-field:has(textarea) {
  grid-template-columns: minmax(0, 1fr); align-items: stretch; gap: 6px;
}
/* Crossword / word-search style row editors (word + clue input pairs) */
.sheet .maker-field-label {
  font-family: var(--sans); font-size: 13px; font-weight: 600;
  color: var(--ink-soft); padding: 10px 10px 4px;
}
.sheet .xw-rows { display: flex; flex-direction: column; gap: 8px; padding: 0 10px; }
.sheet .xw-row { display: flex; gap: 8px; }
.sheet .xw-row input {
  font-family: var(--sans); font-size: 16px; color: var(--ink);
  background: var(--paper-bg); border: 1px solid var(--rule-strong);
  border-radius: var(--radius-sm); padding: 9px 10px; min-height: 44px;
  width: 100%; min-width: 0;
}
.sheet .xw-row input:focus { outline: none; border-color: var(--accent); }
.sheet .xw-row .xw-word { flex: 0 0 38%; }
.sheet .xw-row .xw-clue { flex: 1 1 auto; }
/* .ws-rows are word-only (no clue beside them), so the word takes the width the
   clue would have had instead of leaving 60% of the row empty. */
.sheet .ws-rows .xw-row .xw-word { flex: 1 1 auto; }
/* Buttons the makers render inside their panels (add-row etc.) */
.sheet .setup-panel .tb-btn {
  font-family: var(--sans); font-size: 14px; font-weight: 600;
  min-height: 44px; padding: 0 16px; margin: 8px 10px;
  color: var(--ink); background: var(--paper-bg);
  border: 1px solid var(--rule-strong); border-radius: var(--radius);
  cursor: pointer;
}
.sheet .setup-panel .tb-btn:active { background: var(--surface-2); }
.sheet .maker-row .tb-btn { margin: 4px 0; }   /* the row already indents */

/* Maze touch D-pad, pinned above the bottom bar: ONE circle cut into four
   quadrant wedges (top = up, and so on). A thumb lands on a wedge rather than
   hunting for a 52px key, and there are no dead gaps between the keys — every
   pixel of the circle belongs to a direction. */
.dpad {
  position: absolute; right: 14px; bottom: 14px; z-index: 5;
  width: 132px; height: 132px;
  border-radius: 50%; overflow: hidden;
  background: var(--surface);
  border: 1px solid var(--rule-strong);
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
  touch-action: none;
}
.dpad.dragging { box-shadow: 0 2px 10px rgba(0, 0, 0, 0.12); }
.dpad button {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  background: none; border: none; padding: 0;
  font-size: 20px; color: var(--ink);
  cursor: pointer; -webkit-tap-highlight-color: transparent;
  touch-action: none;
}
/* The pad captures the pointer and picks the wedge from the thumb's position, so
   the wedges must not swallow the press — held direction changes as you slide. */
.dpad .up, .dpad .right, .dpad .down, .dpad .left { pointer-events: none; }
/* Each key is the triangle from the centre out to one edge of the circle. */
.dpad .up    { clip-path: polygon(50% 50%, 0 0, 100% 0); align-items: flex-start; padding-top: 12px; }
.dpad .right { clip-path: polygon(50% 50%, 100% 0, 100% 100%); justify-content: flex-end; padding-right: 12px; }
.dpad .down  { clip-path: polygon(50% 50%, 100% 100%, 0 100%); align-items: flex-end; padding-bottom: 12px; }
.dpad .left  { clip-path: polygon(50% 50%, 0 100%, 0 0); justify-content: flex-start; padding-left: 12px; }
.dpad button:active, .dpad button.pressed { background: var(--accent-soft); }
.dpad button:active .d-ico, .dpad button.pressed .d-ico { color: var(--accent); }
/* The two diagonals that divide the wedges — decoration only, so they must
   never eat a press. */
.dpad::before {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background:
    linear-gradient(45deg, transparent calc(50% - 0.5px), var(--rule) calc(50% - 0.5px) calc(50% + 0.5px), transparent calc(50% + 0.5px)),
    linear-gradient(-45deg, transparent calc(50% - 0.5px), var(--rule) calc(50% - 0.5px) calc(50% + 0.5px), transparent calc(50% + 0.5px));
}
/* The hub sits above the wedges and drags the whole pad out of the way. */
.dpad .d-hub {
  position: absolute; z-index: 1;
  width: 40px; height: 40px; inset: 50% auto auto 50%;
  transform: translate(-50%, -50%);
  padding: 0; border-radius: 50%;
  background: var(--surface); border: 1px solid var(--rule);
  cursor: grab; touch-action: none; -webkit-tap-highlight-color: transparent;
}
.dpad.dragging .d-hub { cursor: grabbing; background: var(--surface-2); }

/* Hidden proxy input that summons the OS keyboard for canvas typing */
.kb-proxy {
  position: absolute; left: 0; bottom: 0;
  width: 2px; height: 2px; opacity: 0.01;
  font-size: 16px; border: none; background: transparent; color: transparent;
  caret-color: transparent;
}

/* ============================================================================
   WIDE — tablet landscape and desktop.
   ============================================================================
   Everything above is the phone, and stays the phone. This block is additive:
   it never edits a rule above it, so a narrow screen renders byte-identically
   to before.

   The play screen turns from a COLUMN (board above its controls, thumbs at the
   bottom) into a ROW (board beside its controls). That is the whole idea: on a
   phone the controls belong under your thumb; on a desktop a full-width strip
   of buttons at the bottom of a 1400px window is just a long way from the
   board. `.control-rail` is `display: contents` on a phone — it isn't a box at
   all, the widgets are laid out by .play-m exactly as they always were — and
   becomes the right-hand column here.

   The board itself needs no work: the engine already sizes it to fit its host
   (and the mouse + keyboard paths in input.js have always worked; nothing but
   the layout was missing).
   ------------------------------------------------------------------------- */
@media (min-width: 900px) {
  /* The shell stops being edge-to-edge. Without this the top bar spans the
     whole 1920px of a monitor while the board sits marooned in the middle. */
  #app {
    max-width: 1180px;
    margin-inline: auto;
    border-inline: 1px solid var(--rule);
  }

  /* Home: one long column of rows is a phone idiom. Fan them out — as CARDS.
     The phone row keeps its blurb on ONE line and ellipses the overflow, which
     is right when the row is as wide as the screen and wrong the moment it is a
     third of it: "Encode a phrase into a letter-substitution puzzle" simply ran
     out of room. A card can take a second line instead of losing the words, and
     because the grid stretches every card in a track to the tallest of them, an
     uneven blurb costs nothing but a little white space. */
  .home-list .cat-rows {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
    gap: 10px;
    padding: 4px 16px 18px;
    align-items: stretch;
  }
  .home-list .cat-rows .row {
    height: 100%;                       /* fill the stretched track */
    align-items: flex-start;            /* swatch aligns to the title, not the middle */
    gap: 10px;
    padding: 13px 14px;
    background: var(--surface);
    border: 1px solid var(--rule);
    border-radius: var(--radius);
    transition: border-color 0.12s ease, background-color 0.12s ease;
  }
  .home-list .cat-rows .row .swatch { margin-top: 5px; }   /* optical: centre on the cap-height */
  .home-list .cat-rows .row .row-name { font-size: 17px; line-height: 1.25; }
  /* The whole point: wrap instead of ellipsing. Clamped at three lines so one
     wordy blurb can't stretch a whole row of cards. */
  .home-list .cat-rows .row .row-blurb {
    white-space: normal;
    text-overflow: clip;
    line-height: 1.4;
    margin-top: 3px;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 3;
    overflow: hidden;
  }
  .home-list .cat-rows .row:hover { border-color: var(--accent); }
  .home-head { padding-top: 18px; }

  /* Play: board left, controls right. */
  .play-m {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 300px;
    grid-template-rows: auto minmax(0, 1fr) auto;
    column-gap: 0;
  }
  .play-m > .topbar   { grid-column: 1 / -1; grid-row: 1; }
  .play-m > .stage-m  { grid-column: 1; grid-row: 2; padding: 20px; }
  .play-m > .status-m { grid-column: 1; grid-row: 3; }

  .control-rail {
    display: flex;                    /* was `contents` on the phone */
    flex-direction: column;
    gap: 0;
    grid-column: 2;
    grid-row: 2 / 4;
    min-height: 0;
    overflow-y: auto;
    background: var(--surface);
    border-left: 1px solid var(--rule-strong);
  }
  /* The widgets span the rail EDGE TO EDGE — they are the column, not cards
     floating in it. No side padding on the rail and no border/radius of their
     own: their only edge is the rule that separates one widget from the next,
     so the rail reads as one continuous strip against the board's border. */
  .control-rail > * {
    border: none;
    border-radius: 0;
    padding-bottom: 0;
    border-bottom: 1px solid var(--rule-strong);
  }
  .control-rail > :last-child { border-bottom: none; }
  .control-rail .bottombar { border-top: none; }
  .control-rail .bb-cell { min-height: 52px; }
  /* A mode bar of 2-3 segments reads better stacked in a narrow rail than
     squeezed side by side. */
  .control-rail .mode-bar { flex-direction: column; }
  .control-rail .mode-bar .bb-cell + .bb-cell {
    border-left: none; border-top: 1px solid var(--rule);
  }
  .control-rail .mode-bar .bb-cell {
    flex-direction: row; gap: 10px; justify-content: flex-start; padding-inline: 16px;
  }
  .control-rail .mode-bar .bb-cell .lbl { font-size: 13px; }

  /* The number pad's `flex-wrap` is tuned for a phone's full width; in a 300px
     rail it crams 1-7 onto one line and then stretches 8 and 9 across the next.
     In the rail it becomes what it looks like anyway: a 3x3 keypad. */
  .control-rail .np-digits {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 1px;
  }
  .control-rail .np-digits .np-key { min-width: 0; }
  /* Clear + Notes: a pair. */
  .control-rail .np-actions { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1px; }
  /* The tray's actions are usually just the Pencil chip; a 2-column grid would
     leave a filled empty cell beside it. Let each row's children share it. */
  .control-rail .pt-actions { display: flex; gap: 1px; }
  .control-rail .pt-actions > * { flex: 1 1 auto; }
  /* Piece trays (magnets/undead/slant…): two per row reads better than a squeeze. */
  .control-rail .pt-pieces { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1px; }
  .control-rail .pt-pieces .piece { min-width: 0; }

  /* Paper puzzles keep their single column (their canvas scrolls and their
     keypad is a grid), but the bar shouldn't hug the window edge. */
  .play-m > .bottombar { grid-column: 1 / -1; grid-row: 4; }
  .canvas-area { grid-column: 1 / -1; grid-row: 2; }
}

/* Pointer affordances. The phone CSS never sets a cursor and gives feedback on
   :active only — which on a mouse means a button looks dead until you press it. */
@media (hover: hover) and (pointer: fine) {
  .row, .bb-cell, .icon-btn, .piece, .np-key, .np-erase, .np-notes,
  .pencil-chip, .cat-head, .sheet-action { cursor: pointer; }
  .row:hover, .cat-head:hover { background: var(--surface-2); }
  .icon-btn:hover:not(:disabled),
  .bb-cell:hover:not(:disabled),
  .piece:hover, .np-key:hover, .np-erase:hover { background: var(--surface-2); }
  .sheet-action:hover { background: var(--surface-2); }
  /* Keep the armed/active highlight winning over hover. */
  .bb-cell.active:hover { background: var(--accent-soft); }
}

/* ---- Icons (js/icons.js) --------------------------------------------------
   Inline SVG, 24x24, stroke: currentColor — so an icon is the same ink as the
   text beside it and flips with the theme. Sized by its container, never by a
   font metric: the Unicode symbols these replaced each had their own baseline
   and optical size, so a row of them never quite lined up. */
.i { width: 1em; height: 1em; display: block; overflow: visible; }
.ico .i { width: 20px; height: 20px; }
.hb-cell .ico .i { width: 21px; height: 21px; }
.bb-cell .ico .i { width: 20px; height: 20px; }
.icon-btn .i { width: 21px; height: 21px; }
.cat-chev .i { width: 13px; height: 13px; }
.p-ico .i { width: 24px; height: 24px; }
/* Icon + word ("Clear", "Notes", "Pencil"): centre them on one line. */
.chip-ico { display: inline-flex; vertical-align: -3px; }
.chip-ico .i { width: 15px; height: 15px; }
.np-erase, .np-notes, .pencil-chip { display: flex; align-items: center; justify-content: center; }
/* The chevron rotates when a category collapses; it used to be a text glyph. */
.cat-chev { display: inline-flex; align-items: center; transition: transform 0.15s ease; }

/* One-shot action bar (profile.actions — Black Box's Verify). Reads as an
   action, not a mode: accent-filled when live, plainly dead when not, so
   "you can't check yet" is visible rather than something you discover by
   tapping. */
.action-bar { border-top: 1px solid var(--rule-strong); }
.action-bar .bb-cell + .bb-cell { border-left: 1px solid var(--rule); }
/* The primary action (Verify) fills with the accent when it becomes legal, so
   "you can check now" is something you SEE, not something you discover. */
.action-bar .bb-cell.primary:not(:disabled) { background: var(--accent-soft); }
.action-bar .bb-cell.primary:not(:disabled) .ico,
.action-bar .bb-cell.primary:not(:disabled) .lbl { color: var(--accent); }
.action-bar .bb-cell:disabled { opacity: 0.4; }
