:root {
  --bg: #12142b; --panel: #1c1f3d; --panel2: #262a52; --ink: #eef0ff; --dim: #9aa0cf;
  /* palette lives in the round-two :root below */
  --good: #34d399; --bad: #f87171; --warn-c: #fbbf24; --empty: #0c0e20;
  --bw: clamp(280px, min(calc(100vw - 16px), calc(100dvh - 310px)), 560px);
  --radius: 10px;
}
* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
html, body { margin: 0; background: var(--bg); color: var(--ink); font: 15px/1.3 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; overscroll-behavior: none; overflow-x: hidden; }
body { min-height: 100dvh; display: flex; justify-content: center; }
button { font: inherit; color: inherit; }
#app { width: min(100%, 600px); padding: 6px 8px calc(8px + env(safe-area-inset-bottom)); display: flex; flex-direction: column; gap: 6px; align-items: center; }
#app[hidden], [hidden] { display: none !important; }

.top { width: 100%; display: flex; justify-content: space-between; align-items: center; }
h1 { margin: 0; font-size: 22px; letter-spacing: .5px; font-weight: 800; }
h1 span { color: var(--l3); }
.scores { display: flex; gap: 14px; }
.scores div { text-align: right; display: flex; flex-direction: column; line-height: 1.05; }
small { color: var(--dim); font-size: 10px; letter-spacing: .8px; }
.scores b { font-size: 20px; font-variant-numeric: tabular-nums; }

.daily { width: 100%; background: var(--panel); border-radius: var(--radius); padding: 6px 10px; display: grid; grid-template-columns: auto 1fr auto; gap: 10px; align-items: center; border: 1px solid #2f3466; }
.daily em { color: var(--l3); font-style: normal; font-weight: 700; }
.daily-word { font-weight: 800; letter-spacing: 3px; font-size: 18px; color: var(--l3); }
.daily-def { color: var(--dim); font-size: 12px; }
.badge { background: var(--good); color: #052; font-weight: 800; border-radius: 99px; padding: 2px 10px; font-size: 12px; }
.daily.done .daily-word { color: var(--good); text-decoration: line-through; text-decoration-thickness: 2px; }

.queues { width: 100%; display: flex; gap: 6px; align-items: stretch; }
.qrows { flex: 1; min-width: 0; display: grid; gap: 3px; }
.q { display: flex; align-items: center; gap: 6px; border-radius: 8px; }
.qlabel { font-size: 10px; width: 40px; flex: none; font-weight: 700; line-height: 1.05; }
.qlabel.l2 { color: #b69cff; } .qlabel.l3 { color: var(--l3); }
.qrow { display: flex; gap: 3px; flex: 1; }
.qt { flex: 1; max-width: 34px; aspect-ratio: 1; border-radius: 6px; display: grid; place-items: center; font-weight: 800; font-size: clamp(11px, 3.4vw, 16px); }
.qt.l2 { background: var(--l2); } .qt.l3 { background: var(--l3); color: #2b1d00; }
.qt:first-child { outline: 2px solid #fff8; }
.qt:nth-child(n+7) { opacity: .6; }

/* Re-roll: the dice button, the "pick a row" state and the slot-machine spin */
.rollbtn { flex: none; width: 46px; border-radius: 10px; border: 1px solid #3a3f7a; background: var(--panel2); display: flex; flex-direction: column; align-items: center; justify-content: center; line-height: 1.05; padding: 2px; cursor: pointer; }
.rollbtn .d { font-size: 18px; } .rollbtn b { font-size: 12px; } .rollbtn small { font-size: 8.5px; color: var(--dim); white-space: nowrap; }
.rollbtn.empty { opacity: .55; }
.rollbtn.active { background: var(--l3); border-color: var(--l3); color: #2b1d00; } .rollbtn.active small { color: #2b1d00; }
.q.pick { cursor: pointer; animation: pickpulse .8s ease-in-out infinite alternate; }
@keyframes pickpulse { from { box-shadow: 0 0 0 0 #ffc40000; } to { box-shadow: 0 0 0 2px var(--l3), 0 0 12px #ffc40088; } }
.qt.spin { filter: brightness(1.3); opacity: .8; transform: translateY(-1px); }
.qt.lock { animation: lockin .2s ease-out; }
@keyframes lockin { from { transform: scale(1.4); filter: brightness(1.8); } to { transform: none; } }

.banner { min-height: 24px; font-weight: 700; font-size: 15px; text-align: center; color: var(--dim); }
.banner.good { color: var(--good); } .banner.bad { color: var(--bad); } .banner.info { color: var(--ink); } .banner.warn { color: var(--warn-c); }

.board-wrap { width: var(--bw); height: var(--bw); }
.board { --n: 9; --cell: calc(var(--bw) / var(--n)); position: relative; width: 100%; height: 100%; border-radius: 12px; background: #0a0c1c; touch-action: none; user-select: none; -webkit-user-select: none; outline-offset: 2px; }
.board:focus-visible { outline: 2px solid var(--l3); }
.bgcell { position: absolute; width: var(--cell); height: var(--cell); left: 0; top: 0; transform: translate(calc((var(--x) + var(--ox, 0)) * var(--cell)), calc((var(--y) + var(--oy, 0)) * var(--cell))); padding: 2%; }
.bgcell::before { content: ""; display: block; width: 100%; height: 100%; border-radius: 22%; background: var(--empty); box-shadow: inset 0 2px 5px #000b; }
.bgcell.dest::before { background: #0f3b2c; box-shadow: inset 0 0 0 2px var(--good); }
.bgcell.cursor::before { box-shadow: inset 0 0 0 2px #fff; }

.tile { position: absolute; left: 0; top: 0; width: var(--cell); height: var(--cell); padding: 2%; transform: translate(calc((var(--x) + var(--ox, 0)) * var(--cell)), calc((var(--y) + var(--oy, 0)) * var(--cell))); transition: transform .2s cubic-bezier(.3,.8,.3,1); pointer-events: none; }
.tile .face { position: relative; width: 100%; height: 100%; border-radius: 22%; display: grid; place-items: center; font-weight: 800; font-size: calc(var(--cell) * .5); box-shadow: 0 2px 0 rgba(0,0,0,.35), inset 0 0 0 1px #ffffff22; transition: transform .1s cubic-bezier(.2,1.4,.4,1); }
/* Layer cue: stacked plates. One plate = blue, two = purple, three = gold (thickness is the colour-blind-safe cue).
   These MUST stay above the state rules below. They set box-shadow at the same specificity as .tile.bomb / .hint /
   .nudge, so whichever comes last in this file wins — and when they sat lower down they silently erased the bomb
   ring, the idle nudge and the hint's non-animated fallback, with no test able to see it. */
.tile.l1 .face { background: linear-gradient(160deg, var(--l1), var(--l1d)); box-shadow: 0 3px 0 #0a1f6b, inset 0 2px 0 #ffffff66, inset 0 0 0 1px #ffffff33; }
.tile.l2 .face { background: linear-gradient(160deg, var(--l2), var(--l2d)); box-shadow: 0 3px 0 #3e1190, 0 6px 0 #25095c, inset 0 2px 0 #ffffff66, inset 0 0 0 1px #ffffff33; }
.tile.l3 .face { background: linear-gradient(160deg, var(--l3), var(--l3d)); color: #2b1d00; box-shadow: 0 3px 0 #9a6200, 0 6px 0 #6f4500, 0 9px 0 #452a00, inset 0 2px 0 #ffffffaa, inset 0 0 0 1px #ffffff55; }
/* A tile under the finger only LIFTS. Its colour, its shape and its meaning are drawn by the selection overlay
   (.trail), because a tile already uses box-shadow for its layer plates and the two would fight over it. */
.tile.sel .face, .tile.ok .face, .tile.slide .face { transform: scale(1.07); }
.tile.warn .face { transform: scale(1.03); }
.tile.sel, .tile.ok, .tile.slide, .tile.warn { z-index: 3; }
.tile.hint .face { animation: hint 0.8s ease-in-out infinite alternate; }
.tile.bomb .face { box-shadow: 0 0 0 3px var(--bad), 0 0 18px var(--bad); animation: hint .5s infinite alternate; }
.tile.shake .face { animation: shake .3s; }
.tile.pop .face { animation: pop .32s cubic-bezier(.2,1.6,.4,1); }
@keyframes pop { 0% { transform: scale(.2) rotate(-12deg); filter: brightness(2); } 100% { transform: none; } }
@keyframes hint { to { filter: brightness(1.5); transform: scale(1.06); } }
@keyframes shake { 20% { transform: translateX(-4px); } 50% { transform: translateX(4px); } 80% { transform: translateX(-2px); } }
.shard { position: absolute; left: 0; top: 0; width: calc(var(--cell) * .2); height: calc(var(--cell) * .2); border-radius: 3px; pointer-events: none; z-index: 5; }
.pop-score { position: absolute; z-index: 6; font-weight: 800; font-size: 18px; color: #fff; text-shadow: 0 2px 6px #000; pointer-events: none; }
.finger { position: absolute; z-index: 8; left: 0; top: 0; width: 26px; height: 26px; margin: -13px 0 0 -13px; border-radius: 50%; background: #fff9; border: 3px solid #fff; pointer-events: none; }

.actionbar { width: 100%; display: flex; gap: 8px; align-items: center; background: var(--panel); border-radius: var(--radius); padding: 6px 10px; }
.actionbar span { flex: 1; font-weight: 700; }
.stats { width: 100%; }
.bar { height: 8px; background: var(--panel); border-radius: 99px; overflow: hidden; }
.bar i { display: block; height: 100%; width: 0; background: linear-gradient(90deg, var(--l1), var(--l2), var(--l3)); transition: width .4s; }
.statline { display: flex; justify-content: space-between; color: var(--dim); font-size: 12px; margin-top: 3px; }
.statline b { color: var(--ink); }

.controls { width: 100%; display: grid; grid-template-columns: repeat(5, 1fr); gap: 6px; }
.btn { background: var(--panel2); border: 1px solid #3a3f7a; border-radius: var(--radius); padding: 8px 10px; cursor: pointer; min-height: 44px; font-weight: 700; }
.btn:hover { filter: brightness(1.15); } .btn:disabled { opacity: .4; cursor: not-allowed; }
.btn.primary { background: var(--l1); border-color: var(--l1); }
.btn.danger { background: #7f1d1d; border-color: #b91c1c; }
.btn.armed { background: var(--bad); color: #300; }
.btn.tool { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 1px; padding: 4px 2px; font-size: 11px; line-height: 1.1; text-align: center; }
.btn.tool .ico { font-size: 18px; }
.btn.tool.active { background: var(--bad); border-color: var(--bad); color: #300; }

.loading { position: fixed; inset: 0; display: grid; place-items: center; background: var(--bg); font-size: 18px; color: var(--dim); }
.toast { position: fixed; left: 50%; bottom: 90px; transform: translate(-50%, 20px); background: #000d; color: #fff; padding: 8px 14px; border-radius: 99px; opacity: 0; transition: .25s; pointer-events: none; z-index: 60; max-width: 90vw; text-align: center; }
.toast.show { opacity: 1; transform: translate(-50%, 0); }

.modal { position: fixed; inset: 0; background: #000b; display: grid; place-items: center; z-index: 50; padding: 12px; overflow: auto; }
.sheet { background: var(--panel); border: 1px solid #3a3f7a; border-radius: 16px; padding: 16px; width: min(100%, 440px); max-height: 94dvh; overflow: auto; display: flex; flex-direction: column; gap: 10px; }
.sheet h2 { margin: 0; font-size: 20px; }
.sheet p { margin: 0; color: var(--dim); }
.row { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
.row > .btn { flex: 1; }
.kv { display: grid; grid-template-columns: 1fr auto; gap: 4px 12px; }
.kv span:nth-child(odd) { color: var(--dim); } .kv span:nth-child(even) { text-align: right; font-weight: 700; }
.toggle { display: flex; justify-content: space-between; align-items: center; padding: 6px 0; }
.toggle.vol input[type=range] { width: 55%; accent-color: var(--l3); } .toggle.vol b { color: var(--l3); font-variant-numeric: tabular-nums; }
.sheet input[type=text], .sheet input[type=number], .sheet textarea { width: 100%; background: var(--empty); color: var(--ink); border: 1px solid #3a3f7a; border-radius: 8px; padding: 8px; font: 13px ui-monospace, monospace; }
.sheet textarea { min-height: 80px; }
.big { font-size: 38px; font-weight: 800; text-align: center; color: var(--l3); }
.adbar { height: 10px; background: var(--empty); border-radius: 99px; overflow: hidden; }
.adbar i { display: block; height: 100%; width: 0; background: var(--good); }
.tut-board { --bw: min(66vw, 260px); width: var(--bw); height: var(--bw); margin: 0 auto; }
.tut-caption { min-height: 66px; font-weight: 700; text-align: center; }
.dots { display: flex; gap: 6px; justify-content: center; } .dots i { width: 8px; height: 8px; border-radius: 50%; background: #3a3f7a; } .dots i.on { background: var(--l3); }
.wordlist { max-height: 120px; overflow: auto; font: 12px ui-monospace, monospace; color: var(--dim); }
.confetti { position: fixed; top: -10px; width: 10px; height: 14px; z-index: 47; pointer-events: none; }

@media (max-height: 640px) { .daily-def { display: none; } .daily { grid-template-columns: 1fr auto; } }

.brand { display: flex; align-items: center; gap: 10px; }
.chip { background: var(--panel); border: 1px solid #2f3466; border-radius: 99px; padding: 3px 10px 3px 8px; display: flex; flex-direction: column; gap: 2px; cursor: pointer; min-width: 52px; }
.chip b { font-size: 11px; color: var(--ink); line-height: 1; } .chip i { display: block; height: 4px; border-radius: 9px; background: #0c0e20; overflow: hidden; } .chip u { display: block; height: 100%; width: 0; background: var(--l3); transition: width .4s; }
.missions { width: 100%; display: flex; gap: 6px; }
.mission { flex: 1; background: var(--panel); border: 1px solid #2a2f5e; border-radius: 8px; padding: 3px 7px; font-size: 11px; line-height: 1.15; color: var(--dim); display: flex; align-items: center; gap: 5px; min-height: 30px; }
.mission.t1 b { color: #7dd3fc; } .mission.t2 b { color: #c4b5fd; } .mission.t3 b { color: var(--l3); }
.mission.done { color: var(--good); border-color: var(--good); text-decoration: line-through; }
.mission b { font-size: 10px; }
.qrow .qt.seg { box-shadow: 0 3px 0 -1px #fff; }
.qrow .qt.seg-start { margin-left: 4px; }
.tile.nudge .face { animation: hint .7s ease-in-out infinite alternate; box-shadow: 0 0 0 2px #fff7; }
.callout { position: absolute; left: 50%; top: 42%; transform: translate(-50%, -50%); font-size: 34px; font-weight: 900; letter-spacing: 2px; color: #fff; text-shadow: 0 3px 0 #0008, 0 0 18px var(--l3); pointer-events: none; z-index: 9; white-space: nowrap; }
.board.shake { animation: bshake .35s; }
@keyframes bshake { 20% { transform: translate(-3px, 2px); } 50% { transform: translate(3px, -2px); } 80% { transform: translate(-2px, 1px); } }
.stars { font-size: 30px; text-align: center; letter-spacing: 4px; } .stars .off { opacity: .25; }
.trophy { display: flex; gap: 8px; align-items: center; padding: 4px 0; border-bottom: 1px solid #2a2f5e; } .trophy.locked { opacity: .4; } .trophy span:first-child { font-size: 20px; width: 26px; text-align: center; }
.cal { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; } .cal i { aspect-ratio: 1; border-radius: 5px; background: #0c0e20; } .cal i.on { background: var(--good); } .cal i.today { outline: 2px solid var(--l3); }
.tag { font-size: 10px; letter-spacing: 1px; color: var(--l3); font-weight: 800; }
.btn.big { padding: 12px; font-size: 16px; }
@media (max-height: 700px) { .missions { display: none; } }

.mchip { padding: 5px 9px; } .mchip b { font-size: 12px; }
.brand { flex-wrap: nowrap; } h1 { font-size: 20px; }

.starsnow { font-style: normal; color: var(--l3); letter-spacing: 1px; margin-left: 4px; }
.bar { position: relative; } .bar::after { content: ''; position: absolute; inset: 0; background: linear-gradient(90deg, transparent calc(15% - 1px), #0008 calc(15% - 1px) 15%, transparent 15% calc(30% - 1px), #0008 calc(30% - 1px) 30%, transparent 30% calc(50% - 1px), #0008 calc(50% - 1px) 50%, transparent 50% calc(70% - 1px), #0008 calc(70% - 1px) 70%, transparent 70% calc(88% - 1px), #0008 calc(88% - 1px) 88%, transparent 88%); pointer-events: none; }

@media (min-height: 701px) { :root { --bw: clamp(280px, min(calc(100vw - 16px), calc(100dvh - 352px)), 560px); } }

/* ---- launch menu (two modes) ---- */
/* Above the run's own effect layers (.fly-score 45, .burst-canvas 46, .confetti 47) and below the surfaces the
   home screen itself raises (.modal 50 for its dialogs, .toast 60 for "Result copied" from its Share button). At
   40 it sat under all three effects, so finishing a run and taking "Main menu" while a burst or the daily-word
   confetti was still in flight rained the last run's particles down the front of the menu. */
.home { position: fixed; inset: 0; z-index: 48; background: var(--bg); overflow: auto; display: flex; justify-content: center; padding: calc(10px + env(safe-area-inset-top)) 12px calc(14px + env(safe-area-inset-bottom)); }
.home-in { width: min(100%, 460px); display: flex; flex-direction: column; gap: 12px; }
.home-title { text-align: center; font-size: 28px; margin: 4px 0 0; }
.home-title span { color: var(--l3); }
.home-sub { margin: 0; text-align: center; color: var(--dim); }
.mode { background: var(--panel); border: 1px solid #3a3f7a; border-radius: 16px; padding: 14px; display: flex; flex-direction: column; gap: 10px; }
.mode.last { border-color: var(--l1); box-shadow: 0 0 0 1px var(--l1); }
.mode header { display: flex; align-items: center; gap: 8px; }
.mode h2 { margin: 0; font-size: 19px; flex: 1; }
.mode .mi { font-size: 22px; }
.mode .tag { font-size: 11px; font-weight: 700; padding: 3px 8px; border-radius: 99px; background: var(--panel2); color: var(--dim); white-space: nowrap; }
.wotd { background: var(--empty); border-radius: 12px; padding: 8px 12px; display: flex; flex-direction: column; gap: 2px; }
.wotd small { color: var(--l3); font-weight: 800; letter-spacing: .6px; font-size: 11px; }
.wotd b { font-size: 24px; letter-spacing: 2px; }
.wotd span { color: var(--dim); font-size: 13px; }
.facts { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; font-size: 14px; }
.facts li { display: flex; gap: 8px; align-items: flex-start; }
.facts i { font-style: normal; font-weight: 800; min-width: 1.2em; text-align: center; }
.facts i.y { color: var(--good); } .facts i.n { color: var(--bad); }
.mode .note { margin: 0; color: var(--dim); font-size: 13px; }
.mode .result { background: var(--empty); border-radius: 12px; padding: 8px 12px; display: flex; flex-direction: column; gap: 2px; }
.mode .result.win b { color: var(--good); }
.mode .result span { color: var(--dim); font-size: 13px; }
.mode .row { display: flex; gap: 8px; } .mode .row > .btn { flex: 1; }
.btn.big { min-height: 50px; font-size: 16px; color: #fff; }
.home-links { display: flex; flex-wrap: wrap; justify-content: center; gap: 4px 14px; padding: 2px 0 6px; }
.link { background: none; border: none; color: var(--dim); text-decoration: underline; padding: 8px 4px; cursor: pointer; min-height: 40px; }
.link:hover { color: var(--ink); }

/* ---- selection overlay: the one thing that says what a gesture is doing ----
   A rounded ribbon through the cells under the finger (--rib wide, set from the cell size), with a thin bright
   edge on top. It sits above the tiles but stays translucent, so the word you are tracing is still readable.
   Used by both the straight-line game and the chain version. Colour is the only thing that changes per state. */
.trail { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; z-index: 4; overflow: visible; }
.trail polyline { fill: none; transition: stroke .12s linear; }
.trail .ribbon { stroke: #ffffff; stroke-width: var(--rib, 34px); opacity: .22; }
.trail .edge { stroke: #ffffff; stroke-width: 3px; opacity: .85; }
.trail[data-state="ready"] .ribbon { stroke: var(--good); opacity: .34; }
.trail[data-state="ready"] .edge { stroke: var(--good); opacity: 1; }
.trail[data-state="slide"] .ribbon { stroke: #7dd3fc; opacity: .26; }
.trail[data-state="slide"] .edge { stroke: #7dd3fc; stroke-dasharray: 2 10; }
.trail[data-state="warn"] .ribbon { stroke: var(--warn-c); opacity: .34; }
.trail[data-state="warn"] .edge { stroke: var(--warn-c); }
@media (prefers-reduced-motion: reduce) { .trail polyline { transition: none; } }
.rulestag { margin-left: 6px; font-size: 11px; font-weight: 800; padding: 2px 8px; border-radius: 99px; background: var(--panel2); color: var(--l3); vertical-align: middle; }
.rules-banner { margin: 0; padding: 8px 12px; border-radius: 12px; background: var(--panel2); border: 1px solid var(--l3); font-size: 13px; color: var(--ink); }
a.link { display: inline-flex; align-items: center; }
.trail[data-state="slide"] polyline { stroke: #7dd3fc; stroke-dasharray: 2 10; }

.privacy { margin: 0; text-align: center; color: var(--dim); font-size: 12px; }
.privacy .link { min-height: 28px; padding: 2px 6px; }

/* ---- hint: a shining box-ring (casino treasure style) around every hinted cell; it stays until the player acts ---- */
.tile.hint { z-index: 3; }
.tile.hint .face { animation: hintring 1s ease-in-out infinite alternate; box-shadow: 0 0 0 3px #fff3b0, 0 0 0 6px #ffb400, 0 0 18px 8px #ffc83dcc, inset 0 0 10px #fff9; }
@keyframes hintring {
  from { box-shadow: 0 0 0 3px #fff3b0, 0 0 0 6px #ffb400, 0 0 10px 4px #ffc83d99, inset 0 0 6px #fff6; filter: brightness(1.05); }
  to   { box-shadow: 0 0 0 3px #ffffff, 0 0 0 7px #ffd54a, 0 0 26px 12px #ffd54aee, inset 0 0 14px #fffb; filter: brightness(1.3); }
}
.tile.hint::after { content: '✦'; position: absolute; right: -3px; top: -5px; font-size: 14px; line-height: 1; color: #fff7c2; text-shadow: 0 0 6px #ffb400, 0 0 2px #fff; animation: twinkle .9s ease-in-out infinite alternate; pointer-events: none; }
@keyframes twinkle { from { opacity: .35; transform: scale(.8) rotate(0deg); } to { opacity: 1; transform: scale(1.25) rotate(25deg); } }

/* ---- combo strip: timer ring, level, slot reels, last win (above the board, never over it, never blocks input) ---- */
.combostrip { width: 100%; display: flex; align-items: center; gap: 10px; padding: 3px 8px; background: var(--panel); border: 1px solid #2f3466; border-radius: 12px; min-height: 46px; pointer-events: none; }
.ringwrap { position: relative; width: 38px; height: 38px; flex: none; display: grid; place-items: center; }
.ring { position: absolute; inset: 0; width: 100%; height: 100%; transform: rotate(-90deg); }
.ring circle { fill: none; stroke-width: 4; }
.ring .bg { stroke: #2f3466; }
.ring .arc { stroke: var(--l3); stroke-linecap: round; stroke-dasharray: 119.4; stroke-dashoffset: 119.4; }
.combostrip.hot .arc { stroke: var(--bad); }
.ringwrap b { font-size: 12px; position: relative; }
.reels { display: flex; gap: 4px; flex: none; }
.reel { font-style: normal; width: 32px; height: 32px; display: grid; place-items: center; font-size: 19px; background: var(--empty); border: 1px solid #3a3f7a; border-radius: 8px; overflow: hidden; }
.reel.spin { animation: reelspin .12s linear infinite; filter: blur(.6px); }
@keyframes reelspin { from { transform: translateY(-30%); } to { transform: translateY(30%); } }
.combostrip.win { border-color: var(--l3); box-shadow: 0 0 14px #ffc40088; }
.combostrip.win .reel { border-color: var(--l3); background: #3a2a00; }
.combonote { flex: 1; font-size: 12px; color: var(--dim); min-width: 0; }
.combostrip.win .combonote { color: var(--l3); font-weight: 700; }
body.has-combo { --bw: clamp(280px, min(calc(100vw - 16px), calc(100dvh - 366px)), 560px); }
@media (prefers-reduced-motion: reduce) { .reel.spin, .tile.hint::after { animation: none; } }

/* =====================================================================================================================
   Round 2 (2026-11-10): cleaner HUD, big timer pie, celebration strip, stacked-plate layers, queue highlights, pause
   ===================================================================================================================== */
:root { --l1: #2f6bff; --l1d: #1535b0; --l2: #a24dff; --l2d: #5a1bbd; --l3: #ffc400; --l3d: #c48600; }

/* ---- top HUD: score, gap meter, timer pie, streak ---- */
.hud { width: 100%; display: grid; grid-template-columns: 1fr 1.15fr 64px 1fr; gap: 8px; align-items: center; }
.hstat { display: flex; flex-direction: column; line-height: 1.05; min-width: 0; }
.hstat small { font-size: 10px; letter-spacing: .6px; color: var(--dim); font-weight: 700; text-transform: uppercase; }
.hstat b { font-size: 22px; font-variant-numeric: tabular-nums; }
.hstat.streakstat { text-align: right; align-items: flex-end; }
.hstat.streakstat span { font-size: 11px; color: var(--dim); }
.hstat .bar { height: 6px; margin-top: 3px; border-radius: 9px; background: #0c0e20; overflow: hidden; }
.hstat .bar i { display: block; height: 100%; width: 0; background: linear-gradient(90deg, var(--l1), var(--l2), var(--l3)); transition: width .3s; }
.timerpie { position: relative; width: 64px; height: 64px; justify-self: center; }
.timerpie svg { width: 100%; height: 100%; display: block; }
.timerpie .pbg { fill: #0c0e20; stroke: #2f3466; stroke-width: .08; }
.timerpie .pslice { fill: var(--l3); stroke: #fff8; stroke-width: .03; }
.timerpie b { position: absolute; inset: 0; display: grid; place-items: center; font-size: 20px; color: #2b1d00; text-shadow: 0 0 4px #fff8; pointer-events: none; }
.timerpie.off { opacity: .35; }
/* Near the end it shakes, blinks and flashes, harder as it runs down (--warn goes 0 to 1). main.js quantises --warn to
   fifths so it does not rewrite the value every frame, so the first non-zero step lands at 4.5 s left, not 5 s. */
.timerpie.warn { animation: pieshake calc(.5s - .34s * var(--warn, 0)) linear infinite; }
.timerpie.warn .pslice { animation: pieflash calc(.7s - .36s * var(--warn, 0)) steps(2, jump-none) infinite; }
@keyframes pieshake { 0% { transform: translate(0, 0) rotate(0); } 25% { transform: translate(calc(2px + 3px * var(--warn, 0)), 1px) rotate(3deg); } 50% { transform: translate(calc(-2px - 3px * var(--warn, 0)), -1px) rotate(-3deg); } 75% { transform: translate(1px, calc(2px * var(--warn, 0))) rotate(2deg); } 100% { transform: translate(0, 0) rotate(0); } }
@keyframes pieflash { 0% { fill: var(--l3); } 100% { fill: #ff4d4d; filter: brightness(1.5); } }
@media (prefers-reduced-motion: reduce) { .timerpie.warn, .timerpie.warn .pslice { animation: none; } .timerpie.warn .pslice { fill: #ff4d4d; } }
body.reduce-motion .timerpie.warn, body.reduce-motion .timerpie.warn .pslice { animation: none; }

/* ---- celebration strip: a slot-machine-WIN look (lights, coins, a big number), never over the board, never blocking input ---- */
/* No overflow clip: the coins fly up to 99px and sideways up to 176px, and a 50px strip with overflow:hidden ate
   almost all of a 20-coin burst. The gradient is painted by ::before so the rounded corners still look clipped. */
/* `isolation: isolate` is load-bearing: without a stacking context here the z-index:-1 ::before joins the ROOT
   negative layer and the opaque body background paints straight over it, so the strip loses its plate entirely. */
.celebrate { position: relative; isolation: isolate; width: 100%; min-height: 50px; display: flex; align-items: center; gap: 10px; padding: 4px 12px; border-radius: 14px; border: 1px solid #3a3f7a; pointer-events: none; }
.celebrate::before { content: ''; position: absolute; inset: -1px; border-radius: inherit; background: linear-gradient(180deg, #2a1f4d, #181434); z-index: -1; }
.celebrate.t3::before, .celebrate.t4::before { background: linear-gradient(180deg, #5a2a00, #2b1300); }
.celebrate.echo:not(.t3):not(.t4)::before { background: linear-gradient(180deg, #4d2a1f, #2a1410); }
.celebrate .lights { display: flex; flex-direction: column; justify-content: space-between; align-self: stretch; padding: 3px 0; }
.celebrate .lights i { display: block; width: 7px; height: 7px; border-radius: 50%; background: #5b4d2a; }
.celebrate .celtext { display: flex; align-items: baseline; flex-wrap: wrap; column-gap: 10px; row-gap: 0; min-width: 0; flex: 1; }
.celebrate .celtext b { font-size: 28px; color: var(--l3); font-variant-numeric: tabular-nums; text-shadow: 0 0 10px #ffb40088; }
.celebrate .celtext span { font-size: 12px; color: var(--dim); line-height: 1.15; }
.celebrate.on .celtext span { color: #fff; font-weight: 800; font-size: 14px; }
/* The bulbs and the border pulse used to fire on EVERY word, so a 30-point three-letter word lit the strip exactly
   as hard as a jackpot, hundreds of times a run. They now start at tier 2; tier 1 gets the number and nothing else. */
.celebrate.on { border-color: var(--l3); }
.celebrate.on:not(.t1) { animation: celpulse .5s ease-in-out 3; }
.celebrate.on:not(.t1) .lights i { background: #ffd54a; animation: bulb .34s steps(2, jump-none) infinite; }
.celebrate.on:not(.t1) .lights i:nth-child(even) { animation-delay: .17s; }
.celebrate.t2 .celtext b { font-size: 34px; } .celebrate.t3 .celtext b { font-size: 38px; }
/* Above the top threshold the tier stops moving but the score does not, so --cel-over (0 at 900 points, 1 at
   14,400+) keeps the number growing. Without it 720 and 262,080 points rendered identically. */
.celebrate.t4 .celtext b { font-size: calc(42px + 14px * var(--cel-over, 0)); color: #fff; text-shadow: 0 0 14px #ffd54a, 0 0 28px #ff9a00; }
@keyframes bulb { from { opacity: .25; } to { opacity: 1; box-shadow: 0 0 6px #ffd54a; } }
@keyframes celpulse { from { box-shadow: 0 0 0 0 #ffd54a00; } 50% { box-shadow: 0 0 22px 2px #ffd54aaa; } to { box-shadow: 0 0 0 0 #ffd54a00; } }
.coins { position: absolute; inset: 0; pointer-events: none; }
.coin { position: absolute; left: 50%; bottom: 8px; width: 16px; height: 16px; margin-left: -8px; border-radius: 50%; background: radial-gradient(circle at 35% 30%, #fff6b0, #ffc400 55%, #b07f00); border: 1px solid #7a5200; box-shadow: 0 1px 3px #0008; animation: coinfly 1.1s cubic-bezier(.2,.7,.3,1) forwards; animation-delay: var(--dl, 0ms); opacity: 0; }
@keyframes coinfly { 0% { opacity: 0; transform: translate(0, 0) rotate(0); } 12% { opacity: 1; } 60% { opacity: 1; } 100% { opacity: 0; transform: translate(var(--dx), var(--dy)) rotate(var(--rot)); } }
@media (prefers-reduced-motion: reduce) { .celebrate.on, .celebrate.on .lights i, .coin { animation: none; } .coin { display: none; } }
body.reduce-motion .celebrate.on, body.reduce-motion .celebrate.on .lights i { animation: none; } body.reduce-motion .coin { display: none; }

/* ---- bigger queue letters, and the ones the dragged word will use, coloured by the layer they replace ---- */
.qt { max-width: 38px; font-size: clamp(14px, 4.4vw, 19px); }
.qlabel { width: 38px; }
.qt.use { transform: translateY(-2px) scale(1.08); box-shadow: 0 0 0 3px var(--ringc, #fff), 0 0 12px var(--ringc, #fff); z-index: 1; }
.qt.use.u1 { --ringc: var(--l1); } .qt.use.u2 { --ringc: var(--l2); }

/* ---- room for the stacked plates. The plate rules themselves live up next to `.tile .face` (search "Layer cue: stacked
   plates"), because they must stay ABOVE the state rules to win the cascade; only the spacing is left here ---- */
.tile { padding: calc(var(--cell) * .07) calc(var(--cell) * .07) calc(var(--cell) * .15); } /* room under each tile for its plates (percent padding would be a share of the board's width) */
.bgcell { padding: calc(var(--cell) * .07) calc(var(--cell) * .07) calc(var(--cell) * .15); }

/* The letters a word will reveal are shown in the queue at the top, not as ghosts over the board: a ghost on the
   tile covered the very word the player was tracing (removed 2026-11-10). See .qt.use for the queue highlight. */

/* A gap lights up only when the finger is on it (.dest). Lighting every reachable gap at once was removed on
   2026-11-11: cells the player was nowhere near appeared to respond, and the whole board pulsed during a slide. */
.bgcell.dest::before { background: #14634a; box-shadow: inset 0 0 0 3px #fff, 0 0 14px var(--good); }

/* ---- hint ring: glow strength rises as the timer runs down (--hint-k 0.3 to 1, set by the UI) ---- */
.tile.hint .face { animation: hintring 1s ease-in-out infinite alternate; }
@keyframes hintring {
  from { box-shadow: 0 0 0 3px #fff3b0, 0 0 0 6px #ffb400, 0 0 calc(8px + var(--hint-k, .3) * 12px) calc(3px + var(--hint-k, .3) * 6px) #ffc83d99, inset 0 0 6px #fff6; filter: brightness(calc(1.0 + var(--hint-k, .3) * .15)); }
  to   { box-shadow: 0 0 0 3px #ffffff, 0 0 0 calc(6px + var(--hint-k, .3) * 3px) #ffd54a, 0 0 calc(18px + var(--hint-k, .3) * 26px) calc(8px + var(--hint-k, .3) * 16px) #ffd54aee, inset 0 0 14px #fffb; filter: brightness(calc(1.1 + var(--hint-k, .3) * .4)); }
}

/* ---- pause ---- */
.board-wrap { position: relative; }
.pausecover { position: absolute; inset: 0; z-index: 30; display: grid; place-items: center; text-align: center; border-radius: 14px; background: #0b0d22cc; backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px); }
.pausecover div { display: flex; flex-direction: column; gap: 10px; align-items: center; padding: 16px; }
.pausecover b { font-size: 26px; } .pausecover span { color: var(--dim); font-size: 13px; }
/* Armed and paused are different kinds of "on" and must not look the same. A bomb waiting for its target is a
   loaded, destructive state and reads red, with a slow breath so it is obvious the next tap will blow something
   up; Pause is merely engaged and stays the calm blue. Both were --l1 before, so an armed bomb was
   indistinguishable from a paused game. */
.btn.tool.active { background: var(--l1); border-color: var(--l1); color: #fff; }
#bombBtn.active { background: var(--bad); border-color: #fff6; color: #3a0000; animation: armed 1.1s ease-in-out infinite; }
@keyframes armed { 50% { box-shadow: 0 0 0 3px #f8717155, 0 0 16px #f87171aa; } }
body.has-streak { --bw: clamp(280px, min(calc(100vw - 16px), calc(100dvh - 392px)), 560px); }
body:not(.has-streak) .hud .timerpie, body:not(.has-streak) .hud .streakstat { visibility: hidden; }

/* ---- board compression (src/engine/compress.js; flag config.compress). The board is a camera over the playing field: --n cells across, the field centred by --ox/--oy ---- */
@property --n { syntax: '<number>'; inherits: true; initial-value: 8; }
@property --ox { syntax: '<number>'; inherits: true; initial-value: 0; }
@property --oy { syntax: '<number>'; inherits: true; initial-value: 0; }
.board { transition: --n .5s ease, --ox .5s ease, --oy .5s ease, box-shadow .25s; }
.board.snap { transition: none; }
.board.cam .tile { transition: none; } /* the camera moves every tile at once: no per-tile easing on top of it */
.bgcell.off { display: none; }
.board.cmpedge { box-shadow: 0 0 0 2px #ffd54a, 0 0 26px #ffd54acc; }
.cmpflash { position: absolute; left: 0; top: 0; width: calc(var(--fw) * var(--cell)); height: calc(var(--fh) * var(--cell)); z-index: 4; pointer-events: none; border-radius: 6px;
  transform: translate(calc((var(--fx) + var(--ox, 0)) * var(--cell)), calc((var(--fy) + var(--oy, 0)) * var(--cell)));
  background: linear-gradient(90deg, transparent, #ffd54a99, transparent), repeating-linear-gradient(0deg, #ffd54a33 0 3px, transparent 3px 6px);
  box-shadow: inset 0 0 0 2px #ffd54a, 0 0 18px 2px #ffd54acc; animation: cmpflash .85s ease-out forwards; }
@keyframes cmpflash { 0% { opacity: 0; } 18% { opacity: 1; } 60% { opacity: .85; } 100% { opacity: 0; } }
body.reduce-motion .cmpflash { display: none; }


/* ======================================================================================================================
   POP (prototype flag): body.pop is set by applySettings() when the flag is on and Reduce Motion is off.
   Cost rules: transform and opacity only; no animated box-shadow, filter or blur. Design: UI research, section 4.7.
   Flash safety: nothing here repeats faster than 3 per second (a test reads this file to check the periods).
   ====================================================================================================================== */

/* pop-ups: 140 ms in, 90 ms out */
body.pop .modal { animation: popFade .12s ease-out; }
body.pop .modal .sheet { animation: sheetIn .14s cubic-bezier(.2,.9,.3,1.15); }
body.pop .modal.closing { animation: popFadeOut .09s ease-in forwards; pointer-events: none; }
body.pop .modal.closing .sheet { animation: sheetOut .09s ease-in forwards; }
@keyframes popFade { from { opacity: 0; } }
@keyframes popFadeOut { to { opacity: 0; } }
@keyframes sheetIn { from { transform: scale(.94); opacity: 0; } }
@keyframes sheetOut { to { transform: scale(.96); opacity: 0; } }

/* toast: slides up 12 px with a small overshoot */
body.pop .toast { transform: translate(-50%, 12px); transition: opacity .16s ease-out, transform .16s cubic-bezier(.2,1.5,.4,1); }
body.pop .toast.show { transform: translate(-50%, 0); }

/* buttons: a 60 ms press squash, a springy release */
body.pop .btn, body.pop .rollbtn { transition: transform .12s cubic-bezier(.3,1.6,.5,1); }
body.pop .btn:active:not(:disabled), body.pop .rollbtn:active { transform: scale(.96); transition-duration: .06s; }

/* tiles that only lose a layer: transform-only pop, staggered along the word by --d (replaces the filter: brightness pop) */
body.pop .tile.pop .face { animation: pop2 .22s cubic-bezier(.2,1.5,.4,1) var(--d, 0ms) backwards; }
@keyframes pop2 { 0% { transform: scale(.55); } 60% { transform: scale(1.1); } 100% { transform: none; } }

/* score text that flies to the HUD, HUD counter pulse, jackpot canvas, bomb ring */
.fly-score { position: fixed; z-index: 45; font-weight: 800; color: #fff; text-shadow: 0 2px 6px #000; pointer-events: none; white-space: nowrap; will-change: transform, opacity; }
body.pop .hstat.score b { font-variant-numeric: tabular-nums; display: inline-block; }
body.pop .hstat.score b.bump { animation: scoreBump .14s ease-out; }
@keyframes scoreBump { 50% { transform: scale(1.14); } }
.burst-canvas { position: fixed; z-index: 46; pointer-events: none; }
/* A burst fired from INSIDE a dialog has to climb over it, and the static band cannot say that. Three rules are all
   true at once and they form a cycle: the effects must paint above .modal (50) or a burst centred on the result
   card's score is invisible behind an opaque sheet; .home (48) must paint above the effects so a burst still in
   flight does not rain down the launch menu (that regression is written up above .home); and .modal must paint above
   .home for the menu's own dialogs. No single number satisfies all three, so the lift is conditional: only while a
   dialog is actually open, and 51 is above .modal while staying under .toast (60), which has to stay readable.
   Of the three canvasBurst callers only the personal-best one (runTally) fires with a dialog open; the jackpot and
   the Word-of-the-Day confetti fire over the board mid-run, where :has(.modal) is false and nothing changes.
   Measured on the real card, real run, burst frozen at birth and stepped 50 ms at a time: 6 of 987 particle pixels
   reached the screen before and the whole-view diff was exactly 0.000000 at every step of the first 500 ms. */
body:has(.modal) .burst-canvas { z-index: 51; }
.bomb-ring { position: absolute; z-index: 5; box-sizing: border-box; border: 3px solid #ffb347; border-radius: 50%; pointer-events: none; }

/* opacity-pulse rings: the same look as the old animated box-shadow and filter, but compositor-only (the glow is painted once, only its opacity moves) */
body.pop .tile.hint .face { animation: none; box-shadow: 0 0 0 3px #fff3b0, 0 0 0 6px #ffb400, inset 0 0 10px #fff9; filter: none; }
body.pop .tile.hint::before, body.pop .tile.nudge::before, body.pop .tile.bomb::before { content: ''; position: absolute; inset: 7% 7% 15% 7%; border-radius: 22%; pointer-events: none; animation: glowPulse 1s ease-in-out infinite alternate; }
body.pop .tile.hint::before { box-shadow: 0 0 18px 8px #ffc83dcc; }
body.pop .tile.nudge .face { animation: none; filter: none; box-shadow: 0 0 0 2px #fff7; }
body.pop .tile.nudge::before { box-shadow: 0 0 14px 5px #ffffff88; }
body.pop .tile.bomb .face { animation: none; filter: none; box-shadow: 0 0 0 3px var(--bad); }
body.pop .tile.bomb::before { box-shadow: 0 0 18px 7px var(--bad); animation-duration: .6s; }
@keyframes glowPulse { from { opacity: .3; } to { opacity: 1; } }

/* pause cover: a flat dim instead of a 14 px backdrop blur */
body.pop .pausecover { backdrop-filter: none; -webkit-backdrop-filter: none; background: #0b0d22e6; }

/* score chain: the strip's number ticks once per term (transform only) */
body.pop .celtext b.tick { display: inline-block; animation: celTick .09s ease-out; }
@keyframes celTick { 50% { transform: scale(1.12); } }

/* run-end tally: rows slide in one at a time, the big number rolls, stars pop (transform and opacity only) */
body.pop .tally { display: flex; flex-direction: column; gap: 2px; margin: 0 0 6px; padding: 6px 10px; background: #0b0d22; border-radius: 10px; }
body.pop .trow { display: flex; justify-content: space-between; font-size: 14px; opacity: 0; transform: translateY(6px); }
body.pop .trow.in { opacity: 1; transform: none; transition: opacity .12s ease-out, transform .12s ease-out; }
body.pop .trow b { font-variant-numeric: tabular-nums; color: var(--good); } body.pop .trow.neg b { color: var(--bad); }
body.pop .stars .st { display: inline-block; font-style: normal; opacity: 0; transform: scale(0); }
body.pop .stars .st.in { animation: starPop .25s cubic-bezier(.2,1.6,.4,1) forwards; }
@keyframes starPop { from { transform: scale(0); opacity: 0; } 60% { transform: scale(1.3); opacity: 1; } to { transform: scale(1); opacity: 1; } }
body.pop .big { font-variant-numeric: tabular-nums; }
body.pop .big.flourish { animation: bigFlourish .3s ease-out; }
@keyframes bigFlourish { 50% { transform: scale(1.18); } }

/* streak flame and star pop (transform and opacity only; nothing blinks) */
body.pop .hstat.streakstat, body.pop .hstat.gapstat { position: relative; }
body.pop #streakNow { display: inline-block; }
body.pop #streakNow.bump { animation: scoreBump .14s ease-out; }
.flame { position: absolute; left: 0; top: 2px; pointer-events: none; transform-origin: 50% 85%; line-height: 1; opacity: 0; z-index: 8; }
.star-pop { position: absolute; left: 50%; top: calc(100% + 2px); z-index: 20; display: flex; gap: 2px; padding: 3px 8px; background: #0b0d22ee; border: 1px solid var(--l3); border-radius: 99px; font-size: 16px; line-height: 1; color: var(--l3); pointer-events: none; opacity: 0; transform: translateX(-50%); }
.star-pop i { font-style: normal; display: inline-block; } .star-pop i.off { opacity: .25; }
.star-pop i.new { animation: starPop .3s cubic-bezier(.2,1.6,.4,1) both; }

.fxnote { margin: 4px 0 0; font-size: 12px; color: var(--l3); }

/* =====================================================================================================================
   REDUCED MOTION. This block is LAST on purpose. Specificity ties are settled by source order, and when this list
   lived mid-file it lost to `.tile.hint .face`, `.tile.nudge .face` and `.celebrate.on:not(.t1)` further down, so
   the people who asked for less motion were the only ones still getting a flashing light bank and a pulsing ring.
   Selectors here are written to match or beat the rule they switch off; `:not(.t1)` is repeated for that reason,
   and `.tile.hint::after` is on its own line because a pseudo-element inside :is() is silently discarded.
   Anything whose only form was motion gets a still fallback rather than vanishing.
   ===================================================================================================================== */
@media (prefers-reduced-motion: reduce) {
  .tile, .tile .face, .bar i, .hstat .bar i, .board, .toast, .trail polyline, .chip u { transition: none; }
  .tile.pop .face, .tile.shake .face, .tile.bomb .face, .tile.nudge .face, .tile.hint .face,
  .q.pick, .qt.lock, .board.shake, .cmpflash, .reel.spin, .star-pop i.new, #bombBtn.active { animation: none; }
  .tile.hint::after { animation: none; }
  #bombBtn.active { box-shadow: 0 0 0 3px #f8717188; }
  .celebrate.on:not(.t1), .celebrate.on:not(.t1) .lights i, .coin { animation: none; }
  /* Both of these are nothing BUT their animation, so switching it off has to remove them rather than freeze them:
     `animation: none` on .cmpflash leaves the gold stripe parked at full opacity (its keyframes, not the rule, are
     what fade it in and out), which is a worse thing to show than the sweep it replaced. The in-app switch below
     already did this; the device switch did not, and the two must agree. */
  .coin, .cmpflash { display: none; }
  .tile.hint .face { box-shadow: 0 0 0 3px #fff3b0, 0 0 0 6px #ffb400; }
  .tile.bomb .face { box-shadow: 0 0 0 3px var(--bad); }
  .tile.nudge .face { box-shadow: 0 0 0 2px #fff7; }
}
body.reduce-motion .tile, body.reduce-motion .tile .face, body.reduce-motion .bar i,
body.reduce-motion .hstat .bar i, body.reduce-motion .board, body.reduce-motion .toast,
body.reduce-motion .trail polyline, body.reduce-motion .chip u { transition: none; }
body.reduce-motion .tile.pop .face, body.reduce-motion .tile.shake .face, body.reduce-motion .tile.bomb .face,
body.reduce-motion .tile.nudge .face, body.reduce-motion .tile.hint .face, body.reduce-motion .q.pick,
body.reduce-motion .qt.lock, body.reduce-motion .board.shake, body.reduce-motion .cmpflash,
body.reduce-motion .reel.spin, body.reduce-motion .star-pop i.new, body.reduce-motion #bombBtn.active { animation: none; }
body.reduce-motion .tile.hint::after { animation: none; }
body.reduce-motion #bombBtn.active { box-shadow: 0 0 0 3px #f8717188; }
body.reduce-motion .celebrate.on:not(.t1), body.reduce-motion .celebrate.on:not(.t1) .lights i,
body.reduce-motion .coin { animation: none; }
body.reduce-motion .coin { display: none; }
body.reduce-motion .tile.hint .face { box-shadow: 0 0 0 3px #fff3b0, 0 0 0 6px #ffb400; }
body.reduce-motion .tile.bomb .face { box-shadow: 0 0 0 3px var(--bad); }
body.reduce-motion .tile.nudge .face { box-shadow: 0 0 0 2px #fff7; }
