/* XWord — "Newsstand" theme (David's re-pick from the §14 pitch, 2026-07-18):
   the newspaper heritage worn honestly — newsprint ground, true ink-black grid,
   serif masthead titles, press-blue accent, gold stars.
   Light = "Morning edition", dark = "Late edition". All color flows from these tokens. */
:root {
  --bg: #f7f5ef; --surface: #fffdf8; --surface2: #eceadf; --border: #d8d4c8;
  --ink: #191817; --muted: #75716a;
  --accent: #1d5fa8; --accent-ink: #ffffff; --gold-accent: #e3b93f;
  --accent-soft: color-mix(in srgb, #1d5fa8 12%, #fffdf8);
  --teal: #2f7d4f; --grape: #1d5fa8; --danger: #c0392b; --gold: #b98a12;
  --shadow: 0 6px 20px rgba(25, 24, 23, .09);
  --radius: 11px;
  --cell-bg: #fffdf8; --cell-black: #191817; --cell-line: #8f8c85;
  --cell-sel: #f9d976; --cell-word: #dce8f5; --cell-rel: #eaf1f9;
  --cluebar-bg: #e9e5da; --cluebar-ink: #191817; --cluebar-num: #1d5fa8;
  --glass: rgba(247, 245, 239, .85);
  --sat: env(safe-area-inset-top, 0px); --sab: env(safe-area-inset-bottom, 0px);
  --sal: env(safe-area-inset-left, 0px); --sar: env(safe-area-inset-right, 0px);
}
:root[data-theme="dark"] {
  --bg: #141517; --surface: #1e2024; --surface2: #24262b; --border: #33353b;
  --ink: #eceae4; --muted: #8f8d86;
  --accent: #6ba3e8; --accent-ink: #10131a; --gold-accent: #d9ad3c;
  --accent-soft: color-mix(in srgb, #6ba3e8 16%, #1e2024);
  --teal: #59b87f; --grape: #6ba3e8; --danger: #ff6b5e; --gold: #d9ad3c;
  --shadow: 0 10px 26px rgba(0, 0, 0, .48);
  --cell-bg: #1e2024; --cell-black: #000000; --cell-line: #3f4147;
  --cell-sel: #8a6a14; --cell-word: #27303c; --cell-rel: #1f242c;
  --cluebar-bg: #24262b; --cluebar-ink: #eceae4; --cluebar-num: #6ba3e8;
  --glass: rgba(20, 21, 23, .85);
}

/* Text sitting DIRECTLY on the page background (no surface behind it) has to
   track the BACKGROUND's tone, not the theme's: a dark custom background under
   the light theme would otherwise paint near-black --ink onto near-black. Can't
   just flip --ink itself — it's also the text color on light --surface cards.
   No custom background set => falls through to --ink, which is already right
   for both themes. */
:root { --on-bg: var(--ink); }
:root[data-bg-tone="dark"] { --on-bg: #f4f1ea; }
:root[data-bg-tone="light"] { --on-bg: #191817; }

* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
html, body { margin: 0; height: 100%; }
body {
  background: var(--bg); color: var(--ink);
  font-family: -apple-system, "Segoe UI", system-ui, Roboto, sans-serif;
  overflow-x: hidden; overscroll-behavior-y: none;
}
/* Marquee display voice: serif for puzzle titles and marquee headings */
.serif, .sol-head .t, .card .big, .tile .v, h2 {
  font-family: Georgia, "Iowan Old Style", "Times New Roman", serif;
  letter-spacing: .01em;
}
button { font: inherit; color: inherit; background: none; border: none; cursor: pointer; padding: 0; }
.hidden { display: none !important; }
h1, h2, h3 { margin: 0; font-weight: 700; }
input, select { font: inherit; color: var(--ink); background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 8px 10px; }

/* ---------- splash / PIN ---------- */
#splash {
  position: fixed; inset: 0; z-index: 100; background: var(--bg);
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: clamp(14px, 3.5vh, 28px);
  padding: calc(var(--sat) + 20px) 20px calc(var(--sab) + 20px);
  touch-action: manipulation; -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; -webkit-tap-highlight-color: transparent;
}
.splash-grid { display: flex; gap: 4px; }
.splash-grid span {
  width: 46px; height: 46px; display: grid; place-items: center;
  background: var(--surface); border: 2px solid var(--ink); font-weight: 700; font-size: 26px; color: var(--ink);
  font-family: Georgia, "Iowan Old Style", serif;
}
.splash-grid span.b { background: var(--cell-black); border-color: var(--cell-black); }
.splash-sub { color: var(--muted); text-align: center; font-size: 15px; }
.pin-dots { display: flex; gap: 10px; justify-content: center; min-height: 14px; margin-bottom: 14px; }
.pin-dots i { width: 14px; height: 14px; border-radius: 50%; border: 2px solid var(--muted); }
.pin-dots i.on { background: var(--accent); border-color: var(--accent); }
#pin-pad.busy .pin-dots i.on { animation: pinpulse .9s ease-in-out infinite; }
@keyframes pinpulse { 50% { opacity: .35; } }
#pin-pad.bad .pin-dots { animation: pinshake .35s; }
@keyframes pinshake { 20%, 60% { transform: translateX(-8px); } 40%, 80% { transform: translateX(8px); } }
.pin-grid { display: grid; grid-template-columns: repeat(3, clamp(64px, min(22vw, 10vh), 84px)); gap: clamp(8px, 1.8vh, 14px); justify-content: center; touch-action: manipulation; }
.pin-grid button {
  aspect-ratio: 1; width: 100%; padding: 0; border-radius: 50%; background: var(--surface); border: 1px solid var(--border);
  font-size: clamp(26px, 4vh, 32px); font-weight: 600; box-shadow: var(--shadow); touch-action: manipulation;
}
.pin-grid button:disabled { opacity: .5; }
.pin-grid button:active { background: var(--accent-soft); }
.pin-msg { min-height: 22px; color: var(--danger); text-align: center; margin-top: 10px; font-size: 16px; max-width: 320px; }
.pin-msg.note { color: var(--muted); }

/* ---------- app chrome ---------- */
/* position:fixed; inset:0 rather than height:100dvh — in a black-translucent standalone
   iOS app 100dvh comes up SHORT by the top inset (~60pt), which showed as a bare band under
   the on-screen keyboard. Every overlay (toasts, modals, splash, badge) lives OUTSIDE #app, so
   the stacking context a fixed box starts changes nothing. */
#app { position: fixed; inset: 0; display: flex; flex-direction: column; }
#topbar {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap; row-gap: 8px;
  padding: calc(var(--sat) + 8px) calc(var(--sar) + 14px) 8px calc(var(--sal) + 14px);
  position: sticky; top: 0; z-index: 20; background: var(--glass); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--border);
}
/* No brand/logo row — the topbar is nav-tabs only, so mobile gets that whole
   former icon+blank-space row back for the puzzle. Centered + natural-width
   here on tablet/desktop (edge-to-edge stretched pills look broken at 1280px);
   the phone breakpoint below turns it into a full-width segmented bar instead,
   since that's the actual "use the space better" win on a narrow screen. */
#tabs { display: flex; gap: 6px; margin: 0 auto; }
.tab { padding: 8px 13px; border-radius: 999px; color: var(--muted); font-weight: 600; font-size: 15px; }
.tab.active { background: var(--accent); color: var(--accent-ink); }
.theme-pill { display: flex; border: 1px solid var(--border); border-radius: 999px; overflow: hidden; margin-left: 8px; }
.theme-pill button { padding: 6px 10px; font-size: 12px; font-weight: 700; color: var(--muted); }
.theme-pill button.active { background: var(--accent); color: var(--accent-ink); }
/* Views scroll inside #main (not the body) so the solver can lock to the viewport. */
#main { flex: 1; min-height: 0; width: 100%; overflow-y: auto; padding: 0 calc(var(--sar)) 0 calc(var(--sal)); }
.view { padding: 16px clamp(12px, 3vw, 28px) calc(var(--sab) + 24px); max-width: 1400px; margin: 0 auto; }

/* ---------- cards / home ---------- */
.cards { display: grid; gap: 14px; grid-template-columns: repeat(auto-fit, minmax(270px, 1fr)); }
.card {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  padding: 16px; box-shadow: var(--shadow);
}
.card h3 { font-size: 15px; color: var(--muted); text-transform: uppercase; letter-spacing: .06em; margin-bottom: 8px; display: flex; align-items: center; gap: 8px; }
.card .big { font-size: 20px; font-weight: 700; margin: 4px 0; }
.card .sub { color: var(--muted); font-size: 14px; }
.btn {
  display: inline-flex; align-items: center; gap: 7px; background: var(--accent); color: var(--accent-ink);
  padding: 10px 16px; border-radius: 12px; font-weight: 700; font-size: 15px; margin-top: 10px;
}
.btn:disabled { opacity: .55; }
.btn.ghost { background: var(--surface2); color: var(--ink); }
.btn.small { padding: 6px 12px; font-size: 13px; margin-top: 0; }
.chips { display: flex; flex-wrap: wrap; gap: 7px; margin: 8px 0; }
.chip {
  padding: 6px 12px; border-radius: 999px; background: var(--surface2); color: var(--ink);
  font-size: 13px; font-weight: 600; border: 1px solid var(--border);
}
.chip.on { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); }
.star-gold { color: var(--gold); } .star-blue { color: var(--grape); }

/* ---------- archive calendar ---------- */
.arch-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 12px; }
.arch-head .spacer { flex: 1; }
.cal { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 12px; box-shadow: var(--shadow); }
.cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 5px; }
.cal-dowhead { text-align: center; font-size: 12px; color: var(--muted); font-weight: 700; padding: 4px 0; }
.cal-day {
  aspect-ratio: 1; border-radius: 10px; background: var(--surface2); border: 1px solid var(--border);
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px;
  font-size: clamp(12px, 2.6vw, 16px); font-weight: 600; position: relative;
}
.cal-day.empty { visibility: hidden; }
.cal-day .dot { width: 7px; height: 7px; border-radius: 50%; }
.cal-day.solved-gold { background: color-mix(in srgb, var(--gold) 22%, var(--surface)); }
.cal-day.solved-blue { background: color-mix(in srgb, var(--grape) 20%, var(--surface)); }
.cal-day.inprog { outline: 2px solid var(--teal); outline-offset: -2px; }
.cal-day .st { font-size: 11px; line-height: 1; }
.cal-day.downloaded::before {
  content: ""; position: absolute; top: 3px; right: 3px; width: 6px; height: 6px;
  border-radius: 50%; background: var(--grape);
}
.list-rows { display: flex; flex-direction: column; gap: 8px; margin-top: 12px; }
.list-row {
  display: flex; align-items: center; gap: 10px; text-align: left; background: var(--surface);
  border: 1px solid var(--border); border-radius: 12px; padding: 10px 14px;
}
.list-row .grow { flex: 1; min-width: 0; }
.list-row .t { font-weight: 700; }
.list-row .s { color: var(--muted); font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ---------- solver ---------- */
/* Solver fills the viewport exactly; nothing outside the clue lists scrolls. */
#view-solver, #view-word, #view-jumble { padding: 8px clamp(6px, 1.5vw, 20px) 0; display: flex; flex-direction: column; height: 100%; overflow: hidden; max-width: none; }
.sol-head { display: flex; align-items: center; gap: 8px; padding: 2px 4px 6px; }
.sol-head .meta { flex: 1; min-width: 0; }
/* .sol-head (shared by the solver, Wordle and Jumble) has no surface of its own,
   so its text sits straight on the page background — must track --on-bg. */
.sol-head .t { color: var(--on-bg); font-weight: 800; font-size: 15px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sol-head .s { color: color-mix(in srgb, var(--on-bg) 68%, transparent); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.iconbtn {
  min-width: 38px; height: 38px; border-radius: 11px; display: grid; place-items: center;
  background: var(--surface2); border: 1px solid var(--border); font-size: 16px; font-weight: 700; padding: 0 8px;
}
.iconbtn.on { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); }
.iconbtn.fav.on { background: var(--surface2); color: var(--gold); border-color: var(--border); }
.timer { color: var(--on-bg); font-variant-numeric: tabular-nums; font-weight: 700; font-size: 15px; }

.sol-body { flex: 1; display: flex; flex-direction: column; min-height: 0; }
.grid-wrap { display: flex; justify-content: center; align-items: flex-start; position: relative; touch-action: manipulation; flex: 1; min-height: 0; }
#grid {
  display: grid; background: var(--cell-line); border: 2px solid var(--cell-black); gap: 1px;
  user-select: none; -webkit-user-select: none;
}
.cell { position: relative; background: var(--cell-bg); display: grid; place-items: center; }
.cell.black { background: var(--cell-black); }
.cell .num { position: absolute; top: 1px; left: 2px; font-size: calc(var(--cs) * .26); color: var(--muted); font-weight: 600; line-height: 1; }
/* position/z-index: the ring is absolutely positioned, so it would otherwise
   paint over the letter. */
.cell .val { position: relative; z-index: 1; font-size: calc(var(--cs) * .62); font-weight: 700; line-height: 1; margin-top: calc(var(--cs) * .12); }
.cell .val.long { font-size: calc(var(--cs) * .30); word-break: break-all; text-align: center; }
.cell.pencil .val { color: var(--muted); font-style: italic; font-weight: 500; }
.cell.rel { background: var(--cell-rel); }
.cell.word { background: var(--cell-word); }
.cell.sel { background: var(--cell-sel); outline: 2px solid var(--accent); outline-offset: -2px; z-index: 1; }
/* Theme circles are structural puzzle data, so they get a real child element
   instead of ::after. .cell.checked-ok and .cell.revealed already claim the
   cell's only two pseudo-elements, and autocheck marks EVERY correct letter
   checked — so on a circled square the two ::after rules used to merge into one
   mangled box (✓ glyph still carrying the ring's border + 50% radius), losing
   the circle exactly on the themed puzzles that need it. Ring width tracks --cs
   so it stays proportional from a 21x21 Sunday on a phone to a 15x15 on desktop. */
.cell .circ {
  position: absolute; inset: 7%; border-radius: 50%; pointer-events: none;
  border: max(1px, calc(var(--cs) * .04)) solid var(--muted);
}
.cell.wrong::before {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(to top right, transparent 46%, var(--danger) 46%, var(--danger) 54%, transparent 54%);
}
.cell.checked-ok .val { color: var(--teal); }
.cell.checked-ok::after { content: "✓"; position: absolute; bottom: 0; right: 2px; font-size: calc(var(--cs) * .24); color: var(--teal); }
.cell.revealed::before { content: ""; position: absolute; top: 2px; right: 2px; width: calc(var(--cs) * .16); height: calc(var(--cs) * .16); border-radius: 50%; background: var(--danger); }

.pause-cover {
  position: absolute; inset: 0; display: flex; flex-direction: column; gap: 14px; align-items: center; justify-content: center;
  background: var(--surface); border: 2px solid var(--cell-black); z-index: 5;
}

.clue-bar {
  display: flex; align-items: stretch; gap: 2px; background: var(--cluebar-bg); color: var(--cluebar-ink);
  border: 1px solid var(--border); border-radius: 12px; margin: 8px 0; min-height: 52px; overflow: hidden;
}
.clue-bar .arrow { width: 44px; display: grid; place-items: center; font-size: 20px; color: var(--cluebar-ink); flex: none; }
.clue-bar .txt { flex: 1; display: flex; align-items: center; justify-content: center; text-align: center; padding: 6px 2px; font-size: 15.5px; font-weight: 600; line-height: 1.25; color: var(--cluebar-ink); }
.clue-bar .txt .cnum { font-weight: 800; margin-right: 6px; white-space: nowrap; color: var(--cluebar-num); }

/* clue lists (desktop side panels; mobile drawer) */
.sol-layout { display: flex; gap: 16px; flex: 1; min-height: 0; }
.clue-panels { display: none; }
@media (min-width: 980px) {
  .sol-layout { flex-direction: row; }
  .clue-panels { display: flex; flex: 1; gap: 14px; min-width: 340px; min-height: 0; }
  /* Real surface, not a transparent column: clue text is --ink, so without its
     own background a custom page background (esp. a dark one under the light
     theme, or a busy photo) shows straight through and the clues vanish. */
  .clue-col {
    flex: 1; display: flex; flex-direction: column; min-height: 0;
    background: var(--surface); border: 1px solid var(--border);
    border-radius: var(--radius); padding: 10px; box-shadow: var(--shadow);
  }
  .clue-col h4 { margin: 0 0 6px; text-transform: uppercase; font-size: 13px; letter-spacing: .07em; color: var(--muted); }
  .clue-list { overflow-y: auto; overscroll-behavior: contain; flex: 1; min-height: 0; display: flex; flex-direction: column; gap: 2px; padding-right: 4px; }
  .clue-bar .arrow { display: none; }
  .clue-bar .arrow.explain { display: grid; }
}
.clue-bar .arrow.explain:disabled { opacity: .5; }
.clue-item { display: flex; gap: 8px; text-align: left; padding: 6px 8px; border-radius: 8px; font-size: 14px; line-height: 1.3; }
.clue-item .n { font-weight: 800; min-width: 22px; text-align: right; color: var(--muted); }
.clue-item.cur { background: var(--accent-soft); }
.clue-item.cur .n { color: var(--accent); }
.clue-item.done { color: var(--muted); }
.clue-item.done .n { color: var(--border); }

/* keyboard (shared by the crossword solver #kb and Wordle #wd-kb) */
#kb, #wd-kb { padding: 4px 2px calc(var(--sab) + 6px); user-select: none; -webkit-user-select: none; flex: 1 1 auto; min-width: 0; }
.kb-row { display: flex; justify-content: center; gap: 5px; margin-bottom: 6px; }
.kb-key {
  flex: 1; max-width: 42px; height: var(--kh, 46px); border-radius: 8px; background: var(--surface);
  border: 1px solid var(--border); box-shadow: 0 1px 2px rgba(0,0,0,.12);
  font-size: 17px; font-weight: 600; display: grid; place-items: center;
}
.kb-key:active { background: var(--accent-soft); }
.kb-key.wide { max-width: 64px; flex: 1.6; font-size: 13px; }
/* Crossword only: Tab / Back / Pivot clue-navigation buttons, docked to the left of
   the on-screen keyboard. Sized/hidden as one unit with #kb (see .kb-dock rules below). */
.kb-dock { display: flex; align-items: stretch; gap: 6px; }
.kb-nav { display: flex; flex-direction: column; gap: 5px; flex: none; padding: 4px 0 calc(var(--sab) + 6px) 4px; }
.kb-nav-btn {
  flex: 1; width: 40px; border-radius: 8px; background: var(--surface); border: 1px solid var(--border);
  box-shadow: 0 1px 2px rgba(0,0,0,.12); font-size: 10.5px; font-weight: 700; color: var(--ink);
  display: grid; place-items: center; text-align: center; line-height: 1.15; padding: 2px;
}
.kb-nav-btn:active { background: var(--accent-soft); }
/* Tablet+ (iPad and up): bigger, wider keys that fill the row and put the extra
   bottom-bar room to use as larger finger-friendly tap targets, not empty space. */
/* min-height too: a phone in LANDSCAPE is >600px wide but only ~390px tall, and
   handing it 64px iPad keys eats vertical space it doesn't have. Real iPads clear
   700px in both orientations, so they still get the big keys. */
@media (min-width: 600px) and (min-height: 700px) {
  #kb, #wd-kb { padding: 10px 14px calc(var(--sab) + 14px); }
  .kb-row { gap: 10px; margin-bottom: 12px; }
  .kb-key { max-width: 78px; height: 64px; font-size: 25px; border-radius: 12px; }
  .kb-key.wide { max-width: 108px; font-size: 16px; }
  .kb-nav { gap: 10px; padding: 10px 0 calc(var(--sab) + 14px) 10px; }
  .kb-nav-btn { width: 62px; font-size: 14px; border-radius: 12px; }
}
/* Hide the CROSSWORD on-screen keyboard (+ its Tab/Back/Pivot dock) only on true
   desktop (mouse) setups — an iPad without a hardware keyboard has no other way to
   type, regardless of viewport width. #wd-kb (Wordle) is EXEMPT: its coloring is the
   ruled-out/ruled-in letter legend, not just an input method, so it stays visible
   (and clickable) on desktop too. */
@media (min-width: 980px) and (hover: hover) and (pointer: fine) { .kb-dock { display: none; } }
/* JS-controlled hide: set on hardware-keystroke detection or via the ⌨ toggle
   (iPadOS reports pointer:coarse even with a Magic Keyboard attached). */
.kb-dock.kb-hidden, #wd-kb.kb-hidden { display: none !important; }

/* ---------- Wordle ---------- */
.wd-body { flex: 1; min-height: 0; display: flex; flex-direction: column; align-items: center; padding: 8px clamp(6px, 1.5vw, 20px) 0; }
/* flex: none overrides the shared `#kb, #wd-kb { flex: 1 1 auto }`. That grow is
   right for the crossword, where #kb sits in the .kb-dock ROW and should fill the
   width beside the Tab/Back/Pivot column — but .wd-body is a COLUMN, so the same
   declaration made the keyboard swallow ~250px of vertical space it didn't need
   and squeezed .wd-grid (flex-basis 0) below its own content height. Centered
   overflow then spilled out BOTH ends and the top tile row landed on the hint. */
#wd-kb { align-self: stretch; flex: none; }
.wd-hint { color: color-mix(in srgb, var(--on-bg) 72%, transparent); font-size: 13.5px; text-align: center; max-width: 420px; margin-bottom: 10px; line-height: 1.4; }
/* Full-strength --on-bg + underline instead of --accent: the accent is a fixed
   press-blue that can't be read against an arbitrary custom background. */
.wd-hint button { background: none; border: none; cursor: pointer; font: inherit; padding: 0; margin-left: 2px; color: var(--on-bg); text-decoration: underline; font-weight: 600; }
/* `safe center` (with plain `center` as the fallback for older engines) centers
   normally but falls back to start-alignment if the rows ever exceed the box —
   plain centered overflow spills out the TOP too, overlapping the hint above. */
/* overflow-y contains any leftover overflow inside the grid's own box rather than
   letting it paint over the keyboard — the sanctioned fallback when a view truly
   can't fit (landscape phone: ~300px of usable height for grid + keyboard). */
.wd-hard { white-space: nowrap; font-weight: 700; color: var(--on-bg); }
.wd-grid { display: flex; flex-direction: column; gap: 6px; flex: 1; justify-content: center; justify-content: safe center; min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
.wd-row { display: flex; gap: 6px; }
.wd-cell {
  position: relative;
  width: clamp(46px, 12vw, 62px); height: clamp(46px, 12vw, 62px);
  display: grid; place-items: center; font-size: clamp(22px, 5vw, 30px); font-weight: 700;
  border-radius: 8px; border: 2px solid var(--border); background: var(--surface); color: var(--ink);
  text-transform: uppercase; transition: transform .15s;
}
/* Letter rides above the reveal mask so it stays readable through the flip. */
.wd-cell span { position: relative; z-index: 1; }
.wd-cell.typed { border-color: var(--ink); }
.wd-cell.correct { background: var(--teal); border-color: var(--teal); color: #fff; }
.wd-cell.present { background: var(--gold-accent); border-color: var(--gold-accent); color: var(--ink); }
.wd-cell.absent { background: var(--surface2); border-color: var(--surface2); color: var(--muted); }
.wd-row.shake .wd-cell { animation: wd-shake .4s; }
@keyframes wd-shake {
  10%, 90% { transform: translateX(-1px); } 20%, 80% { transform: translateX(2px); }
  30%, 50%, 70% { transform: translateX(-4px); } 40%, 60% { transform: translateX(4px); }
}
/* Tile reveal. The result colors are already on the tile in the markup, so a
   neutral ::after mask hides them until the tile is edge-on at the halfway
   point — that's what sells the flip as "turning over" a tile. Stagger comes
   from a per-tile --d custom property (inherits into the pseudo-element, so
   mask and flip stay in lockstep); all CSS, no timers to go stale if the view
   is left mid-reveal. */
.wd-cell.flip { animation: wd-flip .5s ease var(--d, 0ms) both; }
.wd-cell.flip::after {
  content: ""; position: absolute; inset: -2px; border-radius: 8px;
  background: var(--surface); border: 2px solid var(--ink);
  animation: wd-mask .5s step-end var(--d, 0ms) both;
}
@keyframes wd-flip { 0%, 100% { transform: rotateX(0deg); } 50% { transform: rotateX(-90deg); } }
@keyframes wd-mask { 0% { opacity: 1; } 50%, 100% { opacity: 0; } }
/* Winning row hops once the whole row has finished turning over. `forwards`
   (not `both`) matters: a backwards fill would apply bounce's transform during
   its delay and clobber the flip that's still running underneath it. */
.wd-cell.flip.bounce {
  animation: wd-flip .5s ease var(--d, 0ms) both, wd-bounce .5s ease var(--bd, 0ms) forwards;
}
@keyframes wd-bounce {
  0%, 100% { transform: translateY(0); } 40% { transform: translateY(-24px); } 65% { transform: translateY(-7px); }
}
/* Typing feedback — the tile springs as the letter lands. */
.wd-cell.pop { animation: wd-pop .12s ease-out; }
@keyframes wd-pop { 50% { transform: scale(1.09); } }
@media (prefers-reduced-motion: reduce) {
  .wd-cell.flip, .wd-cell.flip::after, .wd-cell.flip.bounce, .wd-cell.pop {
    animation-duration: .01ms !important; animation-delay: 0ms !important;
  }
}
.kb-key.correct { background: var(--teal); border-color: var(--teal); color: #fff; }
.kb-key.present { background: var(--gold-accent); border-color: var(--gold-accent); color: var(--ink); }
.kb-key.absent { background: var(--surface2); border-color: var(--surface2); color: var(--muted); }

/* ---------- Jumble ---------- */
.jm-body { flex: 1; min-height: 0; display: flex; flex-direction: column; align-items: center; padding: 8px clamp(6px, 1.5vw, 20px) 0; }
.jm-hint { color: color-mix(in srgb, var(--on-bg) 72%, transparent); font-size: 13.5px; text-align: center; max-width: 460px; margin-bottom: 10px; line-height: 1.4; }
.jm-hint button { background: none; border: none; cursor: pointer; font: inherit; padding: 0; margin-left: 2px; color: var(--on-bg); text-decoration: underline; font-weight: 600; }
/* Final reveal row: fixed (not scrolling) so the growing bonus word always stays visible. */
.jm-final { flex: none; margin-bottom: 12px; }
.jm-final-row { display: flex; flex-wrap: wrap; justify-content: center; gap: 5px; max-width: 480px; }
.jm-fbox {
  position: relative; width: clamp(28px, 7vw, 40px); height: clamp(32px, 8vw, 46px);
  display: grid; place-items: center; font-size: clamp(15px, 4vw, 20px); font-weight: 700;
  border-radius: 6px; border: 2px solid var(--gold-accent); background: var(--surface); color: var(--ink);
  text-transform: uppercase;
}
.jm-fbox.filled { background: var(--accent-soft); }
.jm-fnum { position: absolute; top: 1px; left: 3px; font-size: 8px; font-weight: 700; color: var(--muted); line-height: 1; }
/* Word list: the ONLY scrolling region in this view (chrome — header, hint, final
   row — all stay fixed), per house rule for content that can't fit one screen. */
.jm-words { flex: 1; min-height: 0; width: 100%; max-width: 520px; overflow-y: auto; overscroll-behavior: contain; padding-bottom: 8px; }
.jm-word {
  background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
  padding: 10px; margin-bottom: 10px; display: flex; flex-direction: column; gap: 8px; align-items: center;
}
.jm-word.solved { background: color-mix(in srgb, var(--teal) 12%, var(--surface)); border-color: var(--teal); }
.jm-word.shake { animation: jm-shake .4s; }
@keyframes jm-shake {
  10%, 90% { transform: translateX(-1px); } 20%, 80% { transform: translateX(2px); }
  30%, 50%, 70% { transform: translateX(-4px); } 40%, 60% { transform: translateX(4px); }
}
.jm-slots, .jm-pool { display: flex; flex-wrap: wrap; justify-content: center; gap: 5px; }
.jm-slot, .jm-tile {
  position: relative; width: clamp(30px, 8vw, 42px); height: clamp(30px, 8vw, 42px);
  display: grid; place-items: center; font-size: clamp(15px, 4vw, 19px); font-weight: 700;
  border-radius: 8px; text-transform: uppercase;
}
.jm-slot { border: 2px dashed var(--border); background: transparent; color: var(--ink); }
.jm-slot.filled { border-style: solid; background: var(--surface2); }
.jm-slot.filled.solved { border-color: var(--teal); background: color-mix(in srgb, var(--teal) 16%, var(--surface)); }
.jm-slot.circled { border-color: var(--gold-accent); }
.jm-tile { background: var(--surface); border: 1px solid var(--border); box-shadow: 0 1px 2px rgba(0,0,0,.12); color: var(--ink); }
.jm-tile:active { background: var(--accent-soft); }
.jm-tile.circled { border-color: var(--gold-accent); border-width: 2px; }
.jm-tile.placeholder { visibility: hidden; }

/* ---------- stats ---------- */
.stat-tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 12px; margin-bottom: 16px; }
.tile { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 14px; box-shadow: var(--shadow); }
.tile .v { font-size: 26px; font-weight: 800; font-variant-numeric: tabular-nums; }
.tile .l { color: var(--muted); font-size: 13px; font-weight: 600; }
.wd-hist { display: flex; flex-direction: column; gap: 5px; margin-bottom: 4px; }
.wd-hist-row { display: flex; align-items: center; gap: 8px; }
.wd-hist-label { width: 12px; text-align: right; font-weight: 700; font-size: 13px; color: var(--muted); flex: none; }
.wd-hist-track { flex: 1; height: 20px; background: var(--surface2); border-radius: 5px; overflow: hidden; }
.wd-hist-bar { height: 100%; background: var(--accent); border-radius: 5px; transition: width .3s ease; }
.wd-hist-bar.hl { background: var(--teal); }
.wd-hist-count { width: 22px; text-align: left; font-weight: 700; font-size: 13px; font-variant-numeric: tabular-nums; flex: none; }
table.stats { width: 100%; border-collapse: collapse; }
table.stats th, table.stats td { padding: 8px 10px; text-align: left; border-bottom: 1px solid var(--border); font-variant-numeric: tabular-nums; }
table.stats th { color: var(--muted); font-size: 13px; text-transform: uppercase; letter-spacing: .05em; }
.tablewrap { overflow-x: auto; }

/* solve heatmap */
.hm-scroll { overflow-x: auto; padding-bottom: 4px; }
.hm-grid { display: grid; grid-auto-flow: column; grid-template-rows: repeat(7, 12px); grid-auto-columns: 12px; gap: 3px; width: max-content; }
.hm-c { width: 12px; height: 12px; border-radius: 3px; background: var(--surface2); border: 1px solid var(--border); box-sizing: border-box; }
.hm-1 { background: color-mix(in srgb, var(--gold-accent) 35%, var(--surface)); border-color: transparent; }
.hm-2 { background: color-mix(in srgb, var(--gold-accent) 65%, var(--surface)); border-color: transparent; }
.hm-3 { background: var(--gold-accent); border-color: transparent; }
.hm-legend { display: flex; align-items: center; gap: 5px; margin-top: 8px; color: var(--muted); font-size: 12px; }

/* ---------- settings ---------- */
.set-group { margin-bottom: 20px; }
/* Group headings/hints have no surface behind them — see --on-bg above. */
.set-group > h2, .set-group > .hint { color: var(--on-bg); }
.set-row { display: flex; align-items: center; gap: 12px; padding: 12px 14px; background: var(--surface); border: 1px solid var(--border); border-radius: 12px; margin-bottom: 8px; }
.set-row .grow { flex: 1; }
.set-row .lbl { font-weight: 600; }
.set-row .hint { color: var(--muted); font-size: 13px; }
.switch { position: relative; width: 48px; height: 28px; border-radius: 999px; background: var(--surface2); border: 1px solid var(--border); transition: background .15s; flex: none; }
.switch.on { background: var(--accent); border-color: var(--accent); }
.switch::after { content: ""; position: absolute; top: 2px; left: 2px; width: 22px; height: 22px; border-radius: 50%; background: #fff; transition: left .15s; box-shadow: 0 1px 3px rgba(0,0,0,.25); }
.switch.on::after { left: 22px; }

.bg-swatches { display: flex; flex-wrap: wrap; gap: 10px; margin: 4px 0 2px; }
.bg-swatch {
  width: 44px; height: 44px; border-radius: 12px; border: 2px solid var(--border);
  box-shadow: var(--shadow); position: relative; flex: none;
}
.bg-swatch.active { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.bg-swatch.active::after {
  content: "✓"; position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  color: #fff; text-shadow: 0 1px 3px rgba(0,0,0,.6); font-weight: 800; font-size: 16px;
}
#bg-photo-preview { object-fit: cover; height: 90px; width: 140px; }

/* ---------- toasts & modals ---------- */
#toasts { position: fixed; bottom: calc(var(--sab) + 18px); left: 50%; transform: translateX(-50%); z-index: 90; display: flex; flex-direction: column; gap: 8px; align-items: center; width: max-content; max-width: 92vw; }
.toast { background: var(--ink); color: var(--bg); padding: 10px 18px; border-radius: 999px; font-weight: 600; font-size: 14px; box-shadow: var(--shadow); animation: pop .18s ease-out; max-width: 92vw; text-align: center; }
@keyframes pop { from { transform: scale(.9); opacity: 0; } }
.modal-back { position: fixed; inset: 0; background: rgba(10, 8, 20, .5); z-index: 80; display: flex; align-items: center; justify-content: center; padding: 20px; }
.modal { background: var(--surface); border-radius: 18px; padding: 22px; max-width: 440px; width: 100%; box-shadow: var(--shadow); max-height: 82dvh; overflow-y: auto; }
.modal h2 { margin-bottom: 10px; }
.modal .row { display: flex; gap: 10px; margin-top: 16px; flex-wrap: wrap; }
.menu-list { display: flex; flex-direction: column; gap: 4px; }
.menu-list button { text-align: left; padding: 11px 12px; border-radius: 10px; font-weight: 600; }
.menu-list button:active, .menu-list button:hover { background: var(--surface2); }
.menu-list .danger { color: var(--danger); }

/* celebration */
.confetti { position: fixed; inset: 0; pointer-events: none; z-index: 95; overflow: hidden; }
.confetti i { position: absolute; width: 10px; height: 14px; top: -20px; animation: fall linear forwards; }
@keyframes fall { to { transform: translateY(110vh) rotate(720deg); } }

/* Phone-only short copy (game head "Mon 9/22/14" + author; Home "Tue, Jan 4, 1994").
   Hidden here, shown inside the phone block BELOW — same specificity, so these defaults
   must come first in source order or they win over the media query and nothing shows. */
.sol-head .t-short, .sol-head .s-short, #view-home .big-short, #view-home .sub-short { display: none; }
.sol-head .t-short { color: var(--on-bg); font-weight: 800; font-size: 15px; white-space: nowrap; }
.sol-head .s-short { color: color-mix(in srgb, var(--on-bg) 68%, transparent); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Phone: nav fills the topbar edge to edge like a native iOS tab bar, instead
   of a small centered pill group with wasted margin on either side. */
@media (max-width: 620px) {
  #tabs { width: 100%; margin: 0; justify-content: space-between; }
  .tab { flex: 1; text-align: center; padding: 8px 10px; font-size: 14px; }
  /* In a game the tab bar is dead weight on a phone: hide it and let the game's own
     ‹ button (every .sol-head has one) be the way back. The game head then owns the
     notch clearance the topbar used to provide. The version badge sits in that same
     band, so it goes too — §26 wants it on the first page that loads, which this isn't. */
  body.in-game #topbar, body.in-game .app-version { display: none; }
  /* Clearance = inset + min(30px, inset) + 6px: on David's iPhone 16 Pro (inset 62pt) iOS 26
     blurs from the status bar down to ~90pt (measured from his 2026-10-06 screenshot: the
     title at 74-88pt read 183 max luminance against ~234 crisp), so the head starts at 98pt.
     min() with the inset makes the extra 0 wherever there is no notch. env() is written
     straight into the property on purpose: inside a custom property it resolved to 0 on iOS
     (see the ios-status-bar-blur-bleed note). If min() ever fails here the result is 68pt,
     i.e. exactly the previous behavior, never worse. */
  body.in-game #view-solver, body.in-game #view-word, body.in-game #view-jumble {
    padding-top: calc(env(safe-area-inset-top, 0px) + min(30px, env(safe-area-inset-top, 0px)) + 6px);
  }
  /* Home/Archive/Stats/Settings tab bar sits in the same blur band. */
  #topbar { padding-top: calc(env(safe-area-inset-top, 0px) + min(30px, env(safe-area-inset-top, 0px)) + 8px); }
  /* Compact game head: 34px icon buttons, and a short "Mon 9/22/14 / author" meta
     instead of the full masthead title that truncated to "NY… Mond…" beside six buttons. */
  .sol-head { gap: 6px; padding: 2px 2px 6px; }
  .sol-head .iconbtn { min-width: 34px; height: 36px; padding: 0 6px; font-size: 15px; }
  .sol-head .t.has-short, .sol-head .s.has-short { display: none; }
  .sol-head .t-short, .sol-head .s-short { display: block; }
  .clue-bar { min-height: 56px; margin: 6px 0; }
  /* A width-limited 15x15 leaves height to spare: fitGrid hands up to 18px of it to
     each key row; whatever is left centers the grid instead of leaving a hole above the clue bar. */
  .grid-wrap { align-items: center; align-items: safe center; }
  .clue-bar .txt { font-size: 16.5px; }
  /* Home: two cards per row so the four game cards pair up (Wordle | Jumble,
     Continue | Today); chip-heavy cards span both columns. Tighter card density. */
  #view-home .cards { grid-template-columns: 1fr 1fr; gap: 10px; grid-auto-flow: dense; }
  #view-home .card.wide { grid-column: 1 / -1; }
  #view-home .card { padding: 12px; }
  #view-home .card h3 { font-size: 13px; margin-bottom: 6px; }
  #view-home .card .big { font-size: 17px; }
  #view-home .card .sub { font-size: 13px; }
  #view-home .btn { padding: 8px 14px; font-size: 14px; margin-top: 8px; }
  /* Phone-only short copy on the Continue / Today cards ("Tue, Jan 4, 1994" + author)
     — the full masthead title repeated the date and wrapped to four lines in a half-width card. */
  #view-home .has-short { display: none; }
  #view-home .big-short, #view-home .sub-short { display: block; }
  #view-home .chips { gap: 6px; }
  #view-home .chip { padding: 6px 7px; font-size: 13px; }
  #view-home .view, #view-home { padding-top: 12px; }
}
@media (max-width: 480px) {
  .tab { padding: 7px 4px; font-size: 13px; }
}

/* ---------- version badge (index.html, filled from /version.js) ----------
   Top-left, inside the notch (--sat), in the topbar's 8px top padding: 8px line box, so on
   phones it ends where the full-width tab bar begins, and on wider screens the tabs are
   centered away from the corner. Tiny on purpose. Measured, not eyeballed: --muted is only
   4.45:1 on the light page, and the topbar glass lets 15% of a custom background through, so
   it uses --ink: 16.26:1 light on #f7f5ef (11.50:1 even over a black custom background),
   15.19:1 dark on #141517 (9.77:1 over a white one). Failure --danger: 4.99:1 light,
   6.54:1 dark. z-index 110 is above the splash/PIN (100), confetti (95) and modals (80). */
.app-version {
  position: fixed; z-index: 110; pointer-events: none;
  top: var(--sat);
  left: calc(var(--sal) + 14px);
  font: 500 9px/8px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  letter-spacing: .2px; color: var(--ink); white-space: nowrap;
}
.app-version.fail { color: var(--danger); font-weight: 700; }
/* Phone: the tab bar moved 30pt down out of the blur band, so the badge moves into the
   space above it (ends at inset + 30, just above the tabs). Must come AFTER the base rule:
   same specificity, so source order decides. */
@media (max-width: 620px) {
  .app-version { top: calc(env(safe-area-inset-top, 0px) + min(22px, env(safe-area-inset-top, 0px))); }
}

/* ---------- iOS Home Screen quirks (neither reproduces in a desktop browser) ----------
   1. Status-bar blur: iOS 26 lays a "scroll edge" blur under the status bar whenever it
      can't sample one flat color at the top edge. An OPAQUE FIXED box touching the top
      edge is reported to stop it (unconfirmed on a device as of 2026-10-06), so this strip
      is belt; the clearance in the phone block above is braces. z 50: above #app content,
      below modals (80), confetti (95), splash/PIN (100) and the badge (110). env() written
      directly, never through a custom property. Height 0 on anything without a notch. */
body::before {
  content: ""; position: fixed; top: 0; left: 0; right: 0; z-index: 50; pointer-events: none;
  height: env(safe-area-inset-top, 0px); background: var(--bg);
}
/* 2. Short layout viewport: a standalone iOS app is laid out 62pt SHORT of the screen
      (layout ends at 812 of 874pt) and iOS paints nothing positioned below it, so the band
      can't be used. html.ios-band (set by measureIosBand() in app.js only when the shortfall
      equals the top inset) drops the home-indicator padding, since the 62pt band below the
      layout already clears the home indicator. Reclaims 34pt for the keys. */
html.ios-band #kb, html.ios-band #wd-kb { padding-bottom: 6px; }
html.ios-band .kb-nav { padding-bottom: 6px; }
html.ios-band .view { padding-bottom: 24px; }
html.ios-band #toasts { bottom: 18px; }
/* An open modal dims the page to #807e82 behind the badge in light, where --ink is 4.41:1;
   pure black is 5.22:1 there (dark: --ink on the dimmed #0f0e12 is 15.99:1). */
:root:not([data-theme="dark"]) body:has(.modal-back) .app-version:not(.fail) { color: #000000; }
