:root{
  --ink:#15203a;
  --ink-soft:#5a678a;
  --page:#e9eef8;
  --panel:#ffffff;
  --well:#d7deee;
  --well-line:#c5cee4;
  --rim:rgba(21,32,58,.10);
  --focus:#2f6bff;
  box-sizing:border-box;
  padding-top:env(safe-area-inset-top,0px);
  padding-bottom:env(safe-area-inset-bottom,0px);
}
@media (prefers-color-scheme:dark){
  :root:not([data-theme="light"]){
    --ink:#f2f5ff;
    --ink-soft:#97a3c4;
    --page:#161a2e;
    --panel:#1f2440;
    --well:#121627;
    --well-line:#2a3153;
    --rim:rgba(0,0,0,.35);
    --focus:#7fa6ff;
  }
}
:root[data-theme="dark"]{
  --ink:#f2f5ff; --ink-soft:#97a3c4; --page:#161a2e; --panel:#1f2440;
  --well:#121627; --well-line:#2a3153; --rim:rgba(0,0,0,.35); --focus:#7fa6ff;
}
*,*::before,*::after{box-sizing:border-box}
html,body{height:100%;margin:0}
html{
  scroll-padding-top:env(safe-area-inset-top,0px);
  -webkit-text-size-adjust:100%;text-size-adjust:100%;
  overscroll-behavior:none;                 /* no rubber-band past the edges */
}
body{
  background:var(--page);
  color:var(--ink);
  font-family:'Fredoka','Trebuchet MS',system-ui,sans-serif;
  font-weight:500;
  display:flex;
  justify-content:center;
  overflow:hidden;
  overscroll-behavior:none;
  /* Nothing here is a document: dragging a piece must never start a text
     selection, and a quick double tap must never zoom. */
  -webkit-user-select:none;-moz-user-select:none;user-select:none;
  -webkit-touch-callout:none;               /* no iOS long-press callout */
  -webkit-tap-highlight-color:transparent;
  touch-action:manipulation;                /* kills double-tap zoom */
}
/* the share fallback is the one place text must stay selectable */
.copybox,.grid-preview{
  -webkit-user-select:text;-moz-user-select:text;user-select:text;
  -webkit-touch-callout:default;
}
.app{
  width:100%;max-width:460px;height:100%;
  display:flex;flex-direction:column;gap:9px;
  padding:10px 14px 14px;
}

/* ---- title strip ---- */
.strip{display:flex;align-items:center;gap:8px;font-size:12.5px;color:var(--ink-soft)}
.strip .name{font-weight:700;color:var(--ink);font-size:14px}
.strip .day{font-variant-numeric:tabular-nums}
.strip .spacer{flex:1}
.strip .best{font-variant-numeric:tabular-nums;white-space:nowrap}
.strip .best b{font-weight:700;color:var(--ink)}
.strip .best.beaten{color:#e8a71a}
.strip .best.beaten b{color:#e8a71a}
.icon-btn{
  font:inherit;font-size:14px;line-height:1;color:var(--ink-soft);
  background:none;border:0;padding:4px 6px;border-radius:8px;cursor:pointer;
}
.icon-btn:hover{color:var(--ink)}
.modes{
  display:flex;gap:2px;padding:2px;border-radius:9px;
  background:var(--well);border:1.5px solid var(--well-line);
}
.mode-btn{
  font:inherit;font-size:11px;font-weight:600;line-height:1;
  color:var(--ink-soft);background:none;border:0;
  padding:4px 8px;border-radius:7px;cursor:pointer;
  touch-action:manipulation;-webkit-tap-highlight-color:transparent;
}
.mode-btn.on{background:var(--panel);color:var(--ink);box-shadow:0 1px 0 var(--rim)}

/* ---- head ---- */
.hud{display:flex;align-items:center;gap:12px}
.goal{display:flex;align-items:center;gap:9px;flex:1;min-width:0}
.goal-label{font-size:15px;color:var(--ink-soft);white-space:nowrap}
.goal-pebble{
  --line:#3a2b45;
  --line:color-mix(in srgb,var(--c,#6b7690) 42%,#1a1026);
  width:44px;height:44px;flex:none;border-radius:14px;
  display:grid;place-items:center;
  font-size:22px;font-weight:700;color:#fff;
  font-variant-numeric:tabular-nums;letter-spacing:-.02em;
  background:var(--c,#6b7690);
  border:2.5px solid var(--line);
  box-shadow:0 3px 0 0 var(--line);
  transition:transform .18s ease;
}
.goal-pebble.hit{transform:scale(1.18) rotate(-5deg)}
/* six pips: bonds banked toward the next target level */
.steps{display:flex;flex-direction:column;gap:3px;flex:none}
.steps i{
  width:7px;height:4px;border-radius:99px;display:block;
  background:var(--well-line);transition:background .2s ease;
}
.steps i.on{background:var(--ink-soft)}
.steps i.next{background:#e8a71a;animation:pipGlow .9s ease-in-out infinite}
.steps.maxed i{opacity:.25}
@keyframes pipGlow{0%,100%{opacity:.45}50%{opacity:1}}
.meta{display:flex;gap:14px;align-items:center}
.stat{text-align:right;line-height:1.05}
.stat b{display:block;font-size:19px;font-weight:700;font-variant-numeric:tabular-nums}
.stat span{font-size:11.5px;color:var(--ink-soft)}
.queue{display:flex;gap:6px;align-items:center}
.slot{display:grid;gap:2px;--m:14px}
.slot.s1{--m:11px;opacity:.9}
.slot.s2{--m:9px;opacity:.68}
/* 3-wide shapes make the queue wide; drop the far preview on small phones */
@media (max-width:390px){ .slot.s2{display:none} }
.slot .m{
  --line:#3a2b45;
  --line:color-mix(in srgb,var(--c,#3B7DE0) 42%,#1a1026);
  width:var(--m);height:var(--m);
  border-radius:calc(var(--m) * .3);
  background:var(--c,#3B7DE0);
  border:1.5px solid var(--line);
  display:grid;place-items:center;
  font-size:calc(var(--m) * .62);font-weight:700;font-style:normal;
  color:var(--on,#fff);font-variant-numeric:tabular-nums;
}

/* ---- board ---- */
.board-wrap{
  flex:1;min-height:0;position:relative;
  display:flex;align-items:center;justify-content:center;gap:9px;
}
/* ---- power charge gauge ---- */
.gauge{
  position:relative;flex:none;width:17px;
  border-radius:99px;overflow:hidden;
  background:var(--well);
  border:2.5px solid #3a2b45;
  display:flex;align-items:flex-end;
}
.gauge-fill{
  width:100%;height:0%;
  background-image:linear-gradient(to top,#3B7DE0 0%,#3DA35D 42%,#F2B01E 74%,#E04F45 100%);
  background-size:100% var(--gh,300px);
  background-position:bottom;
  background-repeat:no-repeat;
  transition:height .3s cubic-bezier(.3,.9,.4,1);
}
.gauge-bolt{
  position:absolute;left:0;right:0;top:4px;
  text-align:center;font-size:10px;line-height:1;
  opacity:.45;pointer-events:none;
}
.gauge.charged .gauge-bolt{opacity:1}
.board{
  position:relative;border-radius:16px;flex:none;
  background:
    repeating-linear-gradient(to right,transparent 0,transparent calc(var(--cell) - 1px),var(--well-line) calc(var(--cell) - 1px),var(--well-line) var(--cell)),
    repeating-linear-gradient(to bottom,transparent 0,transparent calc(var(--cell) - 1px),var(--well-line) calc(var(--cell) - 1px),var(--well-line) var(--cell)),
    var(--well);
  overflow:hidden;
  touch-action:none;
}
/* shake/spark/ring motion is driven by the Web Animations API in JS, not
   keyframes: Safari resolves var() in @keyframes before an inline custom
   property lands, so var()-driven keyframes silently do nothing there. */
.tile{
  position:absolute;top:0;left:0;
  width:var(--cell);height:var(--cell);
  padding:calc(var(--cell) * .055);
  transition:transform .13s cubic-bezier(.3,.7,.4,1);
  will-change:transform;
}
/* flat cartoon sticker: solid fill, inked outline, hard offset shadow */
.tile .body{
  --line:#3a2b45;  /* fallback for Safari < 16.2 */
  --line:color-mix(in srgb,var(--c) 42%,#1a1026);
  width:100%;height:100%;border-radius:calc(var(--cell) * .27);
  background:var(--c);
  border:calc(var(--cell) * .055) solid var(--line);
  box-shadow:0 calc(var(--cell) * .05) 0 0 var(--line);
  display:flex;flex-direction:column;align-items:center;justify-content:center;
  color:var(--on,#fff);position:relative;
  transition:transform .16s ease;
}
/* one flat shine streak — no gradient */
.tile .body::before{
  content:'';position:absolute;left:13%;top:10%;
  width:24%;height:11%;border-radius:99px;
  background:rgba(255,255,255,.62);
  transform:rotate(-20deg);pointer-events:none;
}
/* Stone: an obstacle, not a character. No face and no shine - every other
   block has eyes, so a blank slab reads instantly as "this one is not
   yours and it will never bond". Cracks reuse .body::after, which is free
   here; only the bomb's fuse claims it. */
.tile.stone .body{
  --line:#4a4f5c;
  background:#8A8F9C;
  border-radius:calc(var(--cell) * .16);   /* blockier than a character */
}
.tile.stone .eyes,.tile.stone .pips{display:none}
.tile.stone .num{font-weight:800}
/* chipped speckles instead of the shine streak */
.tile.stone .body::before{
  content:'';position:absolute;inset:0;
  left:0;top:0;width:100%;height:100%;
  border-radius:inherit;transform:none;
  background:
    radial-gradient(circle at 24% 28%, rgba(74,79,92,.55) 0 12%, transparent 13%),
    radial-gradient(circle at 72% 36%, rgba(74,79,92,.40) 0 8%,  transparent 9%),
    radial-gradient(circle at 58% 74%, rgba(74,79,92,.45) 0 10%, transparent 11%),
    radial-gradient(circle at 33% 66%, rgba(255,255,255,.22) 0 7%, transparent 8%);
  pointer-events:none;
}
.slot .m.stone{--line:#4a4f5c;background:#8A8F9C;border-radius:calc(var(--m) * .18)}
.tile .eyes{
  display:flex;gap:calc(var(--cell) * .09);
  margin-bottom:calc(var(--cell) * .035);
  /* Rare, quick blink. The closed frames are ~0.8% of the cycle, so at the
     12-20s period set per tile in makeEl() that lands near 100-160ms.
     Literal scaleY only — var() inside @keyframes does nothing on iOS. */
  transform-origin:center center;
  animation:blink 16s linear infinite;
}
@keyframes blink{
  0%,98.6%{transform:scaleY(1)}
  99.0%{transform:scaleY(.08)}
  99.4%{transform:scaleY(1)}
  100%{transform:scaleY(1)}
}
.tile .eyes i{
  width:calc(var(--cell) * .125);height:calc(var(--cell) * .125);
  border-radius:50%;background:#fff;display:block;position:relative;
  box-shadow:inset 0 0 0 calc(var(--cell) * .016) var(--line);
}
.tile .eyes i::after{
  content:'';position:absolute;top:24%;left:26%;
  width:48%;height:48%;border-radius:50%;background:var(--line);
  /* --px/--py steer the pupil: set per block in render(). Ordinary
     properties, never keyframe values, so Safari honours them. */
  transform:translate(var(--px,0%),var(--py,0%));
  transition:transform .16s ease-out;
}
/* Stubby arms and legs, injected on landing by limbs() and removed after.
   .tile .body is position:relative with no overflow:hidden, so these sit
   outside the block. Ink colour matches the pupils so they read as part
   of the same character. */
.limbs i{
  position:absolute;display:block;
  background:var(--line);
  border-radius:99px;
  transform:scale(0);              /* JS animates them out */
  pointer-events:none;
}
.limbs .arm{width:calc(var(--cell) * .30);height:calc(var(--cell) * .085);top:46%}
.limbs .arm.l{right:100%;transform-origin:right center}
.limbs .arm.r{left:100%;transform-origin:left center}
.limbs .leg{width:calc(var(--cell) * .085);height:calc(var(--cell) * .26);top:100%}
.limbs .leg.l{left:26%;transform-origin:center top}
.limbs .leg.r{right:26%;transform-origin:center top}
/* the grabbing arms: the wrapper pivots at the block's centre and is
   rotated toward the victim; the two arms sit either side of that line */
.grab{
  position:absolute;left:50%;top:50%;width:0;height:0;
  pointer-events:none;z-index:3;
  transform-origin:0 0;
}
.grab i{
  position:absolute;display:block;width:0;
  left:calc(var(--cell) * .40);          /* start at the body edge, not the
                                            centre - otherwise the arm is
                                            drawn across the block's own face */
  height:calc(var(--cell) * .07);
  background:var(--line);
  border-radius:99px;
}
/* spread across the left and right portions of that edge, not stacked */
.grab i:nth-child(1){top:calc(var(--cell) * -.155)}
.grab i:nth-child(2){top:calc(var(--cell) * .085)}

/* A rare hello: a settled block notices the player and waves. Built on
   demand and removed on completion like limbs(), so the steady-state DOM
   stays untouched. The arm swings from the shoulder, not the hand. */
.wave{position:absolute;inset:0;pointer-events:none}
.wave i{
  position:absolute;display:block;
  bottom:46%;
  width:calc(var(--cell) * .085);
  height:calc(var(--cell) * .32);
  background:var(--line);
  border-radius:99px;
  transform-origin:50% 100%;
  transform:scale(0);              /* JS swings it out */
}
.wave i.l{right:100%}
.wave i.r{left:100%}

/* 1 in 100 blocks turns up in sunglasses. The eyes become dark lenses
   joined by a bridge; the pupil is replaced by a glint. Blinking is
   switched off - you can't see eyes blink behind shades. */
.tile.shades .eyes{position:relative;animation:none}
.tile.shades .eyes i{
  width:calc(var(--cell) * .195);
  height:calc(var(--cell) * .175);
  border-radius:50%;                /* fully round lenses */
  background:#14101c;
  box-shadow:inset 0 0 0 calc(var(--cell) * .014) rgba(255,255,255,.26);
}
.tile.shades .eyes i::after{        /* glint, replacing the pupil */
  content:'';position:absolute;
  top:16%;left:14%;width:34%;height:28%;
  border-radius:50%;
  background:rgba(255,255,255,.55);
  transform:rotate(-20deg);
}
/* bridge across the nose - absolute so it stays out of the flex flow */
.tile.shades .eyes::before{
  content:'';position:absolute;z-index:1;
  left:50%;top:40%;transform:translateX(-50%);
  width:calc(var(--cell) * .105);
  height:calc(var(--cell) * .030);
  border-radius:99px;background:#14101c;
}
.tile .num{
  font-size:calc(var(--cell) * .38);font-weight:700;line-height:1;
  letter-spacing:-.02em;font-variant-numeric:tabular-nums;
}
.tile .pips{
  display:flex;gap:calc(var(--cell) * .05);margin-top:calc(var(--cell) * .055);
  height:calc(var(--cell) * .05);
}
.tile .pips i{
  width:calc(var(--cell) * .05);height:100%;border-radius:99px;display:block;
  background:rgba(255,255,255,.55);
  background:color-mix(in srgb,var(--line) 55%,transparent);
}
/* A block crushed by the stack above it cracks, then shatters. The block
   keeps its colour - only the fissures are drawn, so it stays readable.
   .body::after is free on non-bomb tiles (only .tile.bomb uses it for the
   fuse). Four patterns, picked at random per block, in two stages. */
.tile.dying .body::after{
  content:'';position:absolute;inset:0;pointer-events:none;
  border-radius:inherit;z-index:1;
  background-repeat:no-repeat;
}
.tile.dy1 .body::after{opacity:.75}
.tile.dy2 .body::after{opacity:1}
/* Last stage before it shatters: the eyes spiral - the cartoon shorthand
   for concussed. Two offset border arcs make the swirl, so there is no SVG
   data URI or mask involved and it renders identically on Safari. Placed
   after the sunglasses rules so it wins at equal specificity. */
.tile.dy2 .eyes,
.tile.crumble .eyes{animation:none}        /* the dazed do not blink */
.tile.dy2 .eyes i,
.tile.crumble .eyes i{
  background:#fff;                       /* the eyeball stays white */
  border:0;
  border-radius:50%;
  box-shadow:inset 0 0 0 calc(var(--cell) * .016) var(--line);
  overflow:hidden;                       /* keep the swirl inside the eye */
  transform:scale(1.45);
  animation:none;                        /* the swirl spins, the eye does not */
}
/* two opposing arcs inside the eyeball read as a spiral; only these are inked */
.tile.dy2 .eyes i::before,
.tile.crumble .eyes i::before,
.tile.dy2 .eyes i::after,
.tile.crumble .eyes i::after{
  content:'';display:block;position:absolute;
  background:none;
  border-radius:50%;
  border-style:solid;
  animation:dizzy 2.3s linear infinite;
}
.tile.dy2 .eyes i::before,
.tile.crumble .eyes i::before{
  top:10%;left:10%;width:80%;height:80%;
  border-width:calc(var(--cell) * .022);
  border-color:var(--line) transparent transparent var(--line);
}
.tile.dy2 .eyes i::after,
.tile.crumble .eyes i::after{
  top:32%;left:32%;width:36%;height:36%;
  border-width:calc(var(--cell) * .019);
  border-color:transparent var(--line) var(--line) transparent;
}
@keyframes dizzy{
  from{transform:rotate(0deg)}
  to{transform:rotate(360deg)}
}
/* Stars circling overhead - the other half of the concussion shorthand.
   Injected only while a block is fainted (see render) and removed with it.
   The orbit is a rotating zero-size pivot with the star offset inside, so
   the radius stays an ordinary property: a var() inside @keyframes would
   do nothing on iOS Safari. The star counter-rotates to stay upright. */
.stars{
  position:absolute;left:50%;top:-6%;
  width:0;height:0;pointer-events:none;z-index:4;
}
.stars .sp{
  position:absolute;left:0;top:0;width:0;height:0;display:block;
  animation:orbit 1.35s linear infinite;
}
.stars b{
  position:absolute;display:block;
  left:calc(var(--cell) * .20);
  top:calc(var(--cell) * -.08);
  width:calc(var(--cell) * .17);height:calc(var(--cell) * .17);
  background:#ffd84d;
  clip-path:polygon(50% 0%,61% 35%,98% 35%,68% 57%,79% 91%,50% 70%,21% 91%,32% 57%,2% 35%,39% 35%);
  filter:drop-shadow(0 1px 0 #1a1026) drop-shadow(0 -1px 0 #1a1026)
         drop-shadow(1px 0 0 #1a1026) drop-shadow(-1px 0 0 #1a1026);
  animation:orbitUp 1.35s linear infinite;
}
@keyframes orbit{ from{transform:rotate(0deg)} to{transform:rotate(360deg)} }
@keyframes orbitUp{ from{transform:rotate(0deg)} to{transform:rotate(-360deg)} }
/* the other half faint with their eyes simply shut */
.tile.dy2.fnt2 .eyes i,
.tile.crumble.fnt2 .eyes i{
  background:linear-gradient(0deg,transparent 43%,var(--line) 43%,var(--line) 57%,transparent 57%);
  border:0;
  border-radius:99px;
  transform:scale(1.25);
  animation:none;
}
.tile.dy2.fnt2 .eyes i::before,
.tile.crumble.fnt2 .eyes i::before,
.tile.dy2.fnt2 .eyes i::after,
.tile.crumble.fnt2 .eyes i::after{display:none}
.tile.dy2.shades .eyes::before{opacity:.35}
/* stage 1: a couple of fine fissures. stage 2 adds more and darkens them. */
.tile.ck1.dy1 .body::after{background:
  linear-gradient(118deg,transparent 43%,rgba(8,6,14,.55) 44.4%,transparent 45.8%),
  linear-gradient(-58deg,transparent 60%,rgba(8,6,14,.4) 61.2%,transparent 62.4%)}
.tile.ck1.dy2 .body::after{background:
  linear-gradient(118deg,transparent 42%,rgba(8,6,14,.8) 44.4%,transparent 46.8%),
  linear-gradient(-58deg,transparent 59%,rgba(8,6,14,.7) 61.2%,transparent 63.4%),
  linear-gradient(32deg,transparent 70%,rgba(8,6,14,.6) 71.4%,transparent 72.8%),
  linear-gradient(88deg,transparent 28%,rgba(8,6,14,.45) 29.2%,transparent 30.4%)}
.tile.ck2.dy1 .body::after{background:
  linear-gradient(-142deg,transparent 38%,rgba(8,6,14,.55) 39.5%,transparent 41%),
  linear-gradient(74deg,transparent 64%,rgba(8,6,14,.4) 65.3%,transparent 66.6%)}
.tile.ck2.dy2 .body::after{background:
  linear-gradient(-142deg,transparent 36%,rgba(8,6,14,.8) 39.5%,transparent 42%),
  linear-gradient(74deg,transparent 62%,rgba(8,6,14,.7) 65.3%,transparent 68%),
  linear-gradient(-20deg,transparent 50%,rgba(8,6,14,.6) 51.5%,transparent 53%),
  linear-gradient(160deg,transparent 74%,rgba(8,6,14,.45) 75.2%,transparent 76.4%)}
.tile.ck3.dy1 .body::after{background:
  linear-gradient(96deg,transparent 52%,rgba(8,6,14,.55) 53.4%,transparent 54.8%),
  linear-gradient(-100deg,transparent 34%,rgba(8,6,14,.4) 35.3%,transparent 36.6%)}
.tile.ck3.dy2 .body::after{background:
  linear-gradient(96deg,transparent 50%,rgba(8,6,14,.8) 53.4%,transparent 56.8%),
  linear-gradient(-100deg,transparent 32%,rgba(8,6,14,.7) 35.3%,transparent 38%),
  linear-gradient(46deg,transparent 64%,rgba(8,6,14,.6) 65.5%,transparent 67%),
  linear-gradient(-166deg,transparent 22%,rgba(8,6,14,.45) 23.2%,transparent 24.4%)}
.tile.ck4.dy1 .body::after{background:
  linear-gradient(-36deg,transparent 46%,rgba(8,6,14,.55) 47.4%,transparent 48.8%),
  linear-gradient(130deg,transparent 68%,rgba(8,6,14,.4) 69.3%,transparent 70.6%)}
.tile.ck4.dy2 .body::after{background:
  linear-gradient(-36deg,transparent 44%,rgba(8,6,14,.8) 47.4%,transparent 50.8%),
  linear-gradient(130deg,transparent 66%,rgba(8,6,14,.7) 69.3%,transparent 72%),
  linear-gradient(12deg,transparent 30%,rgba(8,6,14,.6) 31.5%,transparent 33%),
  linear-gradient(-84deg,transparent 56%,rgba(8,6,14,.45) 57.2%,transparent 58.4%)}
/* shatter: a hard snap, then the pieces collapse away */
.tile.crumble .body{animation:shatter .46s cubic-bezier(.3,.7,.5,1) forwards}
.tile.crumble .body::after{opacity:1}
@keyframes shatter{
  0%{transform:scale(1) rotate(0);opacity:1}
  14%{transform:scale(1.14) rotate(-4deg);opacity:1}
  30%{transform:scale(.86) rotate(5deg);opacity:.95}
  52%{transform:scale(.92) rotate(-3deg);opacity:.7}
  100%{transform:scale(.22) rotate(16deg) translateY(34%);opacity:0}
}
/* minus square: plain white, dark type, a dashed inner rim so it reads as
   "not one of the colours" at a glance. It pairs with any colour. */
.tile.minus .body{
  --line:#3a2b45;
  background:#ffffff;
}
.tile.minus .num{letter-spacing:-.04em}
.tile.minus .body::after{
  content:'';position:absolute;pointer-events:none;
  inset:calc(var(--cell) * .07);
  border-radius:calc(var(--cell) * .2);
  border:calc(var(--cell) * .018) dashed rgba(58,43,69,.45);
}
.tile.minus .eyes i{box-shadow:inset 0 0 0 calc(var(--cell) * .016) #3a2b45}
.slot .m.minus{--line:#3a2b45;background:#ffffff;color:#1a1026}
/* bomb: black block, rope fuse, flickering flame. .tile .body has no
   overflow:hidden, so the fuse and flame sit outside the block. The corner
   radius is left to inherit, so a bomb is the same rounded square as every
   other block and only its colour and fuse set it apart. */
.tile.bomb .body{
  --line:#07070d;
  background:#26263a;
}
.tile.bomb .num,.tile.bomb .pips{display:none}
.tile.bomb .eyes{margin-bottom:0}
.tile.bomb .eyes i{box-shadow:inset 0 0 0 calc(var(--cell) * .014) #07070d}
/* rope fuse, angled out of the top-right */
.tile.bomb .body::after{
  content:'';position:absolute;z-index:1;
  right:12%;top:-20%;
  width:calc(var(--cell) * .055);height:calc(var(--cell) * .24);
  background:#c08b4a;border-radius:99px;
  transform:rotate(26deg);
  box-shadow:0 0 0 calc(var(--cell) * .013) #07070d;
}
/* flame at the fuse tip - replaces the shine streak for bombs */
.tile.bomb .body::before{
  content:'';position:absolute;z-index:2;
  right:-2%;top:-40%;left:auto;
  width:calc(var(--cell) * .18);height:calc(var(--cell) * .23);
  border-radius:50% 50% 50% 50% / 62% 62% 38% 38%;
  background:linear-gradient(to top,#ff4d1f,#ffa62b 50%,#ffe66d);
  box-shadow:0 0 calc(var(--cell) * .18) rgba(255,150,50,.85);
  transform:none;
  animation:flame .26s ease-in-out infinite alternate;
}
@keyframes flame{
  from{transform:scale(1) rotate(-6deg)}
  to{transform:scale(1.2,1.32) rotate(6deg)}
}
.slot .m.bomb{
  --line:#07070d;
  background:#26263a;
  box-shadow:inset 0 0 0 1px #ffa62b;
}
/* wild: hard-stop stripes of all four hues — flat, no blending */
.tile.wild .body{
  --line:#1a1026;
  /* striped from the session palette; the literals are the classic set,
     kept as a fallback for the first paint before applyPalette() runs */
  background:linear-gradient(135deg,
    var(--w1,#E04F45) 0 25%, var(--w2,#3DA35D) 0 50%,
    var(--w3,#3B7DE0) 0 75%, var(--w4,#F2B01E) 0);
}
.tile.wild .num{
  text-shadow:
    -1.5px 0 0 #1a1026, 1.5px 0 0 #1a1026, 0 -1.5px 0 #1a1026, 0 1.5px 0 #1a1026,
    -1.5px -1.5px 0 #1a1026, 1.5px -1.5px 0 #1a1026,
    -1.5px 1.5px 0 #1a1026, 1.5px 1.5px 0 #1a1026;
}
.tile.wild .pips i{background:rgba(255,255,255,.9)}
.slot .m.wild{
  --line:#1a1026;
  background:linear-gradient(135deg,
    var(--w1,#E04F45) 0 25%, var(--w2,#3DA35D) 0 50%,
    var(--w3,#3B7DE0) 0 75%, var(--w4,#F2B01E) 0);
  color:#fff;
}
.banner{
  position:relative;z-index:8;pointer-events:none;
  text-align:center;font-weight:700;font-size:calc(var(--cell) * .58);
  color:#fff;letter-spacing:.02em;
  text-shadow:
    -2px 0 0 #1a1026, 2px 0 0 #1a1026, 0 -2px 0 #1a1026, 0 2px 0 #1a1026,
    -2px -2px 0 #1a1026, 2px -2px 0 #1a1026, -2px 2px 0 #1a1026, 2px 2px 0 #1a1026,
    0 5px 0 rgba(26,16,38,.4);
  animation:bnr 1.25s ease-out forwards;
}
@keyframes bnr{
  0%{opacity:0;transform:scale(.5) rotate(-6deg)}
  18%{opacity:1;transform:scale(1.12) rotate(2deg)}
  30%{transform:scale(1) rotate(0)}
  78%{opacity:1;transform:scale(1)}
  100%{opacity:0;transform:scale(1.05) translateY(-14px)}
}
.tile.falling{transition:none}
/* the absorbing tile gulps: squash wide, stretch tall, settle */
.tile.bump .body{animation:gulp .3s cubic-bezier(.3,1.5,.5,1)}
@keyframes gulp{
  0%{transform:scale(1,1)}
  28%{transform:scale(1.26,.8)}
  56%{transform:scale(.9,1.16)}
  78%{transform:scale(1.06,.96)}
  100%{transform:scale(1,1)}
}
/* the eaten tile is swallowed — shrinks and spins away as it slides in */
/* a block that got hauled in travels slower, so the grab reads as a pull */
.tile.pulled{transition:transform .36s cubic-bezier(.45,.05,.3,1)}
.tile.pulled .body{animation:swallow .44s cubic-bezier(.4,0,.8,.3) forwards}
.tile.absorb .body{animation:swallow .2s cubic-bezier(.4,0,.8,.3) forwards}
@keyframes swallow{
  0%{transform:scale(1) rotate(0)}
  55%{transform:scale(.6) rotate(10deg);opacity:.9}
  100%{transform:scale(.08) rotate(22deg);opacity:0}
}
/* A bond: anticipation squash, then burst. The blast holds at full size
   for a beat before fading, so the explosion lingers instead of blinking
   out. Keep the removal timeout in render() above this duration. */
.tile.pop .body{animation:burst .55s cubic-bezier(.2,.9,.3,1) forwards}
@keyframes burst{
  0%{transform:scale(1) rotate(0);opacity:1}
  12%{transform:scale(.74) rotate(-7deg);opacity:1}
  32%{transform:scale(1.8) rotate(6deg);opacity:1}
  62%{transform:scale(1.95) rotate(9deg);opacity:.9}
  100%{transform:scale(2.35) rotate(13deg);opacity:0}
}

/* ---- burst effects ---- */
.ring{
  position:absolute;z-index:5;pointer-events:none;
  width:calc(var(--cell) * .9);height:calc(var(--cell) * .9);
  margin-left:calc(var(--cell) * -.45);margin-top:calc(var(--cell) * -.45);
  border-radius:50%;border-style:solid;
  border-width:calc(var(--cell) * .085);
  opacity:0;
}
.spark{
  position:absolute;z-index:6;pointer-events:none;display:block;
  width:calc(var(--cell) * .17);height:calc(var(--cell) * .17);
  margin-left:calc(var(--cell) * -.085);margin-top:calc(var(--cell) * -.085);
  border-radius:50%;
  box-shadow:0 0 0 1.5px #1a1026;
  opacity:0;
}
.flash{
  position:absolute;inset:0;z-index:4;pointer-events:none;background:#fff;opacity:0;
}

/* ---- announcement stack ----
   Every transient callout lives in ONE centred column instead of each
   owning a hand-picked `top:` percentage. Whatever is live stacks and can
   never overlap, however many fire at once. This works only because all of
   these animate with transforms, and transforms do not affect layout - the
   combo can bounce from scale(2.6) without shoving its neighbours. Order is
   fixed in CSS so it does not depend on which happens to fire first. */
.hud-stack{
  position:absolute;left:0;right:0;top:20%;z-index:9;
  display:flex;flex-direction:column;align-items:center;
  gap:calc(var(--cell) * .14);
  pointer-events:none;
}
.hud-stack>.banner{order:1}
.hud-stack>.streak{order:2}
.hud-stack>.combo {order:3}
/* ---- combo callout ---- */
.combo{
  position:relative;z-index:9;pointer-events:none;
  display:flex;flex-direction:column;align-items:center;line-height:1;
  opacity:0;                       /* JS animates it in (see comboFx) */
}
.combo .cw{
  font-weight:600;letter-spacing:.16em;
  color:#fff;margin-bottom:calc(var(--cell) * .06);
}
.combo .cn{font-weight:700;color:#ffd84d}
.combo .cw,.combo .cn{
  text-shadow:
    -2px 0 0 #1a1026, 2px 0 0 #1a1026, 0 -2px 0 #1a1026, 0 2px 0 #1a1026,
    -2px -2px 0 #1a1026, 2px -2px 0 #1a1026, -2px 2px 0 #1a1026, 2px 2px 0 #1a1026,
    0 6px 0 rgba(26,16,38,.35);
}

/* ---- bond streak ----
   Tinted with the streak's own colour, so it follows the session palette. */
.streak{
  position:relative;z-index:8;pointer-events:none;
  font-weight:800;letter-spacing:.06em;line-height:1;
  font-size:calc(var(--cell) * .36);
  color:#fff;opacity:0;            /* JS animates it in (see streakFx) */
  text-shadow:
    -2px 0 0 #1a1026, 2px 0 0 #1a1026, 0 -2px 0 #1a1026, 0 2px 0 #1a1026,
    -2px -2px 0 #1a1026, 2px -2px 0 #1a1026, -2px 2px 0 #1a1026, 2px 2px 0 #1a1026,
    0 4px 0 rgba(26,16,38,.34);
}
.streak b{font-weight:800}

/* ---- chain popups ---- */
.popup{
  position:absolute;pointer-events:none;z-index:6;
  font-weight:700;line-height:1;white-space:nowrap;
  font-size:calc(var(--cell) * .34);color:#fff;
  /* comic outline, drawn with stacked hard shadows */
  text-shadow:
    -2px 0 0 #1a1026, 2px 0 0 #1a1026, 0 -2px 0 #1a1026, 0 2px 0 #1a1026,
    -2px -2px 0 #1a1026, 2px -2px 0 #1a1026, -2px 2px 0 #1a1026, 2px 2px 0 #1a1026,
    0 4px 0 rgba(26,16,38,.4);
  animation:rise .78s cubic-bezier(.2,.8,.3,1) forwards;
}
.popup .x{font-size:calc(var(--cell) * .46);color:#ffd84d}
/* score penalty: red, falls downward, unmistakably not a reward */
.popup.minus{color:#ff5a5a;animation:none}
.stat b.lost{animation:scoreLost .7s ease-out}
@keyframes scoreLost{
  0%{color:#ff5a5a;transform:scale(1.25)}
  35%{color:#ff5a5a;transform:scale(.95)}
  100%{color:inherit;transform:scale(1)}
}
@keyframes rise{
  0%{opacity:0;transform:translate(-50%,0) scale(.6)}
  18%{opacity:1;transform:translate(-50%,-6px) scale(1.15)}
  100%{opacity:0;transform:translate(-50%,-46px) scale(1)}
}

/* ---- overlay ---- */
.overlay{
  position:absolute;inset:0;display:grid;place-items:center;
  background:color-mix(in srgb,var(--page) 84%,transparent);
  -webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px);border-radius:16px;text-align:center;padding:18px;
  overflow:auto;
}
.overlay[hidden]{display:none}
.overlay h1{font-size:26px;margin:0 0 6px;font-weight:700}
.overlay p{margin:0 0 14px;color:var(--ink-soft);font-size:14px;line-height:1.5;max-width:30ch}
.grid-preview{
  font-family:ui-monospace,Menlo,Consolas,monospace;
  font-size:15px;line-height:1.14;letter-spacing:1px;
  margin:0 0 14px;white-space:pre;
}
.btn{
  font:inherit;font-weight:600;font-size:16px;color:#fff;background:#2f6bff;
  border:2.5px solid #16306e;border-radius:13px;padding:11px 24px;cursor:pointer;
  box-shadow:0 4px 0 0 #16306e;
}
.btn:active{transform:translateY(3px);box-shadow:0 1px 0 0 #16306e}
.btn.ghost{background:var(--panel);color:var(--ink);border-color:var(--ink-soft);box-shadow:0 4px 0 0 var(--ink-soft)}
.btn.ghost:active{box-shadow:0 1px 0 0 var(--ink-soft)}
.btn.small{font-size:14px;padding:10px 18px}
.row{display:flex;gap:8px;justify-content:center;flex-wrap:wrap}
.copybox{
  width:100%;max-width:280px;margin:10px auto 0;display:none;
  font:inherit;font-size:12px;padding:8px;border-radius:10px;
  border:1px solid var(--well-line);background:var(--panel);color:var(--ink);
  resize:none;height:120px;
}
.copybox.show{display:block}

/* ---- daily leaderboard ----
   Reuses .overlay for the backdrop and .btn for the control, so this is
   mostly layout for the list itself. */
.lb-sub{margin:0 0 12px !important;font-size:12.5px;letter-spacing:.04em;text-transform:uppercase}
.lb-list{
  list-style:none;margin:0 0 12px;padding:0;
  width:100%;max-width:280px;
  font-variant-numeric:tabular-nums;
}
.lb-list li{
  display:flex;align-items:baseline;gap:8px;
  padding:7px 10px;border-radius:10px;
  background:var(--panel);box-shadow:0 1px 0 var(--rim);
  margin-bottom:4px;font-size:14px;
}
.lb-list li .pos{
  flex:none;width:1.6em;text-align:right;
  color:var(--ink-soft);font-size:12.5px;font-weight:600;
}
.lb-list li .who{
  flex:1;min-width:0;text-align:left;font-weight:600;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
.lb-list li .pts{flex:none;font-weight:700}
/* the player's own row, so they can find themselves at a glance */
.lb-list li.you{background:color-mix(in srgb,#2f6bff 16%,var(--panel));box-shadow:0 1px 0 var(--rim),inset 0 0 0 1.5px #2f6bff}
.lb-list li.empty{justify-content:center;color:var(--ink-soft);font-weight:500;background:none;box-shadow:none}
.lb-note{margin:0 0 12px !important;font-size:12.5px;min-height:1.2em}
.lb-name{
  font:inherit;font-size:15px;text-align:center;
  width:100%;max-width:200px;margin:0 auto 10px;display:block;
  padding:9px 10px;border-radius:11px;
  border:2px solid var(--well-line);background:var(--panel);color:var(--ink);
}
.lb-name:focus{outline:none;border-color:var(--focus)}

/* A cell knocked loose by spam-rotating. It cycles its face the whole way
   down and settles on whatever it shows when it lands, so it has to LOOK
   unstable - otherwise the changing number reads as a rendering bug. */
.tile.scram .body{animation:scram .3s ease-in-out infinite alternate}
@keyframes scram{
  from{filter:brightness(1) saturate(1)}
  to{filter:brightness(1.35) saturate(1.3)}
}
.tile.scram .num{animation:scramNum .3s ease-in-out infinite alternate}
@keyframes scramNum{
  from{transform:scale(1)}
  to{transform:scale(1.12)}
}
.ov-hint{
  margin:12px 0 0;font-size:12px;color:var(--ink-soft);opacity:.85;
}
.ov-hint[hidden]{display:none}

/* ---- pad ---- */
.pad{display:grid;grid-template-columns:1fr 1fr 1fr 1.3fr;gap:7px}
.key{
  font:inherit;font-size:19px;font-weight:600;color:var(--ink);
  background:var(--panel);border:0;border-radius:13px;padding:13px 0;cursor:pointer;
  box-shadow:0 3px 0 var(--rim);
  touch-action:manipulation;                 /* no 300ms tap delay */
  -webkit-user-select:none;user-select:none;
  -webkit-tap-highlight-color:transparent;
}
.key:active{transform:translateY(2px);box-shadow:0 1px 0 var(--rim)}
.key.wide{font-size:14.5px}
:focus-visible{outline:3px solid var(--focus);outline-offset:2px}
.hint{margin:0;text-align:center;font-size:12px;color:var(--ink-soft)}
/* The board is HEIGHT limited on every phone - cells are square and there
   are always 12 rows - so the empty strips either side are slack the board
   cannot use. The only way to bigger squares is to claw back vertical
   space; the side gaps then close on their own. */
@media (pointer:coarse){
  .hint{display:none}            /* no keyboard, so the key hints are dead weight */
}
@media (max-height:860px){
  .app{padding:6px 10px 10px;gap:6px}
  .hint{display:none}
  .key{padding:10px 0;font-size:17px}
  .key.wide{font-size:13.5px}
  .goal-pebble{width:38px;height:38px;font-size:19px;border-radius:12px}
  .strip{font-size:11.5px}
  .strip .name{font-size:12.5px}
  .icon-btn{padding:2px 5px}
  .mode-btn{padding:3px 7px}
  .stat b{font-size:17px}
  .stat span{font-size:10.5px}
  .board-wrap{gap:7px}
}

