:root{
  --bg:#1a1020;
  --panel:#251733;
  --ink:#f5ede4;
  --muted:#a08caa;
  --gold:#ffc94d;
  --coral:#ff6b6b;
  --peach:#ff9e7d;
  --line:#3a2a4a;
  /* tile palette aliases for the logo mark */
  --c-a:var(--gold);
  --c-b:var(--peach);
  --c-c:var(--coral);
  --shadow:0 18px 50px -12px rgba(0,0,0,.72);
}
*{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent;}
:focus{outline:none;}
:focus-visible{outline:2.5px solid var(--gold);outline-offset:2px;border-radius:8px;}
.btn:focus-visible,.chip:focus-visible,.mini-btn:focus-visible,.icon-btn:focus-visible{outline-offset:3px;}
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;
  clip:rect(0,0,0,0);white-space:nowrap;border:0;}
html,body{height:100%;}
body{
  font-family:'Space Mono',monospace;
  background:
    radial-gradient(900px 600px at 85% -5%, rgba(255,201,77,.10), transparent 60%),
    radial-gradient(700px 500px at 0% 100%, rgba(255,158,125,.10), transparent 55%),
    var(--bg);
  color:var(--ink);
  overflow:hidden;
  user-select:none;-webkit-user-select:none;
  touch-action:manipulation;
}
.grain{position:fixed;inset:0;pointer-events:none;opacity:.035;z-index:99;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");}

.screen{position:fixed;inset:0;display:flex;flex-direction:column;
  opacity:0;pointer-events:none;transition:opacity .35s ease;}
.screen.on{opacity:1;pointer-events:auto;}

header{display:flex;align-items:center;gap:12px;padding:18px 20px 6px;}
.logo{font-family:'Fraunces',serif;font-weight:900;font-size:26px;letter-spacing:-.5px;}
.logo b{color:var(--gold);}
/* mini 3x3 puzzle mark in every header — replaces the old recording-dot
   vibe with something that says "tile puzzle" at a glance. Bottom-right
   square is the hole (dashed outline). */
.dot{
  width:26px;height:26px;flex-shrink:0;display:inline-block;
  background:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><rect x='0' y='0' width='6' height='6' rx='1.2' fill='%23ffc94d'/><rect x='9' y='0' width='6' height='6' rx='1.2' fill='%23ff9e7d'/><rect x='18' y='0' width='6' height='6' rx='1.2' fill='%23ff6b6b'/><rect x='0' y='9' width='6' height='6' rx='1.2' fill='%23ff6b6b'/><rect x='9' y='9' width='6' height='6' rx='1.2' fill='%23ffc94d'/><rect x='18' y='9' width='6' height='6' rx='1.2' fill='%23ff9e7d'/><rect x='0' y='18' width='6' height='6' rx='1.2' fill='%23ff9e7d'/><rect x='9' y='18' width='6' height='6' rx='1.2' fill='%23ff6b6b'/><rect x='18' y='18' width='6' height='6' rx='1.2' fill='none' stroke='%23a08caa' stroke-width='1' stroke-dasharray='1.5 1'/></svg>") center/contain no-repeat;
}
.tag{margin-left:auto;font-size:11px;color:var(--muted);letter-spacing:1px;
  background:none;border:none;font-family:inherit;padding:4px 6px;}
.tag.act{color:var(--gold);cursor:pointer;}

/* home — `min-height:0` lets the flex item shrink so its `overflow-y:auto`
   actually engages on short viewports. `justify-content: safe center`
   centers the column when there's room and falls back to flex-start when
   it overflows, so content always renders top-down without items
   overlapping. An earlier auto-margin trick produced grid-mark / hero
   overlap on certain real-device viewports — switched out for `safe`
   centering, which is what the keyword exists for. */
.home-body{flex:1;min-height:0;overflow-y:auto;
  display:flex;flex-direction:column;
  justify-content:safe center;
  align-items:center;padding:24px;gap:24px;text-align:center;
  scrollbar-width:none;-webkit-overflow-scrolling:touch;}
.home-body::-webkit-scrollbar{display:none;}
.hero{font-family:'Fraunces',serif;font-weight:900;font-size:clamp(38px,10.5vw,64px);
  line-height:.96;letter-spacing:-1.5px;}
.hero .em{font-style:italic;color:var(--coral);}
.hero .out{-webkit-text-stroke:1.6px var(--gold);color:transparent;}
.sub{color:var(--muted);font-size:13px;max-width:310px;line-height:1.7;}
/* logo mark — bottom-right quad: 3 tiles rotate clockwise around 1 gap */
/* `flex-shrink:0` is load-bearing: the grid-mark has absolute children at
   top:0..75 and the only thing reserving space for them is the height:107.
   In a flex column with `min-height:0` (which the scrollable home-body
   needs), the item gets compressed below 107 and the absolute children
   spill outside the parent — straight into the hero text below. */
.grid-mark{position:relative;width:107px;height:107px;
  flex-shrink:0;transform:rotate(-6deg);}
.grid-mark i{position:absolute;width:32px;height:32px;border-radius:8px;display:block;}
/* static tiles: top row + left column */
.grid-mark .c0{left:0;top:0;background:var(--c-a);}
.grid-mark .c1{left:37.5px;top:0;background:var(--c-b);}
.grid-mark .c2{left:75px;top:0;background:var(--c-c);}
.grid-mark .c3{left:0;top:37.5px;background:var(--c-c);}
.grid-mark .c6{left:0;top:75px;background:var(--c-b);}
/* rotating quad. corners TL(37.5,37.5) TR(75,37.5) BR(75,75) BL(37.5,75).
   gap starts at BR. cycle has 3 slide moments; each tile slides once,
   in sequence, so they never overlap or collide. timeline per 4.8s:
     0-8%   tile R0 slides BL->BR   (gap BL)
     33-41% tile R1 slides TL->BL   (gap TL)
     66-74% tile R2 slides TR->TL   (gap TR)
   then a 4th slide returns the loop — handled by R0's second leg. */
.grid-mark .rot{animation-duration:4.8s;animation-timing-function:ease-in-out;
  animation-iteration-count:infinite;}
.grid-mark .r0{background:var(--c-c);animation-name:rotA;}
.grid-mark .r1{background:var(--c-b);animation-name:rotB;}
.grid-mark .r2{background:var(--c-a);animation-name:rotC;}
/* R0: TR -> (wait) -> TL? simplest: each tile just orbits all 4 corners,
   offset so holds line up. one shared path, three start phases. */
@keyframes rotA{
  0%,20%   {left:75px;  top:75px;}
  25%,45%  {left:37.5px;top:75px;}
  50%,70%  {left:37.5px;top:37.5px;}
  75%,95%  {left:75px;  top:37.5px;}
  100%     {left:75px;  top:75px;}
}
@keyframes rotB{
  0%,20%   {left:37.5px;top:75px;}
  25%,45%  {left:37.5px;top:37.5px;}
  50%,70%  {left:75px;  top:37.5px;}
  75%,95%  {left:75px;  top:75px;}
  100%     {left:37.5px;top:75px;}
}
@keyframes rotC{
  0%,20%   {left:37.5px;top:37.5px;}
  25%,45%  {left:75px;  top:37.5px;}
  50%,70%  {left:75px;  top:75px;}
  75%,95%  {left:37.5px;top:75px;}
  100%     {left:37.5px;top:37.5px;}
}

.cta-stack{display:flex;flex-direction:column;gap:12px;width:100%;max-width:330px;}
.btn{border:none;font-family:'Space Mono',monospace;font-weight:700;
  font-size:14px;padding:17px 20px;border-radius:16px;cursor:pointer;
  display:flex;align-items:center;justify-content:center;gap:10px;
  transition:transform .12s ease, box-shadow .2s ease;letter-spacing:.3px;}
.btn:active{transform:scale(.96) translateY(1px);}
.btn-primary{background:var(--gold);color:#2a1538;
  box-shadow:0 10px 24px -8px rgba(255,201,77,.6), inset 0 1px 0 rgba(255,255,255,.35);}
.btn-ghost{background:var(--panel);color:var(--ink);border:1.5px solid var(--line);}
.btn-ghost:active{border-color:var(--gold);}
.btn-coral{background:var(--coral);color:#2a1538;
  box-shadow:0 10px 24px -8px rgba(255,107,107,.55), inset 0 1px 0 rgba(255,255,255,.3);}
.btn:disabled{opacity:.4;box-shadow:none;}
.btn svg{width:19px;height:19px;}

.label{font-size:11px;color:var(--muted);letter-spacing:1.5px;margin:6px 4px 5px;}
.label:first-child{margin-top:4px;}

.chips{display:flex;gap:9px;flex-wrap:wrap;}
.chip{flex:1;min-width:62px;background:var(--panel);border:1.5px solid var(--line);
  color:var(--muted);border-radius:13px;padding:12px 6px;cursor:pointer;
  font-weight:700;font-size:12px;text-align:center;transition:all .15s ease;}
.chip .n{font-family:'Fraunces',serif;font-size:20px;color:var(--ink);display:block;}
.chip.sel{border-color:var(--gold);background:rgba(255,201,77,.1);color:var(--gold);}
.chip.sel .n{color:var(--gold);}

.toggle{display:flex;background:var(--panel);border:1.5px solid var(--line);
  border-radius:13px;overflow:hidden;}
.toggle div{flex:1;text-align:center;padding:12px 6px;font-size:12px;
  font-weight:700;color:var(--muted);cursor:pointer;transition:all .15s ease;}
.toggle div.sel{background:var(--peach);color:#2a1538;}
/* three-up variant for the punchline source — needs tighter padding so
   "📷 The photo / 🎭 My face / 🖼 Library" all fit on a phone. */
.toggle-3 div{padding:11px 4px;font-size:11px;letter-spacing:0;}

/* punch layer — the captured punchline image (or live front-cam feed)
   that overlays the hole tile on the crop frame. Positioned + sized in JS
   from state.hole + state.grid. Pointer-events stay off so the underlying
   crop-frame gestures still drive pan/pinch/rotate. */
.punch-layer{position:absolute;overflow:hidden;
  border:2.5px solid var(--gold);box-shadow:0 0 14px rgba(255,201,77,.55);
  pointer-events:none;z-index:3;background:#000;}
.punch-layer canvas,.punch-layer video{
  position:absolute;inset:0;width:100%;height:100%;
  object-fit:cover;display:block;transform-origin:center center;}
/* Live cam = mirror (selfie convention). Inline style from JS layers the
   user's pan / zoom / rotate transform on top, so this rule is only the
   fallback default; JS overrides it as soon as a gesture or slider moves. */
.punch-layer video{transform:scaleX(-1);}
/* While framing live, blend the cam down so the underlying painting / photo
   bleeds through — it's the only way to align an eye with an eye. */
.punch-layer.framing{background:transparent;}
.punch-layer.framing video{opacity:.68;mix-blend-mode:screen;}

/* Big SNAP CTA below the frame — out of the way of the live face so the
   sender can actually see what they're aligning. Visible only while live. */
.punch-snap-cta{width:100%;margin-top:10px;padding:14px 18px;font-size:14px;
  letter-spacing:.5px;}
.punch-snap-cta span{font-size:18px;}

/* PHOTO | PUNCH segmented control — lives in an action bar directly above
   the crop frame (out of the tile hit-area), shown only once a punchline
   image is captured. Picks which target the pan/zoom/rotate gestures act on. */
.crop-actionbar{display:flex;align-items:center;gap:10px;}
.crop-actionbar .ab-label{font-size:11px;color:var(--muted);letter-spacing:1.5px;}
.edit-mode-pill{display:flex;background:var(--panel);
  border:1.5px solid var(--line);border-radius:11px;overflow:hidden;}
.edit-mode-pill button{background:transparent;border:0;
  color:var(--muted);padding:8px 16px;font-family:'Space Mono',monospace;
  font-size:11px;font-weight:700;letter-spacing:.5px;cursor:pointer;
  transition:all .15s ease;}
.edit-mode-pill button.sel{background:var(--gold);color:#2a1538;}

.txt{width:100%;background:var(--panel);border:1.5px solid var(--line);
  border-radius:13px;padding:13px 14px;color:var(--ink);
  font-family:'Space Mono',monospace;font-size:13px;font-weight:700;
  outline:none;transition:border-color .15s ease;}
.txt::placeholder{color:var(--muted);font-weight:400;}
.txt:focus{border-color:var(--gold);}

/* camera */
.cam-stage{flex:1;display:flex;flex-direction:column;align-items:center;
  justify-content:center;padding:20px;gap:14px;}
.cam-wrap{position:relative;width:100%;max-width:420px;aspect-ratio:3/4;
  border-radius:20px;overflow:hidden;border:1.5px solid var(--line);
  background:#000;box-shadow:var(--shadow);}
#cam{width:100%;height:100%;object-fit:cover;display:block;}
.cam-overlay{position:absolute;inset:0;pointer-events:none;}
/* mask: darken letterbox above/below the centered square crop region.
   video is 3:4, so square = full width centered vertically;
   mask bands = (height - width) / 2 each = 12.5% of height. */
.cam-mask{position:absolute;left:0;right:0;background:rgba(0,0,0,.55);}
.cam-mask.top{top:0;height:12.5%;}
.cam-mask.bot{bottom:0;height:12.5%;}
.cam-square{position:absolute;left:0;right:0;top:12.5%;bottom:12.5%;
  box-shadow:inset 0 0 0 2px rgba(255,201,77,.55);}
.cam-grid{position:absolute;inset:0;background-size:var(--gs) var(--gs);
  background-image:
    linear-gradient(rgba(255,255,255,.32) 1px,transparent 1px),
    linear-gradient(90deg,rgba(255,255,255,.32) 1px,transparent 1px);}
.cam-size{display:flex;gap:8px;}
.cam-size .csz{background:var(--panel);border:1.5px solid var(--line);
  color:var(--muted);border-radius:10px;padding:7px 12px;font-size:12px;
  font-weight:700;cursor:pointer;font-family:'Space Mono',monospace;
  transition:all .15s ease;}
.cam-size .csz.sel{border-color:var(--gold);color:var(--gold);
  background:rgba(255,201,77,.1);}
.shutter{width:74px;height:74px;border-radius:50%;background:var(--ink);
  border:5px solid var(--panel);box-shadow:0 0 0 3px var(--gold);cursor:pointer;
  transition:transform .1s ease;}
.shutter:active{transform:scale(.9);}
.cam-row{display:flex;align-items:center;gap:24px;}
.icon-btn{background:var(--panel);border:1.5px solid var(--line);color:var(--ink);
  width:48px;height:48px;border-radius:50%;display:flex;align-items:center;
  justify-content:center;cursor:pointer;}
.icon-btn svg{width:21px;height:21px;}
.hint{font-size:11px;color:var(--muted);text-align:center;line-height:1.7;
  transition:opacity .7s ease;}
.hint.faded{opacity:0;pointer-events:none;}

/* crop / frame step */
.crop-stage{flex:1;min-height:0;overflow-y:auto;display:flex;flex-direction:column;padding:10px 20px 0;gap:8px;}
/* Send / Try CTAs ride at the end of the scrollable setup content rather than
   a pinned bottom bar that crowded the puzzle frame. */
.setup-cta{display:flex;flex-direction:column;gap:10px;margin-top:8px;padding-bottom:22px;}
/* Keep every control in the merged setup at its natural height — without
   this, column-flex shrinks toggle/inputs to a hairline when total content
   exceeds viewport, instead of scrolling like a sensible person would. */
.crop-stage > *{flex-shrink:0;}
.crop-frame{position:relative;width:100%;aspect-ratio:1;flex-shrink:0;
  border-radius:18px;overflow:hidden;background:#000;
  border:1.5px solid var(--line);box-shadow:var(--shadow);
  touch-action:none;cursor:grab;}
.crop-frame.grabbing{cursor:grabbing;}
.crop-frame canvas{position:absolute;left:0;top:0;
  pointer-events:none;will-change:transform;}
.crop-overlay{position:absolute;inset:0;pointer-events:none;
  background:
    linear-gradient(rgba(255,255,255,.16) 1px,transparent 1px) 0 33.33%/100% 33.33%,
    linear-gradient(90deg,rgba(255,255,255,.16) 1px,transparent 1px) 33.33% 0/33.33% 100%;
  box-shadow:inset 0 0 0 2px rgba(255,201,77,.4);}
/* hole indicator on the crop frame — tap a tile in the grid to mark it.
   Position + size set in JS based on state.grid + state.hole. */
.crop-hole-mark{position:absolute;pointer-events:none;
  background:repeating-linear-gradient(45deg,
    rgba(255,107,107,.6) 0 6px,rgba(20,10,26,.45) 6px 12px);
  border:2.5px solid var(--gold);box-shadow:0 0 14px rgba(255,201,77,.55);
  display:flex;align-items:center;justify-content:center;
  font-size:10px;font-weight:700;color:var(--ink);letter-spacing:.5px;
  z-index:3;}
.crop-hole-mark::after{content:'⌖ HOLE';background:var(--ink);
  color:#2a1538;padding:2px 6px;border-radius:5px;}
/* `display:flex` above clobbers the HTML hidden attribute on the
   user-agent stylesheet — reinstate it explicitly. */
.crop-hole-mark[hidden],.punch-layer[hidden],.punch-snap[hidden],
.punch-snap-cta[hidden],.crop-actionbar[hidden],.reply-banner[hidden],
.invite-reply-tag[hidden],
#punch-canvas[hidden],#punch-video[hidden]{display:none;}

/* Reciprocal "send back" banner on the merged setup screen — reminds the
   sender who they're answering. Subtle peach to read as "in-progress
   conversation," not a warning. */
.reply-banner{display:flex;align-items:center;justify-content:center;gap:6px;
  margin:-2px 0 8px;padding:8px 14px;border-radius:10px;
  background:rgba(255,158,125,.14);border:1px solid rgba(255,158,125,.45);
  color:var(--peach);font-size:11.5px;font-weight:700;letter-spacing:.4px;}
.reply-banner b{color:var(--ink);font-family:'Fraunces',serif;font-weight:900;
  font-size:13px;letter-spacing:0;}

/* Receiver-side reply tag on the invite card — same visual language as the
   sender's banner so the loop reads as one thread. */
.invite-reply-tag{display:inline-block;margin-bottom:10px;
  padding:5px 10px;border-radius:8px;
  background:rgba(255,158,125,.16);border:1px solid rgba(255,158,125,.5);
  color:var(--peach);font-size:11px;font-weight:700;letter-spacing:.5px;}
.crop-controls{display:flex;align-items:center;gap:12px;}
.crop-controls span{font-size:16px;}
.crop-controls .readout{font-size:11px;font-weight:700;color:var(--muted);
  min-width:42px;text-align:right;letter-spacing:.3px;}

.zoom{flex:1;-webkit-appearance:none;appearance:none;height:6px;border-radius:6px;
  background:var(--line);outline:none;}
.zoom::-webkit-slider-thumb{-webkit-appearance:none;width:24px;height:24px;
  border-radius:50%;background:var(--gold);cursor:pointer;
  box-shadow:0 2px 8px rgba(0,0,0,.5);}
.zoom::-moz-range-thumb{width:24px;height:24px;border:none;border-radius:50%;
  background:var(--gold);cursor:pointer;}

/* play */
.play-top{display:flex;align-items:center;gap:9px;padding:14px 16px;flex-wrap:wrap;}
.stat{background:var(--panel);border:1.5px solid var(--line);border-radius:11px;
  padding:7px 11px;font-size:12px;font-weight:700;display:flex;gap:6px;align-items:center;}
.stat .v{color:var(--gold);font-family:'Fraunces',serif;font-size:16px;}
.mini-btn{background:var(--panel);border:1.5px solid var(--line);
  color:var(--ink);border-radius:11px;padding:8px 11px;font-size:12px;
  font-weight:700;cursor:pointer;font-family:'Space Mono',monospace;}
.mini-btn:active{transform:scale(.94);}
#board-wrap{flex:1;display:flex;align-items:flex-start;justify-content:center;padding:14px 18px 18px;}
#board{position:relative;border-radius:16px;overflow:hidden;
  /* content-box so the declared size IS the tile area; the 1.5px border
     sits outside it. With border-box the border would eat into the grid
     and clip the rightmost column / bottom row by ~3px. */
  box-sizing:content-box;
  border:1.5px solid var(--line);background:var(--panel);box-shadow:var(--shadow);}
#board::before{content:'';position:absolute;inset:0;pointer-events:none;
  box-shadow:inset 0 0 40px rgba(0,0,0,.4);border-radius:16px;z-index:1;}
.tile{position:absolute;overflow:hidden;cursor:grab;
  transition:left .13s cubic-bezier(.34,.9,.3,1), top .13s cubic-bezier(.34,.9,.3,1);}
.tile.dragging{transition:none;cursor:grabbing;z-index:50;}
.tile.settling{transition:left .19s cubic-bezier(.16,1.3,.4,1), top .19s cubic-bezier(.16,1.3,.4,1);}
.tile canvas{display:block;}
.tile.hole{cursor:default;}
.tile.movable::after{content:'';position:absolute;inset:0;
  box-shadow:inset 0 0 0 2px rgba(255,201,77,.0);transition:box-shadow .15s;}
.tile.movable:hover::after{box-shadow:inset 0 0 0 2px rgba(255,201,77,.55);}
@keyframes wiggle{
  0%,100%{transform:translateX(0);}
  20%{transform:translateX(-5px);}
  40%{transform:translateX(5px);}
  60%{transform:translateX(-3px);}
  80%{transform:translateX(3px);}
}
.tile.nope{animation:wiggle .32s ease;}
.tile-num{position:absolute;left:6px;top:5px;font-size:11px;font-weight:700;
  color:#fff;text-shadow:0 1px 3px #000;background:rgba(0,0,0,.35);
  border-radius:5px;padding:1px 5px;}
.play-foot{padding:0 18px 16px;text-align:center;}
.progress{height:6px;border-radius:6px;background:var(--line);
  overflow:hidden;margin:2px 0 10px;}
.progress-fill{height:100%;width:0%;border-radius:6px;
  background:linear-gradient(90deg,var(--coral),var(--gold));
  transition:width .35s cubic-bezier(.3,1,.4,1);}

/* win */
.win-body{flex:1;display:flex;flex-direction:column;align-items:center;
  justify-content:center;padding:26px;gap:16px;text-align:center;}
.win-card{background:var(--panel);border:1.5px solid var(--line);border-radius:22px;
  padding:22px;box-shadow:var(--shadow);max-width:340px;width:100%;}
.reveal-stage{position:relative;border-radius:14px;overflow:hidden;margin-bottom:16px;
  background:var(--bg);perspective:1200px;}
.reveal-stage img{width:100%;display:block;}
/* the empty socket where the hidden tile lands */
.reveal-socket{position:absolute;background:#120a18;
  box-shadow:inset 0 0 16px rgba(0,0,0,.8);
  display:flex;align-items:center;justify-content:center;}
.reveal-socket span{font-family:'Fraunces',serif;font-weight:900;
  font-size:26px;color:var(--coral);animation:qpulse 1s ease-in-out infinite;}
@keyframes qpulse{50%{opacity:.35;transform:scale(.88);}}
/* the hidden tile itself — flies in from off-stage. backface-visibility
   hides the back face mid-flip so the tile appears to materialize as it
   turns past 90°. */
.reveal-tile{position:absolute;overflow:hidden;
  box-shadow:0 6px 14px rgba(0,0,0,.35);will-change:transform;
  backface-visibility:hidden;-webkit-backface-visibility:hidden;
  transform-style:preserve-3d;}
.reveal-tile canvas{display:block;width:100%;height:100%;}
.reveal-tile.flyin{transition:transform .68s cubic-bezier(.18,1.15,.32,1);}
.reveal-tile.land{animation:flashring .6s ease-out;}
.reveal-tile.land canvas{animation:landpop .42s cubic-bezier(.34,1.56,.64,1);}
@keyframes landpop{0%{transform:scale(.86);}45%{transform:scale(1.09);}70%{transform:scale(.97);}100%{transform:scale(1);}}
@keyframes flashring{
  0%{box-shadow:0 6px 14px rgba(0,0,0,.35),0 0 0 0 rgba(255,201,77,.45);}
  60%{box-shadow:0 6px 14px rgba(0,0,0,.35),0 0 0 10px rgba(255,201,77,0);}
  100%{box-shadow:0 6px 14px rgba(0,0,0,.35),0 0 0 12px rgba(255,201,77,0);}
}
.win-title{font-family:'Fraunces',serif;font-weight:900;font-size:32px;
  color:var(--gold);line-height:1;margin-bottom:4px;
  opacity:0;transform:translateY(8px);transition:opacity .4s ease,transform .4s ease;}
.win-title.in{opacity:1;transform:translateY(0);}
.win-sub{opacity:0;transition:opacity .4s ease .1s;}
.win-sub.in{opacity:1;}
.win-stats{display:flex;justify-content:center;gap:20px;margin:14px 0 4px;
  opacity:0;transition:opacity .4s ease .2s;}
.win-stats.in{opacity:1;}
.win-stats div{font-size:11px;color:var(--muted);}
.win-stats b{display:block;font-family:'Fraunces',serif;font-size:21px;
  color:var(--ink);font-weight:900;}
.win-actions{opacity:0;transform:translateY(10px);
  transition:opacity .45s ease .35s,transform .45s ease .35s;}
.win-actions.in{opacity:1;transform:translateY(0);}
.more-link{background:none;border:none;color:var(--muted);font-size:12px;
  font-family:'Space Mono',monospace;cursor:pointer;text-decoration:underline;
  padding:6px;}
.more-row{display:flex;gap:10px;margin-top:4px;}
.more-row .btn{flex:1;padding:13px;font-size:12px;}

/* invite — received-link landing */
.invite-body{flex:1;display:flex;flex-direction:column;align-items:center;
  justify-content:center;padding:26px;gap:18px;text-align:center;}
.invite-card{background:var(--panel);border:1.5px solid var(--line);
  border-radius:22px;padding:20px;box-shadow:var(--shadow);
  max-width:330px;width:100%;}
.invite-from{font-family:'Fraunces',serif;font-weight:900;font-size:20px;
  margin-bottom:14px;line-height:1.2;}
.invite-from span{color:var(--gold);}
.invite-thumb{aspect-ratio:1;border-radius:14px;overflow:hidden;
  background:#000;border:1.5px solid var(--line);}
.invite-thumb canvas,.invite-thumb img{width:100%;height:100%;display:block;}
.invite-meta{margin-top:12px;font-size:11px;color:var(--muted);letter-spacing:.5px;}

.toast{position:fixed;left:50%;bottom:30px;transform:translate(-50%,80px);
  background:var(--ink);color:#2a1538;font-weight:700;font-size:12px;
  padding:12px 18px;border-radius:12px;z-index:120;
  transition:transform .35s cubic-bezier(.2,1.4,.4,1);
  box-shadow:var(--shadow);max-width:88vw;text-align:center;}
.toast.show{transform:translate(-50%,0);}
/* On the merged setup screen the CTA stack ends up at the bottom of the
   scroll; the default bottom-anchored toast would cover it. Slip the toast
   into the small gap between the header and the SIZE chips — narrow so it
   can never span the header row — and let it fade fast (~1.4s) so the
   chips it briefly overlaps aren't blocked while the user is choosing. */
body:has(#s-crop.on) .toast{bottom:auto;top:62px;max-width:70vw;
  font-size:11.5px;padding:10px 14px;
  transform:translate(-50%,-80px);}
body:has(#s-crop.on) .toast.show{transform:translate(-50%,0);}
.confetti{position:fixed;width:10px;height:14px;top:-20px;z-index:110;pointer-events:none;border-radius:2px;}

/* how-to-play overlay */
.howto{position:fixed;inset:0;z-index:140;background:rgba(20,10,26,.94);
  display:flex;flex-direction:column;align-items:center;justify-content:center;
  padding:30px;gap:22px;opacity:0;pointer-events:none;transition:opacity .3s ease;}
.howto.on{opacity:1;pointer-events:auto;}
.howto h2{font-family:'Fraunces',serif;font-weight:900;font-size:28px;
  color:var(--gold);text-align:center;}
.howto-steps{display:flex;flex-direction:column;gap:16px;max-width:340px;width:100%;}
.howto-step{display:flex;align-items:center;gap:14px;
  background:var(--panel);border:1.5px solid var(--line);
  border-radius:16px;padding:14px;}
.howto-ico{width:54px;height:54px;flex-shrink:0;border-radius:12px;
  background:var(--bg);display:flex;align-items:center;justify-content:center;}
.howto-ico svg{width:42px;height:42px;}
.howto-step p{font-size:12px;line-height:1.6;color:var(--ink);}
.howto-step b{color:var(--gold);}
::-webkit-scrollbar{width:0;}

.busy{position:fixed;inset:0;z-index:150;background:rgba(20,10,26,.92);
  display:none;flex-direction:column;align-items:center;justify-content:center;
  gap:18px;backdrop-filter:blur(4px);}
.busy.on{display:flex;}
.busy .spin{width:54px;height:54px;border-radius:50%;
  border:4px solid var(--line);border-top-color:var(--gold);
  animation:spin 0.9s linear infinite;}
.busy p{color:var(--ink);font-size:13px;letter-spacing:.5px;}
.busy small{color:var(--muted);font-size:11px;margin-top:-10px;}
@keyframes spin{to{transform:rotate(360deg);}}

/* small "report this puzzle" link shown on receiver screens only
   (invite + play). Subtle by default; abuse defense is opt-in. */
.report-link{background:transparent;border:0;color:var(--muted);
  font-family:'Space Mono',monospace;font-size:11px;font-weight:700;
  padding:8px 12px;margin-top:10px;cursor:pointer;text-decoration:underline;
  letter-spacing:.3px;align-self:center;}
.report-link:hover{color:var(--coral);}

/* "Your recent" row under the home CTAs. Sender-side only, cookies-only
   (localStorage). Last 8 puzzles you created, with a tiny teaser thumb
   and the grid / relative-time label. Tap a tile → open /p/{id} as if
   you'd received the link, so you can preview / re-share. Hidden by
   default; the script unhides it when the recent list is non-empty. */
.recent-row{width:100%;max-width:330px;margin-top:18px;display:flex;
  flex-direction:column;gap:8px;}
.recent-label{font-size:11px;color:var(--muted);letter-spacing:1.5px;
  display:flex;align-items:center;padding:0 2px;}
.recent-clear{margin-left:auto;background:transparent;border:0;
  color:var(--muted);font-family:'Space Mono',monospace;font-size:11px;
  font-weight:700;cursor:pointer;text-decoration:underline;
  padding:2px 4px;letter-spacing:.5px;}
.recent-clear:hover{color:var(--coral);}
.recent-scroll{display:flex;gap:8px;overflow-x:auto;padding:2px 2px 8px;
  scrollbar-width:none;-webkit-overflow-scrolling:touch;}
.recent-scroll::-webkit-scrollbar{display:none;}
.recent-tile{position:relative;flex:0 0 auto;width:74px;height:74px;
  border-radius:12px;overflow:hidden;background:var(--panel);
  border:1.5px solid var(--line);cursor:pointer;
  transition:border-color .15s ease, transform .12s ease;}
.recent-tile:hover{border-color:var(--gold);}
.recent-tile:active{transform:scale(.96);}
.recent-tile img{width:100%;height:100%;object-fit:cover;display:block;}
.recent-tile .meta{position:absolute;left:0;right:0;bottom:0;
  background:linear-gradient(to top,rgba(0,0,0,.78),transparent);
  color:var(--ink);font-size:9px;text-align:center;
  padding:10px 2px 3px;line-height:1.1;font-weight:700;letter-spacing:.3px;}
.recent-tile .x{position:absolute;top:3px;right:3px;width:18px;height:18px;
  border-radius:50%;background:rgba(0,0,0,.7);color:var(--ink);border:0;
  font-size:13px;font-weight:700;cursor:pointer;line-height:1;padding:0;
  display:flex;align-items:center;justify-content:center;}
.recent-tile .x:hover{background:var(--coral);color:#2a1538;}

/* "Paste image" tertiary action under the home CTAs. iOS Safari can't
   receive shared images via the OS share sheet (no Web Share Target),
   so paste is the fallback. Hidden by default; the script unveils it
   when the runtime supports navigator.clipboard.read(). */
.paste-link{background:transparent;border:0;color:var(--muted);
  font-family:'Space Mono',monospace;font-size:12px;font-weight:700;
  padding:8px 12px;margin-top:2px;cursor:pointer;
  display:inline-flex;align-items:center;gap:8px;align-self:center;
  letter-spacing:.3px;text-decoration:underline;}
.paste-link:hover{color:var(--gold);}
.paste-link svg{width:15px;height:15px;}

.legal-sep{color:var(--line);margin:0 4px;}
.made-by{font-size:11px;color:var(--muted);letter-spacing:.5px;
  text-align:center;padding:14px 20px 18px;}
.made-by a{color:var(--gold);text-decoration:none;font-weight:700;}
.made-by a:hover{text-decoration:underline;}

@media (prefers-reduced-motion: reduce){
  *,*::before,*::after{
    animation-duration:.01ms !important;
    animation-iteration-count:1 !important;
    transition-duration:.01ms !important;
    scroll-behavior:auto !important;
  }
  .grid-mark .rot,.reveal-socket span{animation:none !important;}
  .grain{display:none;}
}
