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