packing-bingo 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -103,6 +103,27 @@ var ja = {
103
103
  shuffle: "\u{1F500} \u30B7\u30E3\u30C3\u30D5\u30EB",
104
104
  soundOn: "\u{1F50A} \u97F3\u3042\u308A",
105
105
  soundOff: "\u{1F507} \u97F3\u306A\u3057",
106
+ readyTitle: "Are you ready?",
107
+ readyPrompt: "",
108
+ soundHint: "\u7AEF\u672B\u306E\u97F3\u91CF\u3092MAX\u306B",
109
+ vibrationHint: "\u304A\u3084\u3059\u307F\u30E2\u30FC\u30C9\u3092\u30AA\u30D5",
110
+ readyTap: "\u30BF\u30C3\u30D7\u3067\u30B9\u30BF\u30FC\u30C8",
111
+ readyGo: "START!",
112
+ time: "\u30BF\u30A4\u30E0",
113
+ statTime: "\u30AF\u30EA\u30A2\u30BF\u30A4\u30E0",
114
+ modeLabel: "\u30E2\u30FC\u30C9",
115
+ modeNormal: "\u901A\u5E38",
116
+ modeInfinite: "\u221E \u7121\u9650",
117
+ normalTitle: "NORMAL",
118
+ normalSub: "\u901A\u5E38\u30E2\u30FC\u30C9",
119
+ infiniteTitle: "INFINITE",
120
+ infiniteSub: "\u7121\u9650\u30D3\u30F3\u30B4",
121
+ infiniteRule: "\u305D\u308D\u3063\u305F\u5217\u306F\u6D88\u3048\u3066\u3001\u307E\u305F\u57CB\u3081\u3089\u308C\u308B\u3002\u4F55\u5EA6\u3067\u3082\u30D3\u30F3\u30B4\uFF01",
122
+ finish: "\u{1F3C1} FINISH",
123
+ finishTitle: "\u30D5\u30A3\u30CB\u30C3\u30B7\u30E5\uFF01",
124
+ finishStamp: "\u221E",
125
+ finishTime: "\u30D7\u30EC\u30A4\u30BF\u30A4\u30E0",
126
+ again: "\u3082\u3046\u4E00\u5EA6",
106
127
  stamp: "OK",
107
128
  free: "FREE",
108
129
  reachBadge: "\u30EA\u30FC\u30C1",
@@ -113,15 +134,15 @@ var ja = {
113
134
  bannerNth: (n) => `${n} \u30E9\u30A4\u30F3\u76EE\uFF01`,
114
135
  bannerMulti: (n) => `\u4E00\u6C17\u306B ${n} \u30E9\u30A4\u30F3\uFF01`,
115
136
  statusStart: () => "",
116
- statusLines: (lines, left) => `\u30D3\u30F3\u30B4 ${lines} \u30E9\u30A4\u30F3\uFF01 \u306E\u3053\u308A ${left} \u3053`,
137
+ statusLines: () => "",
117
138
  statusReach: () => "",
118
- statusDone: "\u{1F389} \u30D1\u30FC\u30D5\u30A7\u30AF\u30C8\uFF01\u5FD8\u308C\u7269\u30BC\u30ED\u3067\u51FA\u767A\u3067\u304D\u307E\u3059",
139
+ statusDone: "",
119
140
  perfectTag: "\u{1F3C6} PERFECT",
120
141
  perfectStamp: "\u6E96\u5099\n\u5B8C\u4E86",
121
142
  perfectTitle: "\u30D1\u30FC\u30D5\u30A7\u30AF\u30C8\uFF01",
122
- perfectBody: (n) => `${n}\u3053 \u305C\u3093\u3076\u305D\u308D\u3063\u305F\uFF01
123
- \u5FD8\u308C\u7269\u30BC\u30ED\u3067\u3001\u3044\u3063\u3066\u3089\u3063\u3057\u3083\u3044\uFF01`,
143
+ perfectBody: "",
124
144
  perfectBonus: (n) => `PERFECT BONUS +${n}`,
145
+ speedBonus: (n) => `SPEED BONUS +${n.toLocaleString()}`,
125
146
  statScore: "SCORE",
126
147
  statCombo: "MAX COMBO",
127
148
  statLines: "BINGO",
@@ -144,6 +165,27 @@ var en = {
144
165
  shuffle: "\u{1F500} Shuffle",
145
166
  soundOn: "\u{1F50A} Sound on",
146
167
  soundOff: "\u{1F507} Muted",
168
+ readyTitle: "Are you ready?",
169
+ readyPrompt: "",
170
+ soundHint: "Turn your volume to MAX",
171
+ vibrationHint: "Turn off Do Not Disturb",
172
+ readyTap: "Tap to start",
173
+ readyGo: "GO!",
174
+ time: "Time",
175
+ statTime: "Time",
176
+ modeLabel: "Mode",
177
+ modeNormal: "Normal",
178
+ modeInfinite: "\u221E Infinite",
179
+ normalTitle: "NORMAL",
180
+ normalSub: "Normal mode",
181
+ infiniteTitle: "INFINITE",
182
+ infiniteSub: "Infinite bingo",
183
+ infiniteRule: "Completed lines clear, so you can fill them again. Bingo forever!",
184
+ finish: "\u{1F3C1} FINISH",
185
+ finishTitle: "Finish!",
186
+ finishStamp: "\u221E",
187
+ finishTime: "Time played",
188
+ again: "Play again",
147
189
  stamp: "OK",
148
190
  free: "FREE",
149
191
  reachBadge: "REACH",
@@ -154,15 +196,15 @@ var en = {
154
196
  bannerNth: (n) => `Line ${n}!`,
155
197
  bannerMulti: (n) => `${n} lines at once!`,
156
198
  statusStart: () => "",
157
- statusLines: (lines, left) => `${lines} bingo line${lines === 1 ? "" : "s"}! ${left} to go`,
199
+ statusLines: () => "",
158
200
  statusReach: () => "",
159
- statusDone: "\u{1F389} Perfect! Nothing left behind.",
201
+ statusDone: "",
160
202
  perfectTag: "\u{1F3C6} PERFECT",
161
203
  perfectStamp: "ALL\nSET",
162
204
  perfectTitle: "Perfect!",
163
- perfectBody: (n) => `All ${n} packed!
164
- Have a great trip!`,
205
+ perfectBody: "",
165
206
  perfectBonus: (n) => `PERFECT BONUS +${n}`,
207
+ speedBonus: (n) => `SPEED BONUS +${n.toLocaleString()}`,
166
208
  statScore: "SCORE",
167
209
  statCombo: "MAX COMBO",
168
210
  statLines: "BINGO",
@@ -179,8 +221,40 @@ var hz = (semitones) => 523.25 * Math.pow(2, semitones / 12);
179
221
  function createSound(isMuted) {
180
222
  let ac = null;
181
223
  let out = null;
224
+ const voices = /* @__PURE__ */ new Map();
225
+ function track(source, gain, start, nodes) {
226
+ voices.set(source, { gain, start });
227
+ source.onended = () => {
228
+ voices.delete(source);
229
+ source.disconnect();
230
+ nodes.forEach((n) => n.disconnect());
231
+ };
232
+ }
233
+ function stop() {
234
+ const now = ac?.currentTime ?? 0;
235
+ voices.forEach(({ gain, start }, source) => {
236
+ try {
237
+ if (start > now) {
238
+ gain.gain.cancelScheduledValues(now);
239
+ gain.gain.setValueAtTime(0, now);
240
+ source.stop(now);
241
+ } else {
242
+ if (typeof gain.gain.cancelAndHoldAtTime === "function") gain.gain.cancelAndHoldAtTime(now);
243
+ else {
244
+ const value = gain.gain.value;
245
+ gain.gain.cancelScheduledValues(now);
246
+ gain.gain.setValueAtTime(value, now);
247
+ }
248
+ gain.gain.linearRampToValueAtTime(0, now + 0.01);
249
+ source.stop(now + 0.01);
250
+ }
251
+ } catch {
252
+ }
253
+ });
254
+ voices.clear();
255
+ }
182
256
  function audio() {
183
- if (isMuted() || typeof window === "undefined") return null;
257
+ if (isMuted() || typeof window === "undefined" || document.hidden) return null;
184
258
  const ua = navigator.userActivation;
185
259
  if (!ac && ua && !ua.hasBeenActive) return null;
186
260
  if (!ac) {
@@ -208,6 +282,7 @@ function createSound(isMuted) {
208
282
  g.gain.exponentialRampToValueAtTime(vol, t + 0.01);
209
283
  g.gain.exponentialRampToValueAtTime(1e-4, t + dur);
210
284
  o.connect(g).connect(out);
285
+ track(o, g, t, [g]);
211
286
  o.start(t);
212
287
  o.stop(t + dur + 0.05);
213
288
  }
@@ -228,15 +303,19 @@ function createSound(isMuted) {
228
303
  f.Q.value = 0.7;
229
304
  g.gain.value = vol;
230
305
  s.connect(f).connect(g).connect(out);
306
+ track(s, g, t, [f, g]);
231
307
  s.start(t);
232
308
  }
233
309
  const step = (k) => hz(SCALE[Math.min(Math.max(k, 0), SCALE.length - 1)]);
234
310
  return {
235
311
  pop(k) {
236
312
  const f = step(k);
313
+ const energy = Math.min(3, Math.floor((k + 1) / 3));
237
314
  tone(f, 0, 0.16, "triangle", 0.22);
238
315
  tone(f * 2, 0, 0.08, "sine", 0.06);
239
- tone(150, 0, 0.09, "sine", 0.25, 60);
316
+ tone(170 + energy * 15, 0, 0.11, "sine", 0.28, 48);
317
+ noise(0, 0.035, 0.1 + energy * 0.025, 1800);
318
+ if (energy) tone(f * 1.5, 0.035, 0.11, "triangle", 0.04 + energy * 0.015);
240
319
  },
241
320
  off: () => tone(392, 0, 0.1, "sine", 0.1, 262),
242
321
  reach() {
@@ -266,7 +345,39 @@ function createSound(isMuted) {
266
345
  tone(hz(7), 0.34, 0.12, "triangle", 0.14);
267
346
  tone(hz(12), 0.42, 0.2, "triangle", 0.14);
268
347
  },
348
+ count(go) {
349
+ if (go) {
350
+ tone(hz(12), 0, 0.5, "square", 0.07);
351
+ tone(hz(24), 0, 0.5, "triangle", 0.14);
352
+ noise(0, 0.2, 0.12, 3e3);
353
+ } else {
354
+ tone(hz(5), 0, 0.14, "square", 0.07);
355
+ tone(hz(17), 0, 0.14, "triangle", 0.12);
356
+ }
357
+ },
358
+ mode(infinite) {
359
+ if (infinite) {
360
+ tone(196, 0, 0.42, "sine", 0.12, 784);
361
+ [0, 2, 4, 7, 9, 12, 14, 16, 19, 21, 24].forEach((s, i) => tone(hz(s - 12), i * 0.032, 0.16, "triangle", 0.07));
362
+ [12, 16, 19, 24].forEach((s) => tone(hz(s), 0.38, 1.1, "sine", 0.07));
363
+ tone(hz(31), 0.38, 0.9, "triangle", 0.03);
364
+ noise(0, 0.5, 0.07, 6500);
365
+ noise(0.36, 0.7, 0.05, 9e3);
366
+ } else {
367
+ tone(hz(7), 0, 0.1, "square", 0.05);
368
+ tone(hz(0), 0.1, 0.28, "triangle", 0.14);
369
+ tone(160, 0.1, 0.2, "sine", 0.26, 55);
370
+ noise(0.1, 0.12, 0.14, 1400);
371
+ }
372
+ },
373
+ clear(squares) {
374
+ const n = Math.max(1, Math.min(8, squares));
375
+ for (let i = 0; i < n; i++) tone(step(SCALE.length - 1 - i * 2), i * 0.035, 0.18, "sine", 0.07);
376
+ noise(0, 0.3, 0.08, 7e3);
377
+ },
378
+ stop,
269
379
  close() {
380
+ stop();
270
381
  if (ac) void ac.close().catch(() => {
271
382
  });
272
383
  ac = null;
@@ -275,6 +386,64 @@ function createSound(isMuted) {
275
386
  };
276
387
  }
277
388
 
389
+ // src/haptics.ts
390
+ var owner = null;
391
+ function createHaptics(enabled) {
392
+ const token = {};
393
+ let generation = 0;
394
+ const stop = () => {
395
+ generation++;
396
+ if (owner !== token) return;
397
+ owner = null;
398
+ try {
399
+ navigator.vibrate?.(0);
400
+ } catch {
401
+ }
402
+ };
403
+ const send = (pattern) => {
404
+ if (!enabled || document.hidden || typeof navigator.vibrate !== "function") return false;
405
+ if (navigator.userActivation && !navigator.userActivation.hasBeenActive) return false;
406
+ try {
407
+ if (navigator.vibrate(pattern)) {
408
+ owner = token;
409
+ return true;
410
+ }
411
+ } catch {
412
+ }
413
+ return false;
414
+ };
415
+ return {
416
+ play(moment, combo = 1, lines = 1, impactAt = 360) {
417
+ const current = ++generation;
418
+ const tier = Math.min(3, Math.floor(combo / 3));
419
+ const lead = Math.max(80, impactAt - 40);
420
+ const pattern = moment === "off" ? 35 : moment === "perfect" ? [160, 45, 100] : moment === "bingo" ? lines >= 3 ? [lead, 40, 500, 50, 650, 50, 850] : lines === 2 ? [lead, 40, 500, 50, 650] : [lead, 40, 500] : moment === "reach" ? [140, 40, 240] : moment === "shuffle" ? [90, 35, 90, 35, 220] : moment === "count" ? 60 : moment === "go" ? 260 : (
421
+ // Switching modes: a quick rising flutter into a hold, or one firm stamp.
422
+ moment === "infinite" ? [25, 30, 35, 30, 45, 30, 220] : moment === "normal" ? [50, 70, 160] : moment === "finish" ? [120, 40, 120, 40, 320] : tier ? [100 + tier * 20, 15, 80 + tier * 20] : 90
423
+ );
424
+ if (!send(pattern)) return;
425
+ return (beat) => {
426
+ if (generation === current && owner === token) send([...beat]);
427
+ };
428
+ },
429
+ stop
430
+ };
431
+ }
432
+
433
+ // src/celebration.ts
434
+ var PERFECT_REVEAL_MS = 520;
435
+ var PERFECT_STAMP_DELAY_MS = 720;
436
+ var PERFECT_STAMP_BEAT = [260, 45, 160];
437
+ var PERFECT_FIREWORKS = [
438
+ { at: 1820, beat: [200] },
439
+ { at: 2170, beat: [100, 35, 160] },
440
+ { at: 2520, beat: [260] },
441
+ { at: 3e3, beat: [110, 40, 110] },
442
+ { at: 3430, beat: [320] },
443
+ { at: 3950, beat: [140, 40, 200] },
444
+ { at: 4540, beat: [460] }
445
+ ];
446
+
278
447
  // src/storage-key.ts
279
448
  function resolveStorageKey(options) {
280
449
  if (options.controlled) return null;
@@ -302,13 +471,17 @@ var CSS = `
302
471
  .pkb-board.pkb-punch{animation:pkb-punch .4s cubic-bezier(.3,1.6,.5,1)}
303
472
  @keyframes pkb-punch{30%{transform:scale(1.018)}}
304
473
  .pkb-head{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:12px 16px;margin-bottom:18px}
305
- .pkb-tags{display:flex;gap:8px;align-items:center}
474
+ .pkb-tags{display:flex;flex-wrap:wrap;gap:8px;align-items:center}
306
475
  .pkb-kicker{display:inline-block;background:var(--pk-ink);color:var(--pk-sun);font-weight:800;font-size:11px;letter-spacing:.14em;padding:4px 10px;border-radius:999px}
307
476
  .pkb-kicker:empty{display:none}
308
477
  .pkb-kicker.pkb-gold{color:#fff;background:linear-gradient(90deg,#E8473B,#FF9F1C,#1FA971,#2F6FED,#E8473B);background-size:200% 100%;animation:pkb-rb 2s linear infinite}
309
478
  @keyframes pkb-rb{to{background-position:-200% 0}}
310
479
  .pkb button{font-family:var(--pk-body)}
311
480
  .pkb-snd{appearance:none;border:2px solid var(--pk-ink);background:var(--pk-paper);border-radius:999px;padding:2px 9px;font-weight:800;font-size:11px;line-height:1.4;color:var(--pk-ink);cursor:pointer}
481
+ .pkb-watch{display:inline-block;min-width:5.4em;padding:3px 10px 2px;border-radius:999px;background:var(--pk-ink);color:var(--pk-paper);font-family:var(--pk-display);font-size:13px;line-height:1.3;text-align:center;font-variant-numeric:tabular-nums;letter-spacing:.03em}
482
+ .pkb-watch::before{content:"\u23F1 ";font-family:var(--pk-body)}
483
+ .pkb-watch.pkb-run{color:var(--pk-sun)}
484
+ .pkb-watch.pkb-done{background:var(--pk-red);color:#fff}
312
485
  .pkb-title{font-family:var(--pk-display);font-weight:400;font-size:34px;line-height:1.15;margin-top:8px;letter-spacing:.02em;color:inherit}
313
486
  .pkb-sub{font-weight:800;font-size:13px;margin-top:4px;word-break:auto-phrase}
314
487
  .pkb-side{display:flex;flex-direction:column;gap:8px;flex:none}
@@ -326,12 +499,12 @@ var CSS = `
326
499
  .pkb-combo b{font-family:var(--pk-display);font-weight:400;font-size:13px;color:var(--pk-red);white-space:nowrap}
327
500
  .pkb-bar{flex:1;height:9px;border:2px solid var(--pk-ink);border-radius:999px;background:var(--pk-paper);overflow:hidden}
328
501
  .pkb-bar i{display:block;height:100%;background:var(--pk-red);transform-origin:left}
329
- .pkb-combo.pkb-on .pkb-bar i{animation:pkb-drain 2.5s linear forwards}
502
+ .pkb-combo.pkb-on .pkb-bar i.pkb-draining{animation:pkb-drain 2.5s linear forwards}
330
503
  @keyframes pkb-drain{from{transform:scaleX(1)}to{transform:scaleX(0)}}
331
504
  .pkb-gw{position:relative}
332
505
  .pkb-grid{display:grid;grid-template-columns:repeat(var(--pk-n),minmax(0,1fr));gap:calc(12px*var(--pk-d))}
333
506
  .pkb-tile{position:relative;min-width:0;place-self:stretch;aspect-ratio:1/.82;background:var(--pk-paper);border:2.5px solid var(--pk-ink);border-radius:calc(16px*var(--pk-d));box-shadow:4px 4px 0 var(--pk-ink);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:calc(6px*var(--pk-d));cursor:pointer;user-select:none;-webkit-tap-highlight-color:transparent;transition:transform .1s,box-shadow .1s,background-color .25s}
334
- .pkb-tile:active:not(.pkb-free){transform:translate(3px,3px);box-shadow:1px 1px 0 var(--pk-ink)}
507
+ .pkb-tile:active:not(.pkb-free):not(.pkb-lock){transform:translate(3px,3px);box-shadow:1px 1px 0 var(--pk-ink)}
335
508
  .pkb-tile input{position:absolute;opacity:0;pointer-events:none;width:1px;height:1px}
336
509
  .pkb-tile:has(input:focus-visible){outline:3px solid var(--pk-ink);outline-offset:3px}
337
510
  .pkb-emo{font-size:max(14px,calc(32px*var(--pk-s)));line-height:1;transition:opacity .2s}
@@ -339,6 +512,7 @@ var CSS = `
339
512
  .pkb-stamp{position:absolute;left:50%;top:50%;width:64%;aspect-ratio:1;border-radius:50%;border:3px solid var(--pk-red);box-shadow:inset 0 0 0 2px var(--pk-paper),inset 0 0 0 4px var(--pk-red);color:var(--pk-red);display:grid;place-items:center;font-family:var(--pk-display);font-size:max(9px,calc(20px*var(--pk-s)));letter-spacing:.04em;opacity:0;transform:translate(-50%,-50%) rotate(-16deg) scale(1.7);transition:transform .25s cubic-bezier(.3,1.5,.5,1),opacity .15s;pointer-events:none}
340
513
  .pkb-tile.pkb-on .pkb-stamp{opacity:.9;transform:translate(-50%,-50%) rotate(-16deg) scale(1)}
341
514
  .pkb-tile.pkb-on .pkb-emo,.pkb-tile.pkb-on .pkb-lbl{opacity:.35}
515
+ .pkb-tile.pkb-lock{cursor:default}
342
516
  .pkb-tile.pkb-free{cursor:default;container-type:inline-size}
343
517
  .pkb-tile.pkb-free .pkb-stamp{width:72%;opacity:1;transition:none;font-size:17cqi;letter-spacing:.02em;line-height:1;text-align:center;background:var(--pk-red);color:var(--pk-paper);border-color:var(--pk-red);box-shadow:inset 0 0 0 2px var(--pk-red),inset 0 0 0 4px var(--pk-paper)}
344
518
  .pkb-tile.pkb-line{background-color:var(--pk-mint)}
@@ -348,12 +522,14 @@ var CSS = `
348
522
  @keyframes pkb-aura{50%{box-shadow:4px 4px 0 var(--pk-ink),0 0 0 7px rgba(232,71,59,.35)}}
349
523
  .pkb-tile.pkb-reach::after{content:attr(data-reach);position:absolute;top:-11px;right:-8px;z-index:3;background:var(--pk-red);color:#fff;font-size:10.5px;font-weight:800;letter-spacing:.06em;padding:3px 8px;border-radius:999px;border:2px solid var(--pk-ink);transform:rotate(6deg);animation:pkb-bob 1.1s ease-in-out infinite}
350
524
  @keyframes pkb-bob{50%{transform:rotate(6deg) translateY(-4px)}}
351
- .pkb-tile.pkb-squash{animation:pkb-squash .34s cubic-bezier(.3,1.6,.5,1)}
352
- @keyframes pkb-squash{0%{transform:scale(.88)}55%{transform:scale(1.05)}}
525
+ .pkb-tile.pkb-squash{z-index:1;animation:pkb-squash .38s cubic-bezier(.16,1,.3,1)}
526
+ @keyframes pkb-squash{0%{transform:translate(3px,4px) scale(calc(.9 - var(--pkb-impact,0)*.018));box-shadow:0 0 0 var(--pk-ink)}38%{transform:translateY(calc(-2px - var(--pkb-impact,0)*1px)) scale(calc(1.045 + var(--pkb-impact,0)*.012))}70%{transform:translateY(1px) scale(.985)}100%{transform:none}}
527
+ .pkb-tile.pkb-on .pkb-stamp.pkb-hit{animation:pkb-stamp-hit .32s cubic-bezier(.16,1,.3,1) both}
528
+ @keyframes pkb-stamp-hit{0%{opacity:0;transform:translate(-50%,-50%) rotate(-26deg) scale(2.1)}28%{opacity:1;transform:translate(-50%,-50%) rotate(-12deg) scale(.88)}52%{transform:translate(-50%,-50%) rotate(-18deg) scale(1.08)}100%{opacity:.9;transform:translate(-50%,-50%) rotate(-16deg) scale(1)}}
353
529
  .pkb-tile.pkb-hop{animation:pkb-hop .55s cubic-bezier(.3,1.6,.5,1) var(--pkb-d,0ms) both}
354
530
  @keyframes pkb-hop{35%{transform:translateY(-14px) scale(1.07)}}
355
531
  .pkb-ring{position:absolute;left:50%;top:50%;width:70%;aspect-ratio:1;border:4px solid var(--pk-red);border-radius:50%;pointer-events:none;z-index:4;animation:pkb-ring .5s ease-out forwards}
356
- @keyframes pkb-ring{from{transform:translate(-50%,-50%) scale(.3);opacity:.9}to{transform:translate(-50%,-50%) scale(1.9);opacity:0}}
532
+ @keyframes pkb-ring{from{transform:translate(-50%,-50%) scale(.3);opacity:.9}to{transform:translate(-50%,-50%) scale(calc(1.9 + var(--pkb-impact,0)*.18));opacity:0}}
357
533
  .pkb-lines{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;overflow:visible;z-index:2;transition:opacity .4s}
358
534
  .pkb-board.pkb-all .pkb-lines{opacity:0}
359
535
  .pkb-lines line{fill:none;stroke-linecap:round;stroke-dasharray:1;stroke-dashoffset:0}
@@ -364,10 +540,11 @@ var CSS = `
364
540
  .pkb-foot{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:8px 10px;margin-top:18px;font-weight:800;font-size:13px;word-break:auto-phrase}
365
541
  .pkb-foot>span{flex:1 1 14em}
366
542
  .pkb-foot>span:empty{flex-basis:0}
367
- .pkb-acts{display:flex;gap:8px;flex:none;margin-left:auto}
543
+ .pkb-acts{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:8px;flex:0 1 auto;max-width:100%;margin-left:auto}
368
544
  .pkb-reset,.pkb-shuf{appearance:none;flex:none;border:2px solid var(--pk-ink);background:transparent;border-radius:999px;padding:5px 12px;font-weight:800;font-size:12px;line-height:1.4;color:var(--pk-ink);cursor:pointer}
369
545
  .pkb-shuf{background:var(--pk-paper);box-shadow:2px 2px 0 var(--pk-ink)}
370
- .pkb-shuf:active{transform:translate(2px,2px);box-shadow:none}
546
+ .pkb-shuf:active:not(:disabled){transform:translate(2px,2px);box-shadow:none}
547
+ .pkb-shuf:disabled{cursor:default;opacity:.4}
371
548
  .pkb-flash{position:absolute;inset:0;z-index:7;background:#fff;opacity:0;pointer-events:none}
372
549
  .pkb-flash.pkb-go{animation:pkb-flash .4s ease-out}
373
550
  @keyframes pkb-flash{0%{opacity:.8}100%{opacity:0}}
@@ -384,15 +561,43 @@ var CSS = `
384
561
  .pkb-float.pkb-big{font-size:clamp(14px,5.6cqi,34px);color:#FFE066}
385
562
  .pkb-float.pkb-rch{font-size:20px;color:var(--pk-red);-webkit-text-stroke:4px var(--pk-paper)}
386
563
  @keyframes pkb-rise{0%{opacity:0;transform:translate(-50%,-20%) scale(.4)}18%{opacity:1;transform:translate(-50%,-60%) scale(1.2)}30%{transform:translate(-50%,-70%) scale(1)}75%{opacity:1}100%{opacity:0;transform:translate(-50%,-190%) scale(1)}}
564
+ .pkb-ready{appearance:none;position:absolute;inset:0;z-index:12;display:grid;place-items:center;padding:20px;border:0;border-radius:inherit;background:rgba(42,31,20,.5);color:var(--pk-ink);font:inherit;text-align:center;cursor:pointer;-webkit-tap-highlight-color:transparent;transition:background-color .35s}
565
+ .pkb-ready[hidden]{display:none}
566
+ .pkb-ready:focus-visible{outline:3px solid var(--pk-ink);outline-offset:-6px}
567
+ .pkb-ready.pkb-gone{pointer-events:none;background-color:transparent}
568
+ .pkb-board.pkb-waiting .pkb-snd{position:relative;z-index:13}
569
+ .pkb-ready-in,.pkb-count{grid-area:1/1}
570
+ .pkb-ready-in{display:flex;flex-direction:column;align-items:center;width:min(400px,100%);padding:24px 22px 22px;border:2.5px solid var(--pk-ink);border-radius:22px;background:var(--pk-paper)}
571
+ .pkb-ready.pkb-counting .pkb-ready-in{visibility:hidden}
572
+ .pkb-ready-t{font-family:var(--pk-display);font-weight:400;font-size:clamp(26px,7.5cqi,36px);line-height:1.15;color:var(--pk-red)}
573
+ .pkb-ready-q{margin-top:6px;font-weight:800;font-size:18px;line-height:1.4}
574
+ .pkb-ready-l{display:flex;flex-direction:column;gap:4px;width:100%;margin-top:14px;padding:12px 14px;border-radius:14px;background:rgba(42,31,20,.07);font-weight:800;font-size:16px;line-height:1.6;text-align:left;overflow-wrap:anywhere;word-break:auto-phrase}
575
+ .pkb-ready-l>span::before{content:"\u30FB"}
576
+ .pkb-ready-go{margin-top:18px;padding:10px 30px;border:2.5px solid var(--pk-ink);border-radius:999px;background:var(--pk-sun);font-weight:800;font-size:16px;line-height:1.4;box-shadow:3px 3px 0 var(--pk-ink);animation:pkb-beat 1.2s ease-in-out infinite}
577
+ @keyframes pkb-beat{50%{transform:scale(1.06)}}
578
+ .pkb-count{font-family:var(--pk-display);font-weight:400;line-height:1.1;paint-order:stroke fill;font-size:clamp(90px,34cqi,200px);color:var(--pk-sun);-webkit-text-stroke:5px var(--pk-ink);text-shadow:7px 7px 0 var(--pk-ink);opacity:0;pointer-events:none}
579
+ .pkb-count.pkb-go{font-size:clamp(48px,17cqi,120px);color:var(--pk-red);white-space:nowrap}
580
+ .pkb-count.pkb-tick{animation:pkb-cnt 1s cubic-bezier(.2,1.4,.4,1) forwards}
581
+ .pkb-count.pkb-go.pkb-tick{animation:pkb-cnt-go .75s cubic-bezier(.2,1.4,.4,1) forwards}
582
+ @keyframes pkb-cnt{0%{opacity:0;transform:scale(2.2)}18%{opacity:1;transform:scale(.92)}30%{transform:scale(1)}75%{opacity:1}100%{opacity:0;transform:scale(.8)}}
583
+ @keyframes pkb-cnt-go{0%{opacity:0;transform:scale(.4) rotate(-8deg)}25%{opacity:1;transform:scale(1.12) rotate(-5deg)}40%{transform:scale(1) rotate(-5deg)}70%{opacity:1}100%{opacity:0;transform:scale(1.5) rotate(-5deg)}}
584
+ .pkb-ptime{display:inline-flex;align-items:baseline;gap:8px;margin:-4px 0 14px;padding:5px 16px 4px;border-radius:999px;background:var(--pk-ink);color:var(--pk-sun)}
585
+ .pkb-ptime[hidden]{display:none}
586
+ .pkb-ptime small{font-size:11px;font-weight:800;letter-spacing:.08em;color:#fff}
587
+ .pkb-ptime b{font-family:var(--pk-display);font-weight:400;font-size:22px;line-height:1.2;font-variant-numeric:tabular-nums}
387
588
  .pkb-perfect{position:absolute;inset:0;z-index:8;display:grid;place-items:center;padding:20px;background:rgba(42,31,20,.5);opacity:0;visibility:hidden;transition:opacity .3s,visibility .3s}
388
- .pkb-perfect.pkb-show{opacity:1;visibility:visible}
589
+ /* Visible at once when opening (focus can't land on a hidden dialog); hidden only after the fade when closing. */
590
+ .pkb-perfect.pkb-show{opacity:1;visibility:visible;transition:opacity .3s,visibility 0s}
389
591
  .pkb-card{width:min(400px,100%);background:var(--pk-paper);border:2.5px solid var(--pk-ink);border-radius:22px;box-shadow:6px 6px 0 var(--pk-ink);padding:26px 22px 20px;text-align:center;transform:scale(.6) translateY(30px);transition:transform .45s cubic-bezier(.3,1.5,.5,1)}
390
592
  .pkb-perfect.pkb-show .pkb-card{transform:none}
391
593
  .pkb-medal{width:124px;height:124px;margin:0 auto 12px;border-radius:50%;border:5px solid var(--pk-red);box-shadow:inset 0 0 0 4px var(--pk-paper),inset 0 0 0 7px var(--pk-red);color:var(--pk-red);display:grid;place-content:center;font-family:var(--pk-display);font-size:26px;line-height:1.15;white-space:pre-line;opacity:0;transform:rotate(-14deg) scale(2.6)}
392
594
  .pkb-perfect.pkb-show .pkb-medal{animation:pkb-slam .38s cubic-bezier(.5,0,.75,0) .35s forwards}
595
+ .pkb-perfect.pkb-settled,.pkb-perfect.pkb-settled .pkb-card{transition:none}
596
+ .pkb-perfect.pkb-settled .pkb-medal{animation:none;opacity:1;transform:rotate(-14deg) scale(1)}
393
597
  @keyframes pkb-slam{to{opacity:1;transform:rotate(-14deg) scale(1)}}
394
598
  .pkb-ptitle{font-family:var(--pk-display);font-weight:400;font-size:30px;line-height:1.3;color:inherit}
395
599
  .pkb-pbody{font-weight:800;font-size:13.5px;line-height:1.6;margin:6px 0 14px;white-space:pre-line;word-break:auto-phrase}
600
+ .pkb-pbody:empty{margin:0 0 14px}
396
601
  .pkb-stats{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-bottom:16px}
397
602
  .pkb-stats>div{border:2px solid var(--pk-ink);border-radius:12px;padding:6px 4px 5px;background:#fff}
398
603
  .pkb-stats small{display:block;font-size:9.5px;font-weight:800;letter-spacing:.1em}
@@ -402,10 +607,72 @@ var CSS = `
402
607
  .pkb-fx{position:absolute;inset:0;width:100%;height:100%;z-index:10;pointer-events:none}
403
608
  .pkb-demo{max-width:720px;margin:12px auto 4px;display:flex;flex-wrap:wrap;align-items:center;gap:8px;font-family:var(--pk-body);font-size:12px;color:#666}
404
609
  .pkb-demo button{appearance:none;border:1px solid #ccc;background:#f3f3f3;color:#222;border-radius:999px;padding:5px 12px;font-size:12px;cursor:pointer}
405
- @container (max-width:560px){.pkb-board{padding:18px 14px 16px}.pkb-head{flex-direction:column;align-items:stretch;gap:12px}.pkb-score{display:grid;grid-template-columns:repeat(3,1fr);gap:8px}.pkb-pill,.pkb-pill.pkb-sc{min-width:0;padding:5px 4px 4px}.pkb-pill b{font-size:19px}.pkb-title{font-size:26px}.pkb-grid{gap:max(3px,calc(8px*var(--pk-d)))}.pkb-tile{border-radius:calc(12px*var(--pk-d));box-shadow:3px 3px 0 var(--pk-ink);gap:calc(3px*var(--pk-d));aspect-ratio:1/1}.pkb-emo{font-size:max(13px,calc(24px*var(--pk-s)))}.pkb-lbl{font-size:max(8.5px,calc(10.5px*var(--pk-s)));padding:0 calc(2px*var(--pk-d))}.pkb-stamp{font-size:max(9px,calc(14px*var(--pk-s)))}.pkb-tile.pkb-reach::after{font-size:9px;padding:2px 5px;top:-9px;right:-5px}.pkb-lines .pkb-u{stroke-width:calc(10px*var(--pk-d))}.pkb-lines .pkb-o{stroke-width:calc(5px*var(--pk-d))}.pkb-banner::before{width:380px;height:380px;margin:-190px 0 0 -190px}.pkb-banner span{font-size:12px}.pkb-medal{width:96px;height:96px;font-size:20px}.pkb-card{padding:20px 14px 16px}.pkb-ptitle{font-size:24px}.pkb-stats b{font-size:17px}.pkb-float.pkb-pt{font-size:17px}.pkb-float.pkb-cmb{font-size:13px}}
406
- @container (max-width:400px){.pkb-board{padding:16px 10px 14px}.pkb-grid{gap:max(3px,calc(6px*var(--pk-d)))}.pkb-lbl{font-size:max(8.5px,calc(10px*var(--pk-s)));padding:0 calc(1px*var(--pk-d))}.pkb-emo{font-size:max(12px,calc(22px*var(--pk-s)))}.pkb-stamp{font-size:max(9px,calc(13px*var(--pk-s)))}.pkb-foot{margin-top:14px;font-size:12px}.pkb-perfect{padding:12px}.pkb-stats{gap:6px}.pkb-stats>div{padding:6px 2px 5px}.pkb-stats small{letter-spacing:.02em}}
610
+ .pkb-mode{position:relative;isolation:isolate;display:inline-grid;grid-auto-flow:column;grid-auto-columns:1fr;padding:2px;border-radius:999px;background:var(--pk-ink);transition:box-shadow .4s}
611
+ .pkb-mode::after{content:"";position:absolute;inset:0;z-index:2;border-radius:inherit;pointer-events:none;background:linear-gradient(110deg,transparent 30%,rgba(255,255,255,.75) 45%,transparent 60%) no-repeat;background-size:250% 100%;background-position:200% 0;opacity:0}
612
+ .pkb-mode button{appearance:none;position:relative;z-index:1;border:0;background:none;border-radius:999px;padding:3px 11px 2px;font-weight:800;font-size:11px;line-height:1.4;letter-spacing:.04em;white-space:nowrap;color:var(--pk-paper);opacity:.72;cursor:pointer;transition:color .3s,opacity .3s;-webkit-tap-highlight-color:transparent}
613
+ .pkb-mode button[aria-checked="true"]{opacity:1;color:var(--pk-ink);cursor:default}
614
+ .pkb-mode button:focus-visible{outline:2px solid var(--pk-paper);outline-offset:-4px}
615
+ .pkb-mode-th{position:absolute;top:2px;bottom:2px;left:2px;width:calc(50% - 2px);border-radius:999px;background:var(--pk-sun);transition:transform .55s cubic-bezier(.3,1.45,.45,1)}
616
+ .pkb.pkb-inf .pkb-mode{box-shadow:0 0 14px 1px rgba(155,93,229,.6)}
617
+ .pkb.pkb-inf .pkb-mode::after{opacity:1;animation:pkb-glint 3.2s ease-in-out infinite}
618
+ .pkb.pkb-inf .pkb-mode-th{transform:translateX(100%);background:linear-gradient(90deg,#FF9F1C,#E8473B,#9B5DE5,#2F6FED,#1FA971,#FF9F1C) 0 0/200% 100%;animation:pkb-rb 2.4s linear infinite}
619
+ .pkb.pkb-inf .pkb-mode button[data-m="infinite"]{color:#fff;text-shadow:0 1px 1px rgba(42,31,20,.55)}
620
+ .pkb-board.pkb-waiting .pkb-mode,.pkb-board.pkb-switching .pkb-mode{z-index:15}
621
+ .pkb-board.pkb-switching .pkb-fx{z-index:16}
622
+ .pkb.pkb-inf .pkb-board{border-color:transparent;background:radial-gradient(rgba(42,31,20,.12) 1.3px,transparent 1.6px) 0 0/16px 16px padding-box,linear-gradient(var(--pk-sun),var(--pk-sun)) padding-box,linear-gradient(120deg,#FF9F1C,#E8473B 22%,#9B5DE5 45%,#2F6FED 68%,#1FA971 88%,#FF9F1C) border-box;box-shadow:0 0 0 2px var(--pk-ink),0 10px 34px -10px rgba(155,93,229,.7)}
623
+ .pkb.pkb-inf .pkb-demo [data-r="dP"],.pkb.pkb-inf .pkb-demo [data-r="dR"]{display:none}
624
+ .pkb-ready-b,.pkb-ready-r{display:none}
625
+ .pkb.pkb-inf .pkb-ready-in{border-color:transparent;background:linear-gradient(var(--pk-paper),var(--pk-paper)) padding-box,linear-gradient(120deg,#FF9F1C,#E8473B 22%,#9B5DE5 45%,#2F6FED 68%,#1FA971 88%,#FF9F1C) border-box;box-shadow:0 0 0 2px var(--pk-ink),0 0 32px rgba(155,93,229,.5)}
626
+ .pkb.pkb-inf .pkb-ready-b{display:inline-block;margin-bottom:10px;padding:4px 14px 3px;border-radius:999px;background:linear-gradient(90deg,#FF9F1C,#E8473B,#9B5DE5,#2F6FED,#1FA971,#FF9F1C) 0 0/200% 100%;color:#fff;font-weight:800;font-size:13px;line-height:1.4;letter-spacing:.08em;text-shadow:0 1px 1px rgba(42,31,20,.5);box-shadow:0 0 14px rgba(155,93,229,.55);animation:pkb-rb 2.4s linear infinite}
627
+ .pkb.pkb-inf .pkb-ready-r{display:block;margin-top:8px;font-weight:800;font-size:15px;line-height:1.5;word-break:auto-phrase}
628
+ .pkb-tile.pkb-clr{animation:pkb-clr .62s var(--pkb-cd,0ms) both}
629
+ @keyframes pkb-clr{0%{transform:perspective(600px) rotateY(0);animation-timing-function:cubic-bezier(.55,0,.9,.45)}42%{transform:perspective(600px) rotateY(90deg) scale(.9)}42.01%{transform:perspective(600px) rotateY(-90deg) scale(.9);animation-timing-function:cubic-bezier(.2,1.45,.4,1)}100%{transform:perspective(600px) rotateY(0)}}
630
+ .pkb-mfx{position:absolute;inset:0;z-index:14;display:grid;place-items:center;overflow:hidden;border-radius:inherit;pointer-events:none;visibility:hidden;opacity:0;text-align:center}
631
+ .pkb-mfx.pkb-show{visibility:visible;animation:pkb-mfx 1.7s ease-out forwards}
632
+ @keyframes pkb-mfx{0%{opacity:0}9%{opacity:1}72%{opacity:1}100%{opacity:0}}
633
+ .pkb-mfx-inf{background:radial-gradient(120% 95% at 50% 42%,#33205c 0%,#170f2e 52%,#08060f 100%)}
634
+ .pkb-mfx-n{background:var(--pk-sun) radial-gradient(rgba(42,31,20,.16) 1.3px,transparent 1.6px) 0 0/16px 16px}
635
+ .pkb-mfx::after{content:"";position:absolute;top:-30%;bottom:-30%;left:-50%;width:50%;background:linear-gradient(90deg,transparent,rgba(255,255,255,.55),transparent);transform:skewX(-18deg);opacity:0}
636
+ .pkb-mfx.pkb-show::after{animation:pkb-beam 1s cubic-bezier(.4,0,.2,1) .22s both}
637
+ @keyframes pkb-beam{0%{opacity:1;transform:skewX(-18deg) translateX(0)}100%{opacity:1;transform:skewX(-18deg) translateX(400%)}}
638
+ .pkb-mfx-glow{position:absolute;left:50%;top:46%;width:min(150cqi,1000px);aspect-ratio:1;translate:-50% -50%;border-radius:50%;background:conic-gradient(#FF9F1C,#E8473B,#9B5DE5,#2F6FED,#1FA971,#FF9F1C);-webkit-mask:radial-gradient(circle,transparent 14%,#000 30%,rgba(0,0,0,.4) 48%,transparent 68%);mask:radial-gradient(circle,transparent 14%,#000 30%,rgba(0,0,0,.4) 48%,transparent 68%);filter:blur(28px);opacity:0}
639
+ .pkb-mfx.pkb-show .pkb-mfx-glow{animation:pkb-spin 3s linear infinite,pkb-glow 1.7s ease-out both}
640
+ @keyframes pkb-glow{0%{opacity:0;scale:.35}30%{opacity:.8;scale:1}100%{opacity:.5;scale:1.12}}
641
+ .pkb-mfx i{position:absolute;left:var(--x);top:var(--y);width:calc(20px*var(--s));aspect-ratio:1;background:var(--c);clip-path:polygon(50% 0,62% 38%,100% 50%,62% 62%,50% 100%,38% 62%,0 50%,38% 38%);opacity:0}
642
+ .pkb-mfx.pkb-show i{animation:pkb-twk .85s ease-in-out var(--t) both}
643
+ @keyframes pkb-twk{0%{opacity:0;scale:0;rotate:0deg}45%{opacity:1;scale:1;rotate:70deg}100%{opacity:0;scale:.2;rotate:160deg}}
644
+ .pkb-mfx-in{position:relative;display:flex;flex-direction:column;align-items:center;padding:0 16px}
645
+ .pkb-mfx-sym{display:block;font-family:var(--pk-display);font-weight:400;line-height:1}
646
+ .pkb-mfx-inf .pkb-mfx-sym{font-size:clamp(110px,36cqi,230px);line-height:.9;padding:0 .06em;background:linear-gradient(90deg,#FFE066,#FF9F1C,#E8473B,#9B5DE5,#2F6FED,#1FA971,#FFE066) 0 0/200% 100%;-webkit-background-clip:text;background-clip:text;color:transparent;filter:drop-shadow(0 0 22px rgba(155,93,229,.9)) drop-shadow(0 0 3px rgba(255,255,255,.8))}
647
+ .pkb-mfx.pkb-show.pkb-mfx-inf .pkb-mfx-sym{animation:pkb-sym 1.7s cubic-bezier(.2,1.25,.35,1) both,pkb-rb 1.7s linear both}
648
+ @keyframes pkb-sym{0%{opacity:0;transform:scale(.25) rotate(-30deg)}26%{opacity:1;transform:scale(1.1) rotate(4deg)}38%{transform:scale(1) rotate(0)}100%{opacity:1;transform:scale(1.06)}}
649
+ .pkb-mfx-n .pkb-mfx-sym{display:grid;place-items:center;width:clamp(110px,30cqi,170px);aspect-ratio:1;border-radius:50%;border:5px solid var(--pk-red);box-shadow:inset 0 0 0 4px var(--pk-sun),inset 0 0 0 7px var(--pk-red);color:var(--pk-red);font-size:clamp(34px,10cqi,56px);letter-spacing:.04em;transform:rotate(-14deg)}
650
+ .pkb-mfx.pkb-show.pkb-mfx-n .pkb-mfx-sym{animation:pkb-mslam .42s cubic-bezier(.5,0,.75,0) .08s both}
651
+ @keyframes pkb-mslam{from{opacity:0;transform:rotate(-14deg) scale(2.6)}to{opacity:1;transform:rotate(-14deg) scale(1)}}
652
+ .pkb-mfx-t{display:block;margin-top:6px;font-family:var(--pk-display);font-weight:400;font-size:clamp(24px,8.5cqi,52px);line-height:1.1;letter-spacing:.2em;margin-right:-.2em;white-space:nowrap}
653
+ .pkb-mfx-inf .pkb-mfx-t{color:#fff;text-shadow:0 0 18px rgba(155,93,229,.95),0 0 4px rgba(255,255,255,.9)}
654
+ .pkb-mfx-n .pkb-mfx-t{margin-top:16px;color:var(--pk-ink)}
655
+ .pkb-mfx.pkb-show .pkb-mfx-t{animation:pkb-track 1.7s cubic-bezier(.16,1,.3,1) .12s both}
656
+ @keyframes pkb-track{0%{opacity:0;letter-spacing:.9em;margin-right:-.9em;filter:blur(8px)}40%{opacity:1;letter-spacing:.2em;margin-right:-.2em;filter:blur(0)}100%{opacity:1;letter-spacing:.24em;margin-right:-.24em}}
657
+ .pkb-mfx-s{display:inline-block;margin-top:12px;padding:5px 16px 4px;border-radius:999px;font-weight:800;font-size:14px;line-height:1.4;letter-spacing:.06em}
658
+ .pkb-mfx-inf .pkb-mfx-s{color:#fff;background:rgba(255,255,255,.12);border:1.5px solid rgba(255,255,255,.4)}
659
+ .pkb-mfx-n .pkb-mfx-s{background:var(--pk-ink);color:var(--pk-sun)}
660
+ .pkb-mfx.pkb-show .pkb-mfx-s{animation:pkb-up .6s cubic-bezier(.2,1.3,.4,1) .4s both}
661
+ @keyframes pkb-up{from{opacity:0;transform:translateY(12px)}}
662
+ .pkb-finbtn{display:none;appearance:none;flex:none;border:2px solid var(--pk-ink);border-radius:999px;padding:5px 14px;font-weight:800;font-size:12px;line-height:1.4;letter-spacing:.04em;color:#fff;text-shadow:0 1px 1px rgba(42,31,20,.55);background:linear-gradient(90deg,#FF9F1C,#E8473B,#9B5DE5,#2F6FED,#1FA971,#FF9F1C) 0 0/200% 100%;box-shadow:2px 2px 0 var(--pk-ink),0 0 12px rgba(155,93,229,.45);cursor:pointer;animation:pkb-rb 2.4s linear infinite}
663
+ .pkb.pkb-inf .pkb-finbtn{display:inline-block}
664
+ .pkb-finbtn:active:not(:disabled){transform:translate(2px,2px);box-shadow:none}
665
+ .pkb-finbtn:disabled{cursor:default;opacity:.4;animation:none;box-shadow:2px 2px 0 var(--pk-ink)}
666
+ .pkb-board.pkb-over .pkb-grid{opacity:.55;filter:saturate(.5);transition:opacity .4s,filter .4s}
667
+ .pkb-pbtns{display:flex;flex-wrap:wrap;justify-content:center;gap:10px}
668
+ .pkb-perfect.pkb-fin .pkb-card{border-color:transparent;background:linear-gradient(var(--pk-paper),var(--pk-paper)) padding-box,linear-gradient(120deg,#FF9F1C,#E8473B 22%,#9B5DE5 45%,#2F6FED 68%,#1FA971 88%,#FF9F1C) border-box;box-shadow:0 0 0 2px var(--pk-ink),6px 6px 0 2px var(--pk-ink),0 0 40px rgba(155,93,229,.5)}
669
+ .pkb-perfect.pkb-fin .pkb-medal{border-color:transparent;background:linear-gradient(var(--pk-paper),var(--pk-paper)) padding-box,conic-gradient(#FF9F1C,#E8473B,#9B5DE5,#2F6FED,#1FA971,#FF9F1C) border-box;box-shadow:0 0 18px rgba(155,93,229,.55);color:#7B3FD0;font-size:clamp(44px,14cqi,64px);line-height:1;text-shadow:0 0 12px rgba(155,93,229,.45)}
670
+ .pkb-perfect.pkb-fin .pkb-again{background:linear-gradient(90deg,#FF9F1C,#E8473B,#9B5DE5,#2F6FED,#1FA971,#FF9F1C) 0 0/200% 100%;color:#fff;text-shadow:0 1px 1px rgba(42,31,20,.55);animation:pkb-rb 2.4s linear infinite}
671
+ .pkb-perfect.pkb-fin .pkb-close:not(.pkb-again){background:var(--pk-paper)}
672
+ @container (max-width:560px){.pkb-board{padding:18px 14px 16px}.pkb-head{flex-direction:column;align-items:stretch;gap:12px}.pkb-score{display:grid;grid-template-columns:repeat(3,1fr);gap:8px}.pkb-pill,.pkb-pill.pkb-sc{min-width:0;padding:5px 4px 4px}.pkb-pill b{font-size:19px}.pkb-title{font-size:26px}.pkb-grid{gap:max(3px,calc(8px*var(--pk-d)))}.pkb-tile{border-radius:calc(12px*var(--pk-d));box-shadow:3px 3px 0 var(--pk-ink);gap:calc(3px*var(--pk-d));aspect-ratio:1/1}.pkb-emo{font-size:max(13px,calc(24px*var(--pk-s)))}.pkb-lbl{font-size:max(8.5px,calc(10.5px*var(--pk-s)));padding:0 calc(2px*var(--pk-d))}.pkb-stamp{font-size:max(9px,calc(14px*var(--pk-s)))}.pkb-tile.pkb-reach::after{font-size:9px;padding:2px 5px;top:-9px;right:-5px}.pkb-lines .pkb-u{stroke-width:calc(10px*var(--pk-d))}.pkb-lines .pkb-o{stroke-width:calc(5px*var(--pk-d))}.pkb-banner::before{width:380px;height:380px;margin:-190px 0 0 -190px}.pkb-banner span{font-size:12px}.pkb-medal{width:96px;height:96px;font-size:20px}.pkb-card{padding:20px 14px 16px}.pkb-ptitle{font-size:24px}.pkb-stats b{font-size:17px}.pkb-float.pkb-pt{font-size:17px}.pkb-float.pkb-cmb{font-size:13px}.pkb-ready-in{padding:20px 16px 18px}.pkb-ready-l{font-size:15px;padding:10px 12px}.pkb-ready-go{font-size:15px}.pkb-ptime b{font-size:19px}}
673
+ @container (max-width:400px){.pkb-board{padding:16px 10px 14px}.pkb-mode button{padding:3px 8px 2px}.pkb-grid{gap:max(3px,calc(6px*var(--pk-d)))}.pkb-lbl{font-size:max(8.5px,calc(10px*var(--pk-s)));padding:0 calc(1px*var(--pk-d))}.pkb-emo{font-size:max(12px,calc(22px*var(--pk-s)))}.pkb-stamp{font-size:max(9px,calc(13px*var(--pk-s)))}.pkb-foot{margin-top:14px;font-size:12px}.pkb-perfect,.pkb-ready{padding:12px}.pkb-stats{gap:6px}.pkb-stats>div{padding:6px 2px 5px}.pkb-stats small{letter-spacing:.02em}}
407
674
  @container (max-width:320px){.pkb-board{padding-inline:8px}.pkb-grid{gap:max(3px,calc(5px*var(--pk-d)))}.pkb-lbl{font-size:max(8.5px,calc(9.5px*var(--pk-s)));padding:0}}
408
- @media (prefers-reduced-motion:reduce){.pkb-board.pkb-shake,.pkb-board.pkb-punch,.pkb-flash.pkb-go,.pkb-tile.pkb-hop,.pkb-tile.pkb-squash,.pkb-tile.pkb-reach,.pkb-tile.pkb-line::before,.pkb-pill.pkb-hot,.pkb-bump,.pkb-tile.pkb-reach::after,.pkb-banner::before,.pkb-lines .pkb-draw line,.pkb-kicker.pkb-gold{animation:none}.pkb-banner.pkb-show{animation:pkb-fade 1.8s forwards}.pkb-float{animation:pkb-fade 1s both}.pkb-perfect.pkb-show .pkb-medal{animation:none;opacity:1;transform:rotate(-14deg)}.pkb-card{transition:none}}
675
+ @media (prefers-reduced-motion:reduce){.pkb-board.pkb-shake,.pkb-board.pkb-punch,.pkb-flash.pkb-go,.pkb-tile.pkb-hop,.pkb-tile.pkb-squash,.pkb-tile.pkb-on .pkb-stamp.pkb-hit,.pkb-tile.pkb-reach,.pkb-tile.pkb-line::before,.pkb-pill.pkb-hot,.pkb-bump,.pkb-tile.pkb-reach::after,.pkb-banner::before,.pkb-lines .pkb-draw line,.pkb-kicker.pkb-gold,.pkb-ready-go,.pkb.pkb-inf .pkb-mode-th,.pkb.pkb-inf .pkb-mode::after,.pkb.pkb-inf .pkb-ready-b,.pkb-tile.pkb-clr,.pkb-finbtn,.pkb-perfect.pkb-fin .pkb-again,.pkb-mfx.pkb-show.pkb-mfx-inf .pkb-mfx-sym,.pkb-mfx.pkb-show.pkb-mfx-n .pkb-mfx-sym,.pkb-mfx.pkb-show .pkb-mfx-t,.pkb-mfx.pkb-show .pkb-mfx-s,.pkb-mfx.pkb-show .pkb-mfx-glow,.pkb-mfx.pkb-show::after{animation:none}.pkb-count.pkb-tick,.pkb-count.pkb-go.pkb-tick{animation:pkb-fade 1s forwards}.pkb-mfx.pkb-show{animation:pkb-fade 1.4s forwards}.pkb-mode-th{transition:none}.pkb-ready{transition:none}.pkb-banner.pkb-show{animation:pkb-fade 1.8s forwards}.pkb-float{animation:pkb-fade 1s both}.pkb-perfect.pkb-show .pkb-medal{animation:none;opacity:1;transform:rotate(-14deg)}.pkb-card,.pkb-tile,.pkb-stamp{transition:none}}
409
676
  @keyframes pkb-fade{0%,100%{opacity:0}15%,80%{opacity:1}}
410
677
  `;
411
678
 
@@ -432,7 +699,22 @@ function writeStore(key, value) {
432
699
  }
433
700
  var glintDelay = (c, n) => `${+((Math.floor(c / n) + c % n) * 0.36 / Math.max(1, n - 1)).toFixed(3)}s`;
434
701
  var lineKey = (l) => l.slice().sort((a, b) => a - b).join(",");
702
+ var COUNT_MS = 1e3;
703
+ var GO_MS = 750;
704
+ function formatTime(ms, digits = 1) {
705
+ const unit = 10 ** digits;
706
+ const t = Math.floor(Math.max(0, ms) / (1e3 / unit));
707
+ const frac = digits ? "." + String(t % unit).padStart(digits, "0") : "";
708
+ const sec = Math.floor(t / unit), s = sec % 60, m = Math.floor(sec / 60) % 60, h = Math.floor(sec / 3600);
709
+ const pad = (v) => String(v).padStart(2, "0");
710
+ return (h ? `${h}:${pad(m)}` : String(m)) + `:${pad(s)}${frac}`;
711
+ }
712
+ function speedBonus(items, ms) {
713
+ return Math.round(items * 1e6 / Math.max(ms, 1));
714
+ }
435
715
  var FLY_MS = 560;
716
+ var CLEAR_MS = 1e3;
717
+ var SPARK = ["#FFFFFF", "#FFE066", "#FF9F1C", "#E8473B", "#9B5DE5", "#2F6FED", "#1FA971"];
436
718
  function ensureStyles() {
437
719
  if (document.getElementById("pkb-styles")) return;
438
720
  const s = document.createElement("style");
@@ -440,7 +722,8 @@ function ensureStyles() {
440
722
  s.textContent = CSS;
441
723
  document.head.appendChild(s);
442
724
  }
443
- function template(L, M, { demo, shuffle, vibrate }) {
725
+ var opt = (v, fallback) => v ?? fallback ?? "";
726
+ function template(L, M, { demo, shuffle, vibrate, ready, modes }) {
444
727
  const n = L.size;
445
728
  const input = vibrate ? `<input type="checkbox" switch role="checkbox">` : `<input type="checkbox">`;
446
729
  const tiles = Array.from({ length: n * n }, (_, c) => {
@@ -452,9 +735,15 @@ function template(L, M, { demo, shuffle, vibrate }) {
452
735
  }).join("");
453
736
  const scale = `--pk-n:${n};--pk-s:${+Math.min(1.35, 4 / n).toFixed(3)};--pk-d:${+Math.min(1, 4 / n).toFixed(3)}`;
454
737
  const demoBar = demo ? `<div class="pkb-demo"><span>${esc(M.demoLabel)}</span><button type="button" data-r="dB">${esc(M.demoBingo)}</button><button type="button" data-r="dP">${esc(M.demoPerfect)}</button><button type="button" data-r="dR">${esc(M.demoRestore)}</button></div>` : "";
738
+ const hints = [opt(M.soundHint, ja.soundHint), vibrate ? opt(M.vibrationHint, ja.vibrationHint) : ""].filter((h) => h.trim());
739
+ const prompt = opt(M.readyPrompt, ja.readyPrompt);
740
+ const rule = opt(M.infiniteRule, ja.infiniteRule);
741
+ const readyScreen = ready ? `<button type="button" class="pkb-ready" data-r="ready"><span class="pkb-ready-in"><span class="pkb-ready-b">\u221E ${esc(M.infiniteSub?.trim() || ja.infiniteSub)}</span><b class="pkb-ready-t">${esc(M.readyTitle?.trim() || ja.readyTitle)}</b>` + (prompt.trim() ? `<span class="pkb-ready-q">${esc(prompt)}</span>` : "") + (rule.trim() ? `<span class="pkb-ready-r">${esc(rule)}</span>` : "") + (hints.length ? `<span class="pkb-ready-l">${hints.map((h) => `<span>${esc(h)}</span>`).join("")}</span>` : "") + `<span class="pkb-ready-go">${esc(M.readyTap?.trim() || ja.readyTap)}</span></span><b class="pkb-count" data-r="count" aria-live="assertive"></b></button>` : "";
742
+ const timeLabel = M.time?.trim() || ja.time;
743
+ const modeSwitch = modes ? `<span class="pkb-mode" data-r="mode" role="radiogroup" aria-label="${esc(M.modeLabel?.trim() || ja.modeLabel)}"><i class="pkb-mode-th" data-r="modeTh"></i><button type="button" role="radio" data-m="normal">${esc(M.modeNormal?.trim() || ja.modeNormal)}</button><button type="button" role="radio" data-m="infinite">${esc(M.modeInfinite?.trim() || ja.modeInfinite)}</button></span>` : "";
455
744
  return `<div class="pkb" style="${scale}"><div class="pkb-board">
456
745
  <div class="pkb-head"><div>
457
- <div class="pkb-tags"><span class="pkb-kicker" data-r="kick">${esc(M.tag)}</span><button type="button" class="pkb-snd" data-r="snd"></button></div>
746
+ <div class="pkb-tags"><span class="pkb-kicker" data-r="kick">${esc(M.tag)}</span>${modeSwitch}<button type="button" class="pkb-snd" data-r="snd"></button><span class="pkb-watch" data-r="watch" role="timer" aria-label="${esc(timeLabel)}">${formatTime(0)}</span></div>
458
747
  <h2 class="pkb-title">${esc(M.title)}</h2>${M.subtitle ? `<p class="pkb-sub">${esc(M.subtitle)}</p>` : ""}</div>
459
748
  <div class="pkb-side"><div class="pkb-score">
460
749
  <div class="pkb-pill"><small>${esc(M.packed)}</small><b data-r="cnt"></b></div>
@@ -463,13 +752,14 @@ function template(L, M, { demo, shuffle, vibrate }) {
463
752
  <div class="pkb-combo" data-r="combo"><b data-r="cmbN"></b><span class="pkb-bar"><i data-r="cbar"></i></span></div></div></div>
464
753
  <div class="pkb-gw"><div class="pkb-grid">${tiles}</div><svg class="pkb-lines" aria-hidden="true"></svg>
465
754
  <div class="pkb-banner" data-r="banner" aria-hidden="true"><b data-r="bT"></b><span data-r="bS"></span></div></div>
466
- <div class="pkb-foot"><span data-r="msg" aria-live="polite"></span><div class="pkb-acts">${shuffle ? `<button type="button" class="pkb-shuf" data-r="shuf">${esc(M.shuffle)}</button>` : ""}<button type="button" class="pkb-reset" data-r="reset">${esc(M.reset)}</button></div></div>
755
+ <div class="pkb-foot"><span data-r="msg" aria-live="polite"></span><div class="pkb-acts"><button type="button" class="pkb-finbtn" data-r="fin" disabled>${esc(M.finish?.trim() || ja.finish)}</button>${shuffle ? `<button type="button" class="pkb-shuf" data-r="shuf">${esc(M.shuffle)}</button>` : ""}<button type="button" class="pkb-reset" data-r="reset">${esc(M.reset)}</button></div></div>
467
756
  <div class="pkb-flash" data-r="flash"></div>
468
- <div class="pkb-perfect" data-r="perfect"><div class="pkb-card" role="dialog" aria-label="${esc(M.perfectTitle)}">
469
- <div class="pkb-medal">${esc(M.perfectStamp)}</div><h3 class="pkb-ptitle">${esc(M.perfectTitle)}</h3><p class="pkb-pbody" data-r="pbody">${esc(perfectText(M, L.items.length))}</p>
757
+ <div class="pkb-perfect" data-r="perfect"><div class="pkb-card" data-r="pcard" role="dialog" aria-label="${esc(M.perfectTitle)}">
758
+ <div class="pkb-medal" data-r="medal">${esc(M.perfectStamp)}</div><h3 class="pkb-ptitle" data-r="ptitle">${esc(M.perfectTitle)}</h3><p class="pkb-pbody" data-r="pbody">${esc(perfectText(M, L.items.length))}</p>
759
+ <p class="pkb-ptime" data-r="ptime" hidden><small data-r="pTl">${esc(M.statTime?.trim() || ja.statTime)}</small><b data-r="pT"></b></p>
470
760
  <div class="pkb-stats"><div><small>${esc(M.statScore)}</small><b data-r="sScore">0</b></div><div><small>${esc(M.statCombo)}</small><b data-r="sCombo">\xD70</b></div><div><small>${esc(M.statLines)}</small><b data-r="sBingo">0</b></div></div>
471
- <button type="button" class="pkb-close" data-r="close">${esc(M.close)}</button></div></div>
472
- <canvas class="pkb-fx"></canvas></div>${demoBar}</div>`;
761
+ <div class="pkb-pbtns"><button type="button" class="pkb-close pkb-again" data-r="again" hidden>${esc(M.again?.trim() || ja.again)}</button><button type="button" class="pkb-close" data-r="close">${esc(M.close)}</button></div></div></div>
762
+ <canvas class="pkb-fx"></canvas>${readyScreen}<div class="pkb-mfx" data-r="mfx" aria-hidden="true"></div></div>${demoBar}</div>`;
473
763
  }
474
764
  var perfectText = (M, total) => typeof M.perfectBody === "function" ? M.perfectBody(total) : M.perfectBody;
475
765
  function createPackingBingo(container, options = {}) {
@@ -501,7 +791,15 @@ function createPackingBingo(container, options = {}) {
501
791
  const orderKey = storageKey && ORDER_PREFIX + storageKey;
502
792
  const demo = !!options.demo;
503
793
  const vibrate = options.vibrate !== false;
794
+ const uncheck = options.uncheck === true;
504
795
  const calm = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
796
+ const haptics = createHaptics(vibrate);
797
+ let mode = options.mode === "infinite" ? "infinite" : "normal";
798
+ const inf = () => mode === "infinite";
799
+ let packing = null;
800
+ let cleared = 0;
801
+ const pending = /* @__PURE__ */ new Set();
802
+ let clearTimer = 0, clearAt = 0;
505
803
  let dead = false;
506
804
  const timers = /* @__PURE__ */ new Set();
507
805
  const later = (fn, ms) => {
@@ -514,12 +812,19 @@ function createPackingBingo(container, options = {}) {
514
812
  const wait = (ms) => new Promise((r) => window.setTimeout(r, ms));
515
813
  let gen = 0;
516
814
  ensureStyles();
517
- container.innerHTML = template(L, M, { demo, shuffle: options.shuffle !== false, vibrate });
815
+ container.innerHTML = template(L, M, {
816
+ demo,
817
+ shuffle: options.shuffle !== false,
818
+ vibrate,
819
+ ready: options.ready !== false,
820
+ modes: options.modeSwitch !== false
821
+ });
518
822
  const $ = (r) => {
519
823
  const el = container.querySelector(`[data-r="${r}"]`);
520
824
  if (!el) throw new Error(`[packing-bingo] internal: missing element "${r}"`);
521
825
  return el;
522
826
  };
827
+ const root = container.querySelector(".pkb");
523
828
  const board = container.querySelector(".pkb-board");
524
829
  const gw = container.querySelector(".pkb-gw");
525
830
  const grid = container.querySelector(".pkb-grid");
@@ -530,12 +835,15 @@ function createPackingBingo(container, options = {}) {
530
835
  const tiles = L.itemCell.map((c) => cellTiles[c]);
531
836
  const freeTile = L.center >= 0 ? cellTiles[L.center] : null;
532
837
  const ins = tiles.map((t) => t.querySelector("input"));
533
- const kick = $("kick"), snd = $("snd"), perfectEl = $("perfect");
838
+ const stamps = tiles.map((t) => t.querySelector(".pkb-stamp"));
839
+ const kick = $("kick"), snd = $("snd"), perfectEl = $("perfect"), mfx = $("mfx");
534
840
  let checked = normalize(void 0);
535
841
  let cur = [];
536
842
  let prevLines = /* @__PURE__ */ new Set();
537
843
  let prevReach = /* @__PURE__ */ new Set();
538
844
  let prevAll = false;
845
+ let perfectPending = false, restorePerfectFocus = false;
846
+ let perfectBeat;
539
847
  let lastN = 0;
540
848
  let score = 0, shown = 0, combo = 0, maxCombo = 0, lastT = 0, gotP = false;
541
849
  const got = /* @__PURE__ */ new Set(), gotL = /* @__PURE__ */ new Set();
@@ -549,6 +857,7 @@ function createPackingBingo(container, options = {}) {
549
857
  snd.addEventListener("click", () => {
550
858
  muted = !muted;
551
859
  writeStore(MUTE_KEY, muted ? "1" : "0");
860
+ if (muted) sfx.stop();
552
861
  setSnd();
553
862
  if (!muted) sfx.pop(4);
554
863
  });
@@ -563,7 +872,7 @@ function createPackingBingo(container, options = {}) {
563
872
  cv.width = Math.round(BW * dpr);
564
873
  cv.height = Math.round(BH * dpr);
565
874
  };
566
- const piece = (x, y, vx, vy, g, s, life, shape2) => ({
875
+ const piece = (x, y, vx, vy, g, s, life, shape2, colors = CONFETTI) => ({
567
876
  x,
568
877
  y,
569
878
  vx,
@@ -575,7 +884,7 @@ function createPackingBingo(container, options = {}) {
575
884
  h: (3 + Math.random() * 5) * s,
576
885
  r: Math.random() * 6,
577
886
  vr: (Math.random() - 0.5) * 0.35,
578
- c: CONFETTI[Math.random() * CONFETTI.length | 0]
887
+ c: colors[Math.random() * colors.length | 0]
579
888
  });
580
889
  const go = () => {
581
890
  if (!raf && ctx) raf = requestAnimationFrame(tick);
@@ -586,7 +895,7 @@ function createPackingBingo(container, options = {}) {
586
895
  const a = (o.angle ?? -Math.PI / 2) + (Math.random() - 0.5) * (o.spread ?? Math.PI * 2);
587
896
  const v = (o.speed ?? 10) * (0.35 + Math.random() * 0.85);
588
897
  const sh = Math.random() < (o.star ?? 0.15) ? "star" : Math.random() < 0.3 ? "round" : "rect";
589
- P.push(piece(x, y, Math.cos(a) * v, Math.sin(a) * v, o.g ?? 0.28, o.size ?? 1, (o.life ?? 110) + Math.random() * 40, sh));
898
+ P.push(piece(x, y, Math.cos(a) * v, Math.sin(a) * v, o.g ?? 0.28, o.size ?? 1, (o.life ?? 110) + Math.random() * 40, sh, o.colors));
590
899
  }
591
900
  go();
592
901
  }
@@ -681,8 +990,10 @@ function createPackingBingo(container, options = {}) {
681
990
  };
682
991
  grid.addEventListener("animationend", (e) => {
683
992
  const t = e.target;
993
+ if (e.animationName === "pkb-stamp-hit") t.classList.remove("pkb-hit");
684
994
  if (t.classList.contains("pkb-tile") && (e.animationName === "pkb-hop" || e.animationName === "pkb-squash"))
685
995
  t.classList.remove(e.animationName);
996
+ if (t.classList.contains("pkb-tile") && e.animationName === "pkb-clr") t.classList.remove("pkb-clr");
686
997
  });
687
998
  const ring = (t) => {
688
999
  const r = document.createElement("span");
@@ -690,15 +1001,6 @@ function createPackingBingo(container, options = {}) {
690
1001
  t.appendChild(r);
691
1002
  r.addEventListener("animationend", () => r.remove());
692
1003
  };
693
- const buzz = (pattern) => {
694
- if (!vibrate) return;
695
- const ua = navigator.userActivation;
696
- if (ua && !ua.hasBeenActive) return;
697
- try {
698
- navigator.vibrate?.(pattern);
699
- } catch {
700
- }
701
- };
702
1004
  const floatText = (txt, x, y, cls, delay = 0, color) => {
703
1005
  const el = document.createElement("div");
704
1006
  el.className = "pkb-float pkb-" + cls;
@@ -710,20 +1012,20 @@ function createPackingBingo(container, options = {}) {
710
1012
  board.appendChild(el);
711
1013
  el.addEventListener("animationend", () => el.remove());
712
1014
  };
713
- const prog = (t, t0, d) => Math.max(0, Math.min(1, (t - t0) / d));
1015
+ const prog = (t, t02, d) => Math.max(0, Math.min(1, (t - t02) / d));
714
1016
  const ease = (k) => 1 - Math.pow(1 - k, 3);
715
1017
  let tw = 0;
716
1018
  function countScore() {
717
1019
  cancelAnimationFrame(tw);
718
- const a = shown, b = score, t0 = performance.now();
1020
+ const a = shown, b = score, t02 = performance.now();
719
1021
  const st = (t) => {
720
1022
  if (dead) return;
721
- const k = prog(t, t0, 450);
1023
+ const k = prog(t, t02, 450);
722
1024
  shown = Math.round(a + (b - a) * ease(k));
723
1025
  $("sc").textContent = shown.toLocaleString();
724
1026
  if (k < 1) tw = requestAnimationFrame(st);
725
1027
  };
726
- st(t0);
1028
+ st(t02);
727
1029
  if (!calm) replay($("sp"), "pkb-bump");
728
1030
  }
729
1031
  const showGain = (pts, x, y, cls = "pt") => {
@@ -731,12 +1033,12 @@ function createPackingBingo(container, options = {}) {
731
1033
  countScore();
732
1034
  };
733
1035
  function countUp(el, a, b, d, fmt, ticks = false) {
734
- const t0 = performance.now();
1036
+ const t02 = performance.now();
735
1037
  const g = gen;
736
1038
  let lt = 0;
737
1039
  const st = (t) => {
738
1040
  if (dead || g !== gen) return;
739
- const k = prog(t, t0, d);
1041
+ const k = prog(t, t02, d);
740
1042
  el.textContent = fmt(Math.round(a + (b - a) * ease(k)));
741
1043
  if (ticks && k < 1 && t - lt > 55) {
742
1044
  sfx.tick();
@@ -745,7 +1047,7 @@ function createPackingBingo(container, options = {}) {
745
1047
  if (k < 1) requestAnimationFrame(st);
746
1048
  else if (!calm) replay(el, "pkb-bump");
747
1049
  };
748
- st(t0);
1050
+ st(t02);
749
1051
  }
750
1052
  const ctr = (t) => {
751
1053
  return { x: t.offsetLeft + t.offsetWidth / 2, y: t.offsetTop + t.offsetHeight / 2 };
@@ -764,25 +1066,147 @@ function createPackingBingo(container, options = {}) {
764
1066
  return `<g${fresh.includes(k) ? ' class="pkb-draw"' : ""}><line class="pkb-u" ${at}/><line class="pkb-o" ${at}/></g>`;
765
1067
  }).join("");
766
1068
  }
767
- function tapFx(i, on) {
1069
+ const readyEl = container.querySelector('[data-r="ready"]');
1070
+ const watch = $("watch");
1071
+ let phase = "idle";
1072
+ let t0 = 0, watchTimer = 0;
1073
+ let finalMs = null;
1074
+ const cdTimers = /* @__PURE__ */ new Set();
1075
+ const cd = (fn, ms) => {
1076
+ const id = window.setTimeout(() => {
1077
+ cdTimers.delete(id);
1078
+ if (!dead) fn();
1079
+ }, ms);
1080
+ cdTimers.add(id);
1081
+ };
1082
+ const stopTimers = () => {
1083
+ cdTimers.forEach((id) => clearTimeout(id));
1084
+ cdTimers.clear();
1085
+ clearInterval(watchTimer);
1086
+ };
1087
+ const elapsed = () => phase === "play" ? performance.now() - t0 : finalMs ?? 0;
1088
+ const showWatch = () => {
1089
+ watch.textContent = formatTime(elapsed());
1090
+ };
1091
+ function startWatch() {
1092
+ phase = "play";
1093
+ t0 = performance.now();
1094
+ finalMs = null;
1095
+ clearInterval(watchTimer);
1096
+ watchTimer = window.setInterval(showWatch, 100);
1097
+ watch.classList.remove("pkb-done");
1098
+ watch.classList.add("pkb-run");
1099
+ showWatch();
1100
+ syncFin();
1101
+ }
1102
+ const finBtn = $("fin");
1103
+ const shufBtn = container.querySelector('[data-r="shuf"]');
1104
+ const syncFin = () => {
1105
+ finBtn.disabled = !(inf() && phase === "play");
1106
+ if (shufBtn) shufBtn.disabled = inf() && phase === "done";
1107
+ };
1108
+ function finish() {
1109
+ if (phase === "play") {
1110
+ finalMs = performance.now() - t0;
1111
+ watch.classList.replace("pkb-run", "pkb-done");
1112
+ } else if (phase !== "done") {
1113
+ stopTimers();
1114
+ openGrid(false);
1115
+ }
1116
+ phase = "done";
1117
+ clearInterval(watchTimer);
1118
+ showWatch();
1119
+ }
1120
+ function openGrid(fade) {
1121
+ grid.removeAttribute("inert");
1122
+ if (!readyEl || readyEl.hidden) return;
1123
+ const focused = document.activeElement === readyEl;
1124
+ readyEl.classList.add("pkb-gone");
1125
+ board.classList.remove("pkb-waiting");
1126
+ readyEl.tabIndex = -1;
1127
+ if (focused) grid.querySelector("input")?.focus({ preventScroll: true });
1128
+ if (fade) cd(() => readyEl.hidden = true, GO_MS);
1129
+ else readyEl.hidden = true;
1130
+ }
1131
+ function toReady() {
1132
+ stopTimers();
1133
+ finalMs = null;
1134
+ watch.classList.remove("pkb-run", "pkb-done");
1135
+ phase = readyEl ? "ready" : "idle";
1136
+ showWatch();
1137
+ syncFin();
1138
+ board.classList.remove("pkb-over");
1139
+ grid.removeAttribute("inert");
1140
+ if (!readyEl) return;
1141
+ readyEl.hidden = false;
1142
+ readyEl.removeAttribute("tabindex");
1143
+ readyEl.classList.remove("pkb-gone", "pkb-counting");
1144
+ board.classList.add("pkb-waiting");
1145
+ $("count").textContent = "";
1146
+ grid.setAttribute("inert", "");
1147
+ }
1148
+ let resetAsked = false;
1149
+ const restartGame = () => {
1150
+ if (!controlled || !checked.some(Boolean)) toReady();
1151
+ else resetAsked = true;
1152
+ };
1153
+ function skipReady() {
1154
+ if (phase === "play" || phase === "done") return;
1155
+ stopTimers();
1156
+ openGrid(false);
1157
+ startWatch();
1158
+ }
1159
+ function countdown() {
1160
+ if (phase !== "ready") return;
1161
+ phase = "count";
1162
+ readyEl.classList.add("pkb-counting");
1163
+ const count = $("count");
1164
+ const words = ["3", "2", "1", M.readyGo?.trim() || ja.readyGo];
1165
+ words.forEach((word, k) => {
1166
+ const go2 = k === words.length - 1;
1167
+ const step = () => {
1168
+ count.textContent = word;
1169
+ count.classList.toggle("pkb-go", go2);
1170
+ replay(count, "pkb-tick");
1171
+ sfx.count(go2);
1172
+ haptics.play(go2 ? "go" : "count");
1173
+ if (go2) {
1174
+ startWatch();
1175
+ openGrid(true);
1176
+ }
1177
+ };
1178
+ if (k) cd(step, k * COUNT_MS);
1179
+ else step();
1180
+ });
1181
+ }
1182
+ readyEl?.addEventListener("click", countdown);
1183
+ function tapFx(i, on, moment, fresh) {
768
1184
  const p = rel(tiles[i]);
769
1185
  if (!on) {
1186
+ stamps[i].classList.remove("pkb-hit");
770
1187
  combo = 0;
771
1188
  $("combo").classList.remove("pkb-on");
772
1189
  sfx.off();
773
- buzz(8);
1190
+ haptics.play("off");
774
1191
  return;
775
1192
  }
776
1193
  const now = performance.now();
777
1194
  combo = now - lastT < COMBO_MS ? combo + 1 : 1;
778
1195
  lastT = now;
779
1196
  maxCombo = Math.max(maxCombo, combo);
1197
+ const seqLength = new Set(fresh.flatMap((k) => cellLines[k])).size;
1198
+ const impactAt = Math.min(70 * seqLength, 300) + 60;
1199
+ const beat = haptics.play(moment, combo, fresh.length, impactAt);
1200
+ perfectBeat = moment === "perfect" ? beat : void 0;
1201
+ const energy = Math.min(3, Math.floor(combo / 3));
780
1202
  sfx.pop(combo - 1);
781
1203
  if (!calm) {
1204
+ tiles[i].style.setProperty("--pkb-impact", String(energy));
782
1205
  replay(tiles[i], "pkb-squash");
1206
+ replay(stamps[i], "pkb-hit");
783
1207
  ring(tiles[i]);
784
1208
  }
785
- burst(p.x, p.y, 18, { speed: 6, g: 0.2, life: 30, size: 0.6, star: 0.5 });
1209
+ burst(p.x, p.y, 18 + energy * 6, { speed: 6 + energy, g: 0.2, life: 30, size: 0.6, star: 0.5 });
786
1210
  if (!got.has(i)) {
787
1211
  got.add(i);
788
1212
  score += 10 * combo;
@@ -791,9 +1215,10 @@ function createPackingBingo(container, options = {}) {
791
1215
  if (combo >= 2) {
792
1216
  floatText(M.comboPop(combo), p.x, p.y + 22, "cmb", 90, COMBO_COLORS[(combo - 2) % COMBO_COLORS.length]);
793
1217
  $("cmbN").textContent = M.combo(combo);
794
- replay($("combo"), "pkb-on");
1218
+ $("combo").classList.add("pkb-on");
1219
+ replay($("cbar"), "pkb-draining");
1220
+ if (!calm) replay($("cmbN"), "pkb-bump");
795
1221
  }
796
- buzz(18);
797
1222
  }
798
1223
  $("cbar").addEventListener("animationend", () => {
799
1224
  $("combo").classList.remove("pkb-on");
@@ -806,7 +1231,7 @@ function createPackingBingo(container, options = {}) {
806
1231
  });
807
1232
  later(() => sfx.reach(), 150);
808
1233
  }
809
- function bingo(fresh, pts) {
1234
+ function bingo(fresh, pts, total) {
810
1235
  const nl = fresh.length;
811
1236
  const seq = [...new Set(fresh.flatMap((k) => cellLines[k]))].map((c) => cellTiles[c]);
812
1237
  const step = Math.min(70, 300 / seq.length);
@@ -820,7 +1245,7 @@ function createPackingBingo(container, options = {}) {
820
1245
  );
821
1246
  later(() => {
822
1247
  $("bT").textContent = nl >= 3 ? M.bannerTriple : nl === 2 ? M.bannerDouble : M.bannerSingle;
823
- $("bS").textContent = nl > 1 ? M.bannerMulti(nl) : M.bannerNth(cur.length);
1248
+ $("bS").textContent = nl > 1 ? M.bannerMulti(nl) : M.bannerNth(total);
824
1249
  replay($("banner"), "pkb-show");
825
1250
  if (!calm) {
826
1251
  replay($("flash"), "pkb-go");
@@ -829,54 +1254,126 @@ function createPackingBingo(container, options = {}) {
829
1254
  const c = rel(gw);
830
1255
  burst(c.x, c.y, nl > 1 ? 180 : 110, { speed: nl > 1 ? 16 : 13, star: 0.3 });
831
1256
  sfx.bingo(nl);
832
- buzz(nl > 1 ? [40, 60, 40, 60, 90] : [40, 60, 40]);
833
1257
  if (pts) later(() => showGain(pts, c.x, c.y + 105, "big"), 350);
834
1258
  }, seq.length * step + 60);
835
1259
  }
836
1260
  function perfect(events) {
1261
+ const beat = perfectBeat;
1262
+ perfectPending = true;
1263
+ perfectEl.classList.remove("pkb-settled");
837
1264
  const bonus = gotP ? 0 : 1e3;
1265
+ const speed = gotP || finalMs == null ? 0 : speedBonus(N, finalMs);
838
1266
  gotP = true;
839
- score += bonus;
1267
+ score += bonus + speed;
840
1268
  cellTiles.forEach((t, c) => hop(t, far(c) * 180 / Math.max(1, n - 1)));
841
1269
  sfx.perfect();
842
1270
  if (!calm) replay($("flash"), "pkb-go");
1271
+ setResult(false);
843
1272
  $("pbody").textContent = perfectText(M, N);
1273
+ $("ptime").hidden = finalMs == null;
1274
+ $("pT").textContent = formatTime(finalMs ?? 0, 2);
844
1275
  $("sScore").textContent = "0";
845
1276
  $("sCombo").textContent = "\xD70";
846
1277
  $("sBingo").textContent = "0";
847
1278
  later(() => {
1279
+ perfectPending = false;
848
1280
  replay(perfectEl, "pkb-show");
849
1281
  $("close").focus({ preventScroll: true });
850
1282
  later(() => {
851
1283
  if (!calm) replay(board, "pkb-shake");
852
1284
  sfx.boom();
1285
+ beat?.(PERFECT_STAMP_BEAT);
853
1286
  burst(0, BH, 90, { angle: -Math.PI / 3, spread: 0.8, speed: 22, g: 0.32, star: 0.3 });
854
1287
  burst(BW, BH, 90, { angle: -Math.PI * 2 / 3, spread: 0.8, speed: 22, g: 0.32, star: 0.3 });
855
1288
  rain(3500);
856
- buzz([60, 80, 60, 80, 200]);
857
1289
  if (bonus) floatText(M.perfectBonus(bonus), BW / 2, BH * 0.16, "big");
1290
+ const gap = Math.min(34, Math.max(14, BW * 0.056)) * 1.3 + 6;
1291
+ if (speed) floatText((M.speedBonus ?? ja.speedBonus)(speed), BW / 2, Math.max(gap / 2 + 4, BH * 0.16 - gap), "big");
858
1292
  countUp($("sScore"), 0, score, 1300, (v) => v.toLocaleString(), true);
859
1293
  countUp($("sCombo"), 0, maxCombo, 700, (v) => "\xD7" + v);
860
- countUp($("sBingo"), 0, cur.length, 700, String);
1294
+ countUp($("sBingo"), 0, resultLines(), 700, String);
861
1295
  countScore();
862
- }, 720);
863
- [1300, 1650, 2e3, 2350, 2700].forEach(
864
- (t) => later(() => firework(BW * (0.15 + Math.random() * 0.7), BH * (0.12 + Math.random() * 0.3)), t)
865
- );
1296
+ }, PERFECT_STAMP_DELAY_MS);
866
1297
  later(() => emojiRain(3e3), 1500);
867
- }, 520);
868
- const e = { score, maxCombo };
1298
+ }, PERFECT_REVEAL_MS);
1299
+ PERFECT_FIREWORKS.forEach(
1300
+ ({ at, beat: pulse }) => later(() => {
1301
+ firework(BW * (0.15 + Math.random() * 0.7), BH * (0.12 + Math.random() * 0.3));
1302
+ beat?.(pulse);
1303
+ }, at)
1304
+ );
1305
+ const e = { score, maxCombo, time: finalMs == null ? null : Math.round(finalMs), speedBonus: speed };
869
1306
  events.push(() => options.onPerfect?.(e));
870
1307
  }
1308
+ function clearNow(animate) {
1309
+ clearTimeout(clearTimer);
1310
+ clearTimer = 0;
1311
+ if (!pending.size) return;
1312
+ const gone = [...pending].filter((i) => checked[i]);
1313
+ pending.clear();
1314
+ checked = checked.slice();
1315
+ gone.forEach((i) => {
1316
+ checked[i] = false;
1317
+ ins[i].checked = false;
1318
+ got.delete(i);
1319
+ });
1320
+ gotL.clear();
1321
+ const left = completeLines();
1322
+ left.forEach((k) => gotL.add(lineKey(lines[k])));
1323
+ prevLines = new Set(left);
1324
+ render(null, true);
1325
+ if (!animate || !gone.length) return;
1326
+ sfx.clear(gone.length);
1327
+ gone.forEach((i, j) => {
1328
+ const t = tiles[i];
1329
+ if (!calm) {
1330
+ t.style.setProperty("--pkb-cd", Math.min(j * 28, 280) + "ms");
1331
+ t.classList.remove("pkb-squash", "pkb-hop");
1332
+ replay(t, "pkb-clr");
1333
+ }
1334
+ const p = rel(t);
1335
+ burst(p.x, p.y, 9, { speed: 4.5, g: 0.06, life: 45, size: 0.75, star: 1, colors: SPARK.slice(1) });
1336
+ });
1337
+ }
1338
+ function scheduleClear(ms) {
1339
+ const at = performance.now() + ms;
1340
+ if (clearTimer && at <= clearAt) return;
1341
+ clearTimeout(clearTimer);
1342
+ clearAt = at;
1343
+ clearTimer = window.setTimeout(() => {
1344
+ clearTimer = 0;
1345
+ if (!dead) clearNow(true);
1346
+ }, ms);
1347
+ }
1348
+ const dropClear = () => {
1349
+ clearTimeout(clearTimer);
1350
+ clearTimer = 0;
1351
+ pending.clear();
1352
+ };
1353
+ function setResult(fin) {
1354
+ const title = fin ? M.finishTitle?.trim() || ja.finishTitle : M.perfectTitle;
1355
+ perfectEl.classList.toggle("pkb-fin", fin);
1356
+ $("pcard").setAttribute("aria-label", title);
1357
+ $("medal").textContent = fin ? M.finishStamp?.trim() || ja.finishStamp : M.perfectStamp;
1358
+ $("ptitle").textContent = title;
1359
+ $("pTl").textContent = fin ? M.finishTime?.trim() || ja.finishTime : M.statTime?.trim() || ja.statTime;
1360
+ $("again").hidden = !fin;
1361
+ }
1362
+ const resultLines = () => inf() ? cleared : cur.length;
1363
+ const resultFocus = () => $(perfectEl.classList.contains("pkb-fin") ? "again" : "close");
871
1364
  function cancelFx() {
1365
+ if (pending.size) clearNow(false);
872
1366
  gen++;
1367
+ perfectPending = false;
873
1368
  timers.forEach((id) => clearTimeout(id));
874
1369
  timers.clear();
1370
+ haptics.stop();
1371
+ sfx.stop();
875
1372
  cancelAnimationFrame(tw);
876
1373
  shown = score;
877
1374
  $("sc").textContent = score.toLocaleString();
878
1375
  }
879
- function clearVisuals() {
1376
+ function clearVisuals(keepPerfect = false) {
880
1377
  P = [];
881
1378
  cancelAnimationFrame(raf);
882
1379
  raf = 0;
@@ -885,18 +1382,33 @@ function createPackingBingo(container, options = {}) {
885
1382
  $("banner").classList.remove("pkb-show");
886
1383
  $("flash").classList.remove("pkb-go");
887
1384
  $("combo").classList.remove("pkb-on");
888
- perfectEl.classList.remove("pkb-show");
1385
+ $("cbar").classList.remove("pkb-draining");
1386
+ cellTiles.forEach((t) => t.classList.remove("pkb-squash", "pkb-hop", "pkb-clr"));
1387
+ stamps.forEach((s) => s.classList.remove("pkb-hit"));
1388
+ board.classList.remove("pkb-punch", "pkb-shake", "pkb-switching");
1389
+ mfx.classList.remove("pkb-show");
1390
+ if (!keepPerfect) perfectEl.classList.remove("pkb-show", "pkb-settled");
889
1391
  }
890
1392
  const completeLines = () => lines.flatMap((l, k) => l.every((i) => checked[i]) ? [k] : []);
891
1393
  function render(src, quiet) {
892
1394
  const events = [];
893
1395
  const on = checked, c = on.filter(Boolean).length;
1396
+ const endless = inf();
1397
+ const all = c === N && !endless;
894
1398
  cur = completeLines();
895
1399
  const fresh = cur.filter((k) => !prevLines.has(k));
896
1400
  if ([...prevLines].some((k) => !cur.includes(k))) {
897
1401
  cancelFx();
898
1402
  if (c < N) perfectEl.classList.remove("pkb-show");
899
1403
  }
1404
+ if (endless) {
1405
+ if (!quiet) cleared += fresh.length;
1406
+ if (cur.length) {
1407
+ const known = pending.size;
1408
+ cur.forEach((k) => lines[k].forEach((i) => pending.add(i)));
1409
+ if (pending.size > known || !clearTimer) scheduleClear(!quiet && fresh.length ? CLEAR_MS : FLY_MS + 200);
1410
+ }
1411
+ }
900
1412
  const hit = new Set(cur.flatMap((k) => cellLines[k]));
901
1413
  const reach = /* @__PURE__ */ new Set();
902
1414
  for (const l of lines) {
@@ -905,34 +1417,43 @@ function createPackingBingo(container, options = {}) {
905
1417
  }
906
1418
  tiles.forEach((t, i) => {
907
1419
  t.classList.toggle("pkb-on", on[i]);
1420
+ const lock = on[i] && (!uncheck || pending.has(i));
1421
+ t.classList.toggle("pkb-lock", lock);
1422
+ if (lock) ins[i].setAttribute("aria-readonly", "true");
1423
+ else ins[i].removeAttribute("aria-readonly");
908
1424
  t.classList.toggle("pkb-line", hit.has(L.itemCell[slotOf[i]]));
909
1425
  t.classList.toggle("pkb-reach", reach.has(i));
910
1426
  });
911
1427
  freeTile?.classList.toggle("pkb-line", hit.has(L.center));
912
- board.classList.toggle("pkb-all", c === N);
913
- kick.classList.toggle("pkb-gold", c === N);
914
- kick.textContent = c === N ? M.perfectTag : M.tag;
1428
+ board.classList.toggle("pkb-all", all);
1429
+ kick.classList.toggle("pkb-gold", all);
1430
+ kick.textContent = all ? M.perfectTag : M.tag;
915
1431
  $("cnt").textContent = `${c}/${N}`;
916
- $("bng").textContent = String(cur.length);
1432
+ const total = endless ? cleared : cur.length;
1433
+ $("bng").textContent = String(total);
917
1434
  const bp = $("bp");
918
- if (cur.length > lastN) {
1435
+ if (total > lastN) {
919
1436
  bp.classList.remove("pkb-hot");
920
1437
  void bp.offsetWidth;
921
1438
  }
922
- bp.classList.toggle("pkb-hot", cur.length > 0);
923
- lastN = cur.length;
1439
+ bp.classList.toggle("pkb-hot", total > 0);
1440
+ lastN = total;
924
1441
  drawLines(fresh);
925
- if (src !== null) tapFx(src, on[src]);
1442
+ if (src !== null) {
1443
+ const newReach = [...reach].some((i) => !prevReach.has(i));
1444
+ const moment = !quiet && all && !prevAll ? "perfect" : !quiet && fresh.length ? "bingo" : !quiet && newReach ? "reach" : "tap";
1445
+ tapFx(src, on[src], moment, fresh);
1446
+ }
926
1447
  const newL = fresh.map((k) => lineKey(lines[k])).filter((key) => !gotL.has(key));
927
1448
  newL.forEach((key) => gotL.add(key));
928
1449
  const linePts = 100 * newL.length * newL.length;
929
1450
  score += linePts;
930
1451
  if (quiet) {
931
1452
  if (linePts) countScore();
932
- } else if (c === N && !prevAll) perfect(events);
1453
+ } else if (all && !prevAll) perfect(events);
933
1454
  else if (fresh.length) {
934
- bingo(fresh, linePts);
935
- const e = { newLines: fresh, totalLines: cur.length, score };
1455
+ bingo(fresh, linePts, total);
1456
+ const e = { newLines: fresh, totalLines: total, score, mode };
936
1457
  events.push(() => options.onBingo?.(e));
937
1458
  } else if (src !== null && on[src]) {
938
1459
  const nr = [...reach].filter((i) => !prevReach.has(i));
@@ -940,9 +1461,9 @@ function createPackingBingo(container, options = {}) {
940
1461
  }
941
1462
  prevLines = new Set(cur);
942
1463
  prevReach = reach;
943
- prevAll = c === N;
1464
+ prevAll = all;
944
1465
  const labels = [...reach].map((i) => items[i].label);
945
- $("msg").textContent = c === N ? M.statusDone : labels.length ? M.statusReach(labels) : cur.length ? M.statusLines(cur.length, N - c) : M.statusStart(N - c);
1466
+ $("msg").textContent = all ? M.statusDone : labels.length ? M.statusReach(labels) : cur.length ? M.statusLines(cur.length, N - c) : M.statusStart(N - c);
946
1467
  return events;
947
1468
  }
948
1469
  const flush = (events) => {
@@ -952,9 +1473,11 @@ function createPackingBingo(container, options = {}) {
952
1473
  }
953
1474
  };
954
1475
  const save = () => {
955
- if (storageKey) writeStore(storageKey, JSON.stringify(checked));
1476
+ if (storageKey && !inf()) writeStore(storageKey, JSON.stringify(checked));
956
1477
  };
957
1478
  function apply(next, src, quiet) {
1479
+ if (phase === "idle" && src !== null && next[src]) startWatch();
1480
+ if (!inf() && next.every(Boolean)) finish();
958
1481
  checked = next;
959
1482
  ins.forEach((x, i) => x.checked = checked[i]);
960
1483
  const events = render(src, quiet);
@@ -962,8 +1485,12 @@ function createPackingBingo(container, options = {}) {
962
1485
  return events;
963
1486
  }
964
1487
  function baseline(next) {
1488
+ dropClear();
1489
+ cleared = 0;
965
1490
  cancelFx();
966
1491
  clearVisuals();
1492
+ if (!inf() && next.every(Boolean)) finish();
1493
+ else if ((phase === "play" || phase === "done") && !next.some(Boolean)) toReady();
967
1494
  checked = next;
968
1495
  ins.forEach((x, i) => x.checked = checked[i]);
969
1496
  got.clear();
@@ -973,7 +1500,7 @@ function createPackingBingo(container, options = {}) {
973
1500
  done.forEach((k) => gotL.add(lineKey(lines[k])));
974
1501
  prevLines = new Set(done);
975
1502
  prevReach = /* @__PURE__ */ new Set();
976
- prevAll = checked.every(Boolean);
1503
+ prevAll = !inf() && checked.every(Boolean);
977
1504
  gotP = prevAll;
978
1505
  score = shown = got.size * 10 + gotL.size * 100;
979
1506
  combo = 0;
@@ -983,7 +1510,9 @@ function createPackingBingo(container, options = {}) {
983
1510
  save();
984
1511
  }
985
1512
  function request(next, src, opts = {}) {
986
- if (controlled) {
1513
+ const game = inf();
1514
+ if (controlled && !game) {
1515
+ resetAsked = !!opts.reset && resetAsked;
987
1516
  ins.forEach((x, i) => x.checked = checked[i]);
988
1517
  options.onChange?.(next.slice());
989
1518
  return;
@@ -991,7 +1520,7 @@ function createPackingBingo(container, options = {}) {
991
1520
  let events = [];
992
1521
  if (opts.reset) baseline(next);
993
1522
  else events = apply(next, src, !!opts.quiet);
994
- options.onChange?.(checked.slice());
1523
+ if (!game) options.onChange?.(checked.slice());
995
1524
  flush(events);
996
1525
  }
997
1526
  function place(next) {
@@ -1010,12 +1539,12 @@ function createPackingBingo(container, options = {}) {
1010
1539
  focused.focus({ preventScroll: true });
1011
1540
  }
1012
1541
  const saveOrder = () => {
1013
- if (!orderKey) return;
1542
+ if (!orderKey || inf()) return;
1014
1543
  writeStore(orderKey, JSON.stringify(order));
1015
1544
  };
1016
1545
  let flights = [];
1017
1546
  function arrange(next) {
1018
- if (!checked.every(Boolean)) cancelFx();
1547
+ if (inf() || !checked.every(Boolean)) cancelFx();
1019
1548
  const from = calm ? null : tiles.map((t) => t.getBoundingClientRect());
1020
1549
  flights.forEach((a) => a.cancel());
1021
1550
  flights = [];
@@ -1051,6 +1580,7 @@ function createPackingBingo(container, options = {}) {
1051
1580
  svg.innerHTML = "";
1052
1581
  }
1053
1582
  function shuffleCard() {
1583
+ if (inf() && phase === "done") return;
1054
1584
  let next = order.slice();
1055
1585
  do {
1056
1586
  for (let s = N - 1; s > 0; s--) {
@@ -1060,31 +1590,98 @@ function createPackingBingo(container, options = {}) {
1060
1590
  } while (next.every((v, s) => v === order[s]));
1061
1591
  arrange(next);
1062
1592
  sfx.shuffle();
1063
- buzz([10, 40, 10, 40, 10, 40, 24]);
1593
+ haptics.play("shuffle");
1064
1594
  }
1065
1595
  const unshuffle = () => {
1066
1596
  if (!isIdentity(order)) arrange(identity());
1067
1597
  };
1598
+ const locked = (i) => checked[i] && (!uncheck || pending.has(i));
1599
+ grid.addEventListener("click", (e) => {
1600
+ const i = ins.indexOf(e.target);
1601
+ if (i >= 0 && locked(i)) e.preventDefault();
1602
+ });
1068
1603
  grid.addEventListener("change", (e) => {
1069
1604
  const i = ins.indexOf(e.target);
1070
1605
  if (i < 0) return;
1606
+ if (locked(i)) {
1607
+ ins[i].checked = true;
1608
+ return;
1609
+ }
1071
1610
  const next = checked.slice();
1072
1611
  next[i] = !next[i];
1073
1612
  request(next, i);
1074
1613
  });
1075
1614
  container.querySelector('[data-r="shuf"]')?.addEventListener("click", shuffleCard);
1615
+ const restartInfinite = () => {
1616
+ baseline(normalize(void 0));
1617
+ toReady();
1618
+ };
1076
1619
  $("reset").addEventListener("click", () => {
1620
+ if (inf()) return restartInfinite();
1077
1621
  unshuffle();
1622
+ restartGame();
1078
1623
  request(normalize(void 0), null, { reset: true });
1079
1624
  });
1080
1625
  $("close").addEventListener("click", () => perfectEl.classList.remove("pkb-show"));
1626
+ $("again").addEventListener("click", () => {
1627
+ if (!inf()) return;
1628
+ restartInfinite();
1629
+ (readyEl && !readyEl.hidden ? readyEl : grid.querySelector("input"))?.focus({ preventScroll: true });
1630
+ });
1631
+ function finishGame() {
1632
+ if (dead || !inf() || phase !== "play") return;
1633
+ cancelFx();
1634
+ clearVisuals();
1635
+ finalMs = performance.now() - t0;
1636
+ phase = "done";
1637
+ clearInterval(watchTimer);
1638
+ watch.classList.replace("pkb-run", "pkb-done");
1639
+ showWatch();
1640
+ syncFin();
1641
+ combo = 0;
1642
+ grid.setAttribute("inert", "");
1643
+ board.classList.add("pkb-over");
1644
+ setResult(true);
1645
+ $("pbody").textContent = "";
1646
+ $("ptime").hidden = false;
1647
+ $("pT").textContent = formatTime(finalMs, 2);
1648
+ $("sScore").textContent = "0";
1649
+ $("sCombo").textContent = "\xD70";
1650
+ $("sBingo").textContent = "0";
1651
+ perfectEl.classList.remove("pkb-settled");
1652
+ replay(perfectEl, "pkb-show");
1653
+ resultFocus().focus({ preventScroll: true });
1654
+ sfx.perfect();
1655
+ const beat = haptics.play("finish");
1656
+ if (!calm) replay($("flash"), "pkb-go");
1657
+ later(() => {
1658
+ if (!calm) replay(board, "pkb-shake");
1659
+ sfx.boom();
1660
+ beat?.(PERFECT_STAMP_BEAT);
1661
+ burst(0, BH, 80, { angle: -Math.PI / 3, spread: 0.8, speed: 21, g: 0.32, star: 0.5, colors: SPARK });
1662
+ burst(BW, BH, 80, { angle: -Math.PI * 2 / 3, spread: 0.8, speed: 21, g: 0.32, star: 0.5, colors: SPARK });
1663
+ rain(2500);
1664
+ countUp($("sScore"), 0, score, 1300, (v) => v.toLocaleString(), true);
1665
+ countUp($("sCombo"), 0, maxCombo, 700, (v) => "\xD7" + v);
1666
+ countUp($("sBingo"), 0, cleared, 700, String);
1667
+ }, PERFECT_STAMP_DELAY_MS);
1668
+ [1250, 1700, 2200].forEach(
1669
+ (at, k) => later(() => {
1670
+ firework(BW * (0.15 + Math.random() * 0.7), BH * (0.12 + Math.random() * 0.3));
1671
+ beat?.(PERFECT_FIREWORKS[k * 2].beat);
1672
+ }, at)
1673
+ );
1674
+ const e = { score, maxCombo, lines: cleared, time: Math.round(finalMs) };
1675
+ options.onFinish?.(e);
1676
+ }
1677
+ finBtn.addEventListener("click", finishGame);
1081
1678
  perfectEl.addEventListener("click", (e) => {
1082
1679
  if (e.target === perfectEl) perfectEl.classList.remove("pkb-show");
1083
1680
  });
1084
1681
  if (demo) {
1085
1682
  let busy = false;
1086
1683
  const prepare = () => {
1087
- if (checked.every(Boolean)) request(normalize(void 0), null, { reset: true });
1684
+ if (!inf() && checked.every(Boolean)) request(normalize(void 0), null, { reset: true });
1088
1685
  else perfectEl.classList.remove("pkb-show");
1089
1686
  };
1090
1687
  const run = async (steps) => {
@@ -1092,6 +1689,7 @@ function createPackingBingo(container, options = {}) {
1092
1689
  busy = true;
1093
1690
  try {
1094
1691
  prepare();
1692
+ skipReady();
1095
1693
  const g = gen;
1096
1694
  for (const s of steps) {
1097
1695
  await wait(s.delay);
@@ -1105,21 +1703,100 @@ function createPackingBingo(container, options = {}) {
1105
1703
  }
1106
1704
  };
1107
1705
  $("dB").addEventListener("click", () => {
1706
+ if (inf() && phase === "done") return;
1108
1707
  const open = () => lines.map((l) => l.filter((i) => !checked[i])).filter((m) => m.length).sort((a, b) => a.length - b.length)[0] ?? [];
1109
- const pick = checked.every(Boolean) ? [...lines[0]] : open();
1708
+ const pick = !inf() && checked.every(Boolean) ? [...lines[0]] : open();
1110
1709
  void run(pick.map((i) => ({ i, delay: 320 })));
1111
1710
  });
1112
1711
  $("dP").addEventListener("click", () => {
1712
+ if (inf()) return;
1113
1713
  const all = items.map((_, i) => i);
1114
1714
  const miss = checked.every(Boolean) ? all : all.filter((i) => !checked[i]);
1115
1715
  void run(miss.map((i, k) => k < miss.length - 1 ? { i, delay: 170, quiet: true } : { i, delay: 450 }));
1116
1716
  });
1117
1717
  $("dR").addEventListener("click", () => {
1118
- if (busy) return;
1718
+ if (busy || inf()) return;
1119
1719
  unshuffle();
1720
+ restartGame();
1120
1721
  request(demoStart(), null, { reset: true });
1121
1722
  });
1122
1723
  }
1724
+ const modeEl = container.querySelector('[data-r="mode"]');
1725
+ const radios = modeEl ? Array.from(modeEl.querySelectorAll("button[data-m]")) : [];
1726
+ const syncMode = () => {
1727
+ root.classList.toggle("pkb-inf", inf());
1728
+ radios.forEach((b) => {
1729
+ const on = b.dataset.m === mode;
1730
+ b.setAttribute("aria-checked", String(on));
1731
+ b.tabIndex = on ? 0 : -1;
1732
+ });
1733
+ };
1734
+ function modeFx() {
1735
+ const endless = inf();
1736
+ const title = endless ? M.infiniteTitle?.trim() || ja.infiniteTitle : M.normalTitle?.trim() || ja.normalTitle;
1737
+ const sub = endless ? M.infiniteSub?.trim() || ja.infiniteSub : M.normalSub?.trim() || ja.normalSub;
1738
+ const sparks = endless && !calm ? Array.from({ length: 18 }, (_, k) => {
1739
+ const x = (5 + Math.random() * 90).toFixed(1), y = (6 + Math.random() * 88).toFixed(1);
1740
+ const s = (0.45 + Math.random() * 0.95).toFixed(2), t = Math.round(100 + Math.random() * 950);
1741
+ return `<i style="--x:${x}%;--y:${y}%;--s:${s};--t:${t}ms;--c:${SPARK[k % SPARK.length]}"></i>`;
1742
+ }).join("") : "";
1743
+ mfx.className = "pkb-mfx " + (endless ? "pkb-mfx-inf" : "pkb-mfx-n");
1744
+ mfx.innerHTML = (endless ? `<span class="pkb-mfx-glow"></span>` : "") + sparks + `<span class="pkb-mfx-in"><b class="pkb-mfx-sym">${endless ? "\u221E" : esc(M.stamp)}</b><b class="pkb-mfx-t">${esc(title)}</b><span class="pkb-mfx-s">${esc(sub)}</span></span>`;
1745
+ replay(mfx, "pkb-show");
1746
+ board.classList.add("pkb-switching");
1747
+ if (modeEl && !calm) replay(modeEl, "pkb-bump");
1748
+ sfx.mode(endless);
1749
+ haptics.play(endless ? "infinite" : "normal");
1750
+ const picked = radios.find((b) => b.dataset.m === mode);
1751
+ if (picked) {
1752
+ const p = rel(picked);
1753
+ burst(p.x, p.y, endless ? 46 : 24, { speed: endless ? 7 : 5, g: 0.1, life: 55, size: 0.8, star: endless ? 0.9 : 0.3, colors: endless ? SPARK : void 0 });
1754
+ }
1755
+ later(() => {
1756
+ const c = rel(gw);
1757
+ if (endless) burst(c.x, c.y, 90, { speed: 12, g: 0.07, life: 90, star: 0.75, colors: SPARK });
1758
+ else {
1759
+ if (!calm) replay(board, "pkb-punch");
1760
+ burst(c.x, c.y, 70, { speed: 11, star: 0.2 });
1761
+ }
1762
+ }, endless ? 260 : 430);
1763
+ }
1764
+ mfx.addEventListener("animationend", (e) => {
1765
+ if (e.target !== mfx) return;
1766
+ mfx.classList.remove("pkb-show");
1767
+ board.classList.remove("pkb-switching");
1768
+ mfx.innerHTML = "";
1769
+ });
1770
+ function setMode(next) {
1771
+ if (dead || next === mode || next !== "normal" && next !== "infinite") return;
1772
+ const back = packing;
1773
+ if (next === "infinite") packing = { checked: checked.slice(), order: order.slice() };
1774
+ else packing = null;
1775
+ mode = next;
1776
+ resetAsked = false;
1777
+ if (back && back.order.some((v, s) => v !== order[s])) {
1778
+ flights.forEach((a) => a.cancel());
1779
+ flights = [];
1780
+ clearTimeout(landing);
1781
+ landing = 0;
1782
+ place(back.order);
1783
+ }
1784
+ syncMode();
1785
+ toReady();
1786
+ baseline(back ? back.checked : normalize(void 0));
1787
+ modeFx();
1788
+ options.onModeChange?.(mode);
1789
+ }
1790
+ modeEl?.addEventListener("click", (e) => {
1791
+ const b = e.target.closest("button[data-m]");
1792
+ if (b) setMode(b.dataset.m);
1793
+ });
1794
+ modeEl?.addEventListener("keydown", (e) => {
1795
+ if (!["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(e.key)) return;
1796
+ e.preventDefault();
1797
+ setMode(inf() ? "normal" : "infinite");
1798
+ radios.find((b) => b.dataset.m === mode)?.focus();
1799
+ });
1123
1800
  let startOrder = null;
1124
1801
  if (orderKey) {
1125
1802
  try {
@@ -1141,17 +1818,47 @@ function createPackingBingo(container, options = {}) {
1141
1818
  if (!initial && options.defaultChecked) initial = normalize(options.defaultChecked);
1142
1819
  if (!initial && demo) initial = demoStart();
1143
1820
  fit();
1144
- baseline(initial ?? normalize(void 0));
1821
+ if (inf()) packing = { checked: initial ?? normalize(void 0), order: order.slice() };
1822
+ syncMode();
1823
+ baseline(inf() ? normalize(void 0) : initial ?? normalize(void 0));
1824
+ if (phase !== "done") toReady();
1145
1825
  const ro = typeof ResizeObserver === "function" ? new ResizeObserver(() => {
1146
1826
  fit();
1147
1827
  drawLines();
1148
1828
  }) : null;
1149
1829
  ro?.observe(board);
1150
1830
  ro?.observe(gw);
1831
+ const onVisibility = () => {
1832
+ if (!document.hidden) {
1833
+ if (restorePerfectFocus && perfectEl.classList.contains("pkb-show")) resultFocus().focus({ preventScroll: true });
1834
+ restorePerfectFocus = false;
1835
+ return;
1836
+ }
1837
+ const keepPerfect = perfectPending || perfectEl.classList.contains("pkb-show");
1838
+ restorePerfectFocus = keepPerfect;
1839
+ cancelFx();
1840
+ clearVisuals(keepPerfect);
1841
+ if (keepPerfect) {
1842
+ $("sScore").textContent = score.toLocaleString();
1843
+ $("sCombo").textContent = "\xD7" + maxCombo;
1844
+ $("sBingo").textContent = String(resultLines());
1845
+ perfectEl.classList.add("pkb-show", "pkb-settled");
1846
+ }
1847
+ combo = 0;
1848
+ };
1849
+ document.addEventListener("visibilitychange", onVisibility);
1151
1850
  return {
1152
1851
  setChecked(next, opts = {}) {
1153
1852
  if (dead) return;
1154
1853
  const n2 = normalize(next);
1854
+ if (packing) {
1855
+ packing.checked = n2;
1856
+ resetAsked = false;
1857
+ if (storageKey) writeStore(storageKey, JSON.stringify(n2));
1858
+ return;
1859
+ }
1860
+ if (resetAsked && !n2.some(Boolean)) toReady();
1861
+ resetAsked = false;
1155
1862
  const changed = n2.flatMap((v, i) => v !== checked[i] ? [i] : []);
1156
1863
  if (!changed.length) return;
1157
1864
  if (!opts.animate || changed.length !== 1) {
@@ -1162,12 +1869,20 @@ function createPackingBingo(container, options = {}) {
1162
1869
  },
1163
1870
  reset() {
1164
1871
  if (dead) return;
1872
+ if (inf()) return restartInfinite();
1165
1873
  unshuffle();
1874
+ restartGame();
1166
1875
  request(normalize(void 0), null, { reset: true });
1167
1876
  },
1168
1877
  shuffle() {
1169
1878
  if (!dead) shuffleCard();
1170
1879
  },
1880
+ setMode(next) {
1881
+ setMode(next);
1882
+ },
1883
+ finish() {
1884
+ finishGame();
1885
+ },
1171
1886
  refresh() {
1172
1887
  if (!dead) render(null, true);
1173
1888
  },
@@ -1177,22 +1892,31 @@ function createPackingBingo(container, options = {}) {
1177
1892
  lines: cur.slice(),
1178
1893
  score,
1179
1894
  maxCombo,
1180
- complete: checked.every(Boolean),
1895
+ complete: !inf() && checked.every(Boolean),
1896
+ mode,
1897
+ cleared,
1898
+ finished: inf() && phase === "done",
1899
+ packing: packing && { checked: packing.checked.slice(), order: packing.order.slice() },
1181
1900
  size: n,
1182
1901
  free: L.free,
1183
- order: order.slice()
1902
+ order: order.slice(),
1903
+ time: Math.round(elapsed())
1184
1904
  };
1185
1905
  },
1186
1906
  destroy() {
1187
1907
  if (dead) return;
1188
1908
  dead = true;
1909
+ stopTimers();
1189
1910
  timers.forEach((id) => clearTimeout(id));
1190
1911
  timers.clear();
1191
1912
  clearTimeout(landing);
1913
+ clearTimeout(clearTimer);
1192
1914
  flights.forEach((a) => a.cancel());
1193
1915
  cancelAnimationFrame(raf);
1194
1916
  cancelAnimationFrame(tw);
1195
1917
  ro?.disconnect();
1918
+ document.removeEventListener("visibilitychange", onVisibility);
1919
+ haptics.stop();
1196
1920
  sfx.close();
1197
1921
  container.innerHTML = "";
1198
1922
  }
@@ -1206,5 +1930,7 @@ export {
1206
1930
  en,
1207
1931
  resolveStorageKey,
1208
1932
  LINES,
1933
+ formatTime,
1934
+ speedBonus,
1209
1935
  createPackingBingo
1210
1936
  };