packing-bingo 0.5.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,11 +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",
140
- soundHint: "\u7AEF\u672B\u306E\u97F3\u91CF\u3092\u4E0A\u3052\u3066\u306D\u3002",
141
- vibrationHint: "Android\u3067\u632F\u52D5\u3092\u697D\u3057\u3080\u306B\u306F\u3001\u304A\u3084\u3059\u307F\u30E2\u30FC\u30C9\uFF08Do Not Disturb\uFF09\u3092OFF\u306B\u3057\u3066\u306D\u3002",
142
- playTitle: "\u97F3\u3068\u632F\u52D5\u306E\u6E96\u5099\u306FOK\uFF1F",
143
- playStart: "\u6E96\u5099OK\uFF01\u306F\u3058\u3081\u308B",
144
- playSkip: "\u3053\u306E\u307E\u307E\u904A\u3076",
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",
145
162
  stamp: "OK",
146
163
  free: "FREE",
147
164
  reachBadge: "\u30EA\u30FC\u30C1",
@@ -152,15 +169,15 @@ var ja = {
152
169
  bannerNth: (n) => `${n} \u30E9\u30A4\u30F3\u76EE\uFF01`,
153
170
  bannerMulti: (n) => `\u4E00\u6C17\u306B ${n} \u30E9\u30A4\u30F3\uFF01`,
154
171
  statusStart: () => "",
155
- statusLines: (lines, left) => `\u30D3\u30F3\u30B4 ${lines} \u30E9\u30A4\u30F3\uFF01 \u306E\u3053\u308A ${left} \u3053`,
172
+ statusLines: () => "",
156
173
  statusReach: () => "",
157
- statusDone: "\u{1F389} \u30D1\u30FC\u30D5\u30A7\u30AF\u30C8\uFF01\u5FD8\u308C\u7269\u30BC\u30ED\u3067\u51FA\u767A\u3067\u304D\u307E\u3059",
174
+ statusDone: "",
158
175
  perfectTag: "\u{1F3C6} PERFECT",
159
176
  perfectStamp: "\u6E96\u5099\n\u5B8C\u4E86",
160
177
  perfectTitle: "\u30D1\u30FC\u30D5\u30A7\u30AF\u30C8\uFF01",
161
- perfectBody: (n) => `${n}\u3053 \u305C\u3093\u3076\u305D\u308D\u3063\u305F\uFF01
162
- \u5FD8\u308C\u7269\u30BC\u30ED\u3067\u3001\u3044\u3063\u3066\u3089\u3063\u3057\u3083\u3044\uFF01`,
178
+ perfectBody: "",
163
179
  perfectBonus: (n) => `PERFECT BONUS +${n}`,
180
+ speedBonus: (n) => `SPEED BONUS +${n.toLocaleString()}`,
164
181
  statScore: "SCORE",
165
182
  statCombo: "MAX COMBO",
166
183
  statLines: "BINGO",
@@ -183,11 +200,27 @@ var en = {
183
200
  shuffle: "\u{1F500} Shuffle",
184
201
  soundOn: "\u{1F50A} Sound on",
185
202
  soundOff: "\u{1F507} Muted",
186
- soundHint: "Turn up your device volume before playing.",
187
- vibrationHint: "On Android, turn off Do Not Disturb to enjoy vibration.",
188
- playTitle: "Ready for sound and vibration?",
189
- playStart: "Ready! Let's play",
190
- playSkip: "Play as is",
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",
191
224
  stamp: "OK",
192
225
  free: "FREE",
193
226
  reachBadge: "REACH",
@@ -198,15 +231,15 @@ var en = {
198
231
  bannerNth: (n) => `Line ${n}!`,
199
232
  bannerMulti: (n) => `${n} lines at once!`,
200
233
  statusStart: () => "",
201
- statusLines: (lines, left) => `${lines} bingo line${lines === 1 ? "" : "s"}! ${left} to go`,
234
+ statusLines: () => "",
202
235
  statusReach: () => "",
203
- statusDone: "\u{1F389} Perfect! Nothing left behind.",
236
+ statusDone: "",
204
237
  perfectTag: "\u{1F3C6} PERFECT",
205
238
  perfectStamp: "ALL\nSET",
206
239
  perfectTitle: "Perfect!",
207
- perfectBody: (n) => `All ${n} packed!
208
- Have a great trip!`,
240
+ perfectBody: "",
209
241
  perfectBonus: (n) => `PERFECT BONUS +${n}`,
242
+ speedBonus: (n) => `SPEED BONUS +${n.toLocaleString()}`,
210
243
  statScore: "SCORE",
211
244
  statCombo: "MAX COMBO",
212
245
  statLines: "BINGO",
@@ -347,6 +380,36 @@ function createSound(isMuted) {
347
380
  tone(hz(7), 0.34, 0.12, "triangle", 0.14);
348
381
  tone(hz(12), 0.42, 0.2, "triangle", 0.14);
349
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
+ },
350
413
  stop,
351
414
  close() {
352
415
  stop();
@@ -389,7 +452,10 @@ function createHaptics(enabled) {
389
452
  const current = ++generation;
390
453
  const tier = Math.min(3, Math.floor(combo / 3));
391
454
  const lead = Math.max(80, impactAt - 40);
392
- 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] : tier ? [100 + tier * 20, 15, 80 + tier * 20] : 90;
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
+ );
393
459
  if (!send(pattern)) return;
394
460
  return (beat) => {
395
461
  if (generation === current && owner === token) send([...beat]);
@@ -440,24 +506,17 @@ var CSS = `
440
506
  .pkb-board.pkb-punch{animation:pkb-punch .4s cubic-bezier(.3,1.6,.5,1)}
441
507
  @keyframes pkb-punch{30%{transform:scale(1.018)}}
442
508
  .pkb-head{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:12px 16px;margin-bottom:18px}
443
- .pkb-play-dialog{position:fixed;inset:0;margin:auto;width:400px;max-width:calc(100vw - 32px);max-height:calc(100dvh - 32px);overflow:auto;padding:24px;border:2.5px solid var(--pk-ink);border-radius:20px;background:var(--pk-paper);color:var(--pk-ink);font-family:var(--pk-body);text-align:left}
444
- .pkb-play-dialog:not([open]){display:none}
445
- .pkb-play-dialog::backdrop{background:rgba(42,31,20,.6)}
446
- .pkb-play-dialog h3{margin-bottom:16px;font-size:22px;font-weight:800;line-height:1.4}
447
- .pkb-play-dialog p{margin-bottom:12px;font-size:15px;font-weight:700;line-height:1.7;overflow-wrap:anywhere}
448
- .pkb-play-actions{display:grid;gap:10px;margin-top:22px}
449
- .pkb-play-dialog button{padding:12px 16px;border:2px solid var(--pk-ink);border-radius:12px;color:var(--pk-ink);font-size:15px;font-weight:800;line-height:1.5;cursor:pointer}
450
- .pkb-play-start{background:var(--pk-sun)}
451
- .pkb-play-skip{background:transparent}
452
- .pkb-play-dialog button:focus-visible{outline:3px solid var(--pk-ink);outline-offset:3px}
453
- .pkb-play-dialog button:hover{filter:brightness(.96)}
454
- .pkb-tags{display:flex;gap:8px;align-items:center}
509
+ .pkb-tags{display:flex;flex-wrap:wrap;gap:8px;align-items:center}
455
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}
456
511
  .pkb-kicker:empty{display:none}
457
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}
458
513
  @keyframes pkb-rb{to{background-position:-200% 0}}
459
514
  .pkb button{font-family:var(--pk-body)}
460
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}
461
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}
462
521
  .pkb-sub{font-weight:800;font-size:13px;margin-top:4px;word-break:auto-phrase}
463
522
  .pkb-side{display:flex;flex-direction:column;gap:8px;flex:none}
@@ -480,7 +539,7 @@ var CSS = `
480
539
  .pkb-gw{position:relative}
481
540
  .pkb-grid{display:grid;grid-template-columns:repeat(var(--pk-n),minmax(0,1fr));gap:calc(12px*var(--pk-d))}
482
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}
483
- .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)}
484
543
  .pkb-tile input{position:absolute;opacity:0;pointer-events:none;width:1px;height:1px}
485
544
  .pkb-tile:has(input:focus-visible){outline:3px solid var(--pk-ink);outline-offset:3px}
486
545
  .pkb-emo{font-size:max(14px,calc(32px*var(--pk-s)));line-height:1;transition:opacity .2s}
@@ -488,6 +547,7 @@ var CSS = `
488
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}
489
548
  .pkb-tile.pkb-on .pkb-stamp{opacity:.9;transform:translate(-50%,-50%) rotate(-16deg) scale(1)}
490
549
  .pkb-tile.pkb-on .pkb-emo,.pkb-tile.pkb-on .pkb-lbl{opacity:.35}
550
+ .pkb-tile.pkb-lock{cursor:default}
491
551
  .pkb-tile.pkb-free{cursor:default;container-type:inline-size}
492
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)}
493
553
  .pkb-tile.pkb-line{background-color:var(--pk-mint)}
@@ -515,10 +575,11 @@ var CSS = `
515
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}
516
576
  .pkb-foot>span{flex:1 1 14em}
517
577
  .pkb-foot>span:empty{flex-basis:0}
518
- .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}
519
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}
520
580
  .pkb-shuf{background:var(--pk-paper);box-shadow:2px 2px 0 var(--pk-ink)}
521
- .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}
522
583
  .pkb-flash{position:absolute;inset:0;z-index:7;background:#fff;opacity:0;pointer-events:none}
523
584
  .pkb-flash.pkb-go{animation:pkb-flash .4s ease-out}
524
585
  @keyframes pkb-flash{0%{opacity:.8}100%{opacity:0}}
@@ -535,8 +596,33 @@ var CSS = `
535
596
  .pkb-float.pkb-big{font-size:clamp(14px,5.6cqi,34px);color:#FFE066}
536
597
  .pkb-float.pkb-rch{font-size:20px;color:var(--pk-red);-webkit-text-stroke:4px var(--pk-paper)}
537
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}
538
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}
539
- .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}
540
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)}
541
627
  .pkb-perfect.pkb-show .pkb-card{transform:none}
542
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)}
@@ -546,6 +632,7 @@ var CSS = `
546
632
  @keyframes pkb-slam{to{opacity:1;transform:rotate(-14deg) scale(1)}}
547
633
  .pkb-ptitle{font-family:var(--pk-display);font-weight:400;font-size:30px;line-height:1.3;color:inherit}
548
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}
549
636
  .pkb-stats{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-bottom:16px}
550
637
  .pkb-stats>div{border:2px solid var(--pk-ink);border-radius:12px;padding:6px 4px 5px;background:#fff}
551
638
  .pkb-stats small{display:block;font-size:9.5px;font-weight:800;letter-spacing:.1em}
@@ -555,10 +642,72 @@ var CSS = `
555
642
  .pkb-fx{position:absolute;inset:0;width:100%;height:100%;z-index:10;pointer-events:none}
556
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}
557
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}
558
- @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}}
559
- @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}}
560
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}}
561
- @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{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,.pkb-tile,.pkb-stamp{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}}
562
711
  @keyframes pkb-fade{0%,100%{opacity:0}15%,80%{opacity:1}}
563
712
  `;
564
713
 
@@ -568,7 +717,6 @@ var CONFETTI = ["#E8473B", "#2F6FED", "#1FA971", "#FFFDF6", "#2A1F14", "#FF9F1C"
568
717
  var COMBO_COLORS = ["#FF9F1C", "#1FA971", "#2F6FED", "#9B5DE5", "#E8473B"];
569
718
  var COMBO_MS = 2500;
570
719
  var MUTE_KEY = "packing-bingo-muted";
571
- var playReady = /* @__PURE__ */ new WeakSet();
572
720
  var ORDER_PREFIX = "packing-bingo-order:";
573
721
  var esc = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
574
722
  function readStore(key) {
@@ -586,7 +734,22 @@ function writeStore(key, value) {
586
734
  }
587
735
  var glintDelay = (c, n) => `${+((Math.floor(c / n) + c % n) * 0.36 / Math.max(1, n - 1)).toFixed(3)}s`;
588
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
+ }
589
750
  var FLY_MS = 560;
751
+ var CLEAR_MS = 1e3;
752
+ var SPARK = ["#FFFFFF", "#FFE066", "#FF9F1C", "#E8473B", "#9B5DE5", "#2F6FED", "#1FA971"];
590
753
  function ensureStyles() {
591
754
  if (document.getElementById("pkb-styles")) return;
592
755
  const s = document.createElement("style");
@@ -594,7 +757,8 @@ function ensureStyles() {
594
757
  s.textContent = CSS;
595
758
  document.head.appendChild(s);
596
759
  }
597
- function template(L, M, { demo, shuffle, vibrate }) {
760
+ var opt = (v, fallback) => v ?? fallback ?? "";
761
+ function template(L, M, { demo, shuffle, vibrate, ready, modes }) {
598
762
  const n = L.size;
599
763
  const input = vibrate ? `<input type="checkbox" switch role="checkbox">` : `<input type="checkbox">`;
600
764
  const tiles = Array.from({ length: n * n }, (_, c) => {
@@ -606,16 +770,15 @@ function template(L, M, { demo, shuffle, vibrate }) {
606
770
  }).join("");
607
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)}`;
608
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>` : "";
609
- const hints = [M.soundHint ?? "", vibrate ? M.vibrationHint ?? "" : ""].filter(Boolean);
610
- const playTitle = M.playTitle?.trim() || ja.playTitle || M.title;
611
- const playDialog = hints.length ? `<dialog class="pkb-play-dialog" data-r="play" aria-label="${esc(playTitle)}">
612
- <h3>${esc(playTitle)}</h3>
613
- ${hints.map((hint) => `<p>${esc(hint)}</p>`).join("")}
614
- <div class="pkb-play-actions"><button type="button" class="pkb-play-start">${esc(M.playStart?.trim() || ja.playStart || M.close)}</button>
615
- <button type="button" class="pkb-play-skip">${esc(M.playSkip?.trim() || ja.playSkip || M.close)}</button></div></dialog>` : "";
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>` : "";
616
779
  return `<div class="pkb" style="${scale}"><div class="pkb-board">
617
780
  <div class="pkb-head"><div>
618
- <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>
619
782
  <h2 class="pkb-title">${esc(M.title)}</h2>${M.subtitle ? `<p class="pkb-sub">${esc(M.subtitle)}</p>` : ""}</div>
620
783
  <div class="pkb-side"><div class="pkb-score">
621
784
  <div class="pkb-pill"><small>${esc(M.packed)}</small><b data-r="cnt"></b></div>
@@ -624,13 +787,14 @@ ${hints.map((hint) => `<p>${esc(hint)}</p>`).join("")}
624
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>
625
788
  <div class="pkb-gw"><div class="pkb-grid">${tiles}</div><svg class="pkb-lines" aria-hidden="true"></svg>
626
789
  <div class="pkb-banner" data-r="banner" aria-hidden="true"><b data-r="bT"></b><span data-r="bS"></span></div></div>
627
- <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>
628
791
  <div class="pkb-flash" data-r="flash"></div>
629
- <div class="pkb-perfect" data-r="perfect"><div class="pkb-card" role="dialog" aria-label="${esc(M.perfectTitle)}">
630
- <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>
631
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>
632
- <button type="button" class="pkb-close" data-r="close">${esc(M.close)}</button></div></div>
633
- <canvas class="pkb-fx"></canvas></div>${demoBar}${playDialog}</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>`;
634
798
  }
635
799
  var perfectText = (M, total) => typeof M.perfectBody === "function" ? M.perfectBody(total) : M.perfectBody;
636
800
  function createPackingBingo(container, options = {}) {
@@ -662,8 +826,15 @@ function createPackingBingo(container, options = {}) {
662
826
  const orderKey = storageKey && ORDER_PREFIX + storageKey;
663
827
  const demo = !!options.demo;
664
828
  const vibrate = options.vibrate !== false;
829
+ const uncheck = options.uncheck === true;
665
830
  const calm = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
666
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;
667
838
  let dead = false;
668
839
  const timers = /* @__PURE__ */ new Set();
669
840
  const later = (fn, ms) => {
@@ -676,12 +847,19 @@ function createPackingBingo(container, options = {}) {
676
847
  const wait = (ms) => new Promise((r) => window.setTimeout(r, ms));
677
848
  let gen = 0;
678
849
  ensureStyles();
679
- 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
+ });
680
857
  const $ = (r) => {
681
858
  const el = container.querySelector(`[data-r="${r}"]`);
682
859
  if (!el) throw new Error(`[packing-bingo] internal: missing element "${r}"`);
683
860
  return el;
684
861
  };
862
+ const root = container.querySelector(".pkb");
685
863
  const board = container.querySelector(".pkb-board");
686
864
  const gw = container.querySelector(".pkb-gw");
687
865
  const grid = container.querySelector(".pkb-grid");
@@ -693,21 +871,7 @@ function createPackingBingo(container, options = {}) {
693
871
  const freeTile = L.center >= 0 ? cellTiles[L.center] : null;
694
872
  const ins = tiles.map((t) => t.querySelector("input"));
695
873
  const stamps = tiles.map((t) => t.querySelector(".pkb-stamp"));
696
- const kick = $("kick"), snd = $("snd"), perfectEl = $("perfect");
697
- const playDialog = container.querySelector('[data-r="play"]');
698
- const beforePlay = (event) => {
699
- if (!playDialog || playReady.has(container.ownerDocument) || typeof playDialog.showModal !== "function" || !(event.target instanceof Element) || !event.target.closest(".pkb-tile:not(.pkb-free)")) return;
700
- event.preventDefault();
701
- event.stopImmediatePropagation();
702
- playDialog.showModal();
703
- };
704
- grid.addEventListener("click", beforePlay, true);
705
- playDialog?.querySelectorAll("button").forEach((button) => {
706
- button.addEventListener("click", () => playDialog.close());
707
- });
708
- playDialog?.addEventListener("close", () => {
709
- if (!dead) playReady.add(container.ownerDocument);
710
- });
874
+ const kick = $("kick"), snd = $("snd"), perfectEl = $("perfect"), mfx = $("mfx");
711
875
  let checked = normalize(void 0);
712
876
  let cur = [];
713
877
  let prevLines = /* @__PURE__ */ new Set();
@@ -743,7 +907,7 @@ function createPackingBingo(container, options = {}) {
743
907
  cv.width = Math.round(BW * dpr);
744
908
  cv.height = Math.round(BH * dpr);
745
909
  };
746
- const piece = (x, y, vx, vy, g, s, life, shape2) => ({
910
+ const piece = (x, y, vx, vy, g, s, life, shape2, colors = CONFETTI) => ({
747
911
  x,
748
912
  y,
749
913
  vx,
@@ -755,7 +919,7 @@ function createPackingBingo(container, options = {}) {
755
919
  h: (3 + Math.random() * 5) * s,
756
920
  r: Math.random() * 6,
757
921
  vr: (Math.random() - 0.5) * 0.35,
758
- c: CONFETTI[Math.random() * CONFETTI.length | 0]
922
+ c: colors[Math.random() * colors.length | 0]
759
923
  });
760
924
  const go = () => {
761
925
  if (!raf && ctx) raf = requestAnimationFrame(tick);
@@ -766,7 +930,7 @@ function createPackingBingo(container, options = {}) {
766
930
  const a = (o.angle ?? -Math.PI / 2) + (Math.random() - 0.5) * (o.spread ?? Math.PI * 2);
767
931
  const v = (o.speed ?? 10) * (0.35 + Math.random() * 0.85);
768
932
  const sh = Math.random() < (o.star ?? 0.15) ? "star" : Math.random() < 0.3 ? "round" : "rect";
769
- 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));
770
934
  }
771
935
  go();
772
936
  }
@@ -864,6 +1028,7 @@ function createPackingBingo(container, options = {}) {
864
1028
  if (e.animationName === "pkb-stamp-hit") t.classList.remove("pkb-hit");
865
1029
  if (t.classList.contains("pkb-tile") && (e.animationName === "pkb-hop" || e.animationName === "pkb-squash"))
866
1030
  t.classList.remove(e.animationName);
1031
+ if (t.classList.contains("pkb-tile") && e.animationName === "pkb-clr") t.classList.remove("pkb-clr");
867
1032
  });
868
1033
  const ring = (t) => {
869
1034
  const r = document.createElement("span");
@@ -882,20 +1047,20 @@ function createPackingBingo(container, options = {}) {
882
1047
  board.appendChild(el);
883
1048
  el.addEventListener("animationend", () => el.remove());
884
1049
  };
885
- 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));
886
1051
  const ease = (k) => 1 - Math.pow(1 - k, 3);
887
1052
  let tw = 0;
888
1053
  function countScore() {
889
1054
  cancelAnimationFrame(tw);
890
- const a = shown, b = score, t0 = performance.now();
1055
+ const a = shown, b = score, t02 = performance.now();
891
1056
  const st = (t) => {
892
1057
  if (dead) return;
893
- const k = prog(t, t0, 450);
1058
+ const k = prog(t, t02, 450);
894
1059
  shown = Math.round(a + (b - a) * ease(k));
895
1060
  $("sc").textContent = shown.toLocaleString();
896
1061
  if (k < 1) tw = requestAnimationFrame(st);
897
1062
  };
898
- st(t0);
1063
+ st(t02);
899
1064
  if (!calm) replay($("sp"), "pkb-bump");
900
1065
  }
901
1066
  const showGain = (pts, x, y, cls = "pt") => {
@@ -903,12 +1068,12 @@ function createPackingBingo(container, options = {}) {
903
1068
  countScore();
904
1069
  };
905
1070
  function countUp(el, a, b, d, fmt, ticks = false) {
906
- const t0 = performance.now();
1071
+ const t02 = performance.now();
907
1072
  const g = gen;
908
1073
  let lt = 0;
909
1074
  const st = (t) => {
910
1075
  if (dead || g !== gen) return;
911
- const k = prog(t, t0, d);
1076
+ const k = prog(t, t02, d);
912
1077
  el.textContent = fmt(Math.round(a + (b - a) * ease(k)));
913
1078
  if (ticks && k < 1 && t - lt > 55) {
914
1079
  sfx.tick();
@@ -917,7 +1082,7 @@ function createPackingBingo(container, options = {}) {
917
1082
  if (k < 1) requestAnimationFrame(st);
918
1083
  else if (!calm) replay(el, "pkb-bump");
919
1084
  };
920
- st(t0);
1085
+ st(t02);
921
1086
  }
922
1087
  const ctr = (t) => {
923
1088
  return { x: t.offsetLeft + t.offsetWidth / 2, y: t.offsetTop + t.offsetHeight / 2 };
@@ -936,6 +1101,120 @@ function createPackingBingo(container, options = {}) {
936
1101
  return `<g${fresh.includes(k) ? ' class="pkb-draw"' : ""}><line class="pkb-u" ${at}/><line class="pkb-o" ${at}/></g>`;
937
1102
  }).join("");
938
1103
  }
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);
939
1218
  function tapFx(i, on, moment, fresh) {
940
1219
  const p = rel(tiles[i]);
941
1220
  if (!on) {
@@ -987,7 +1266,7 @@ function createPackingBingo(container, options = {}) {
987
1266
  });
988
1267
  later(() => sfx.reach(), 150);
989
1268
  }
990
- function bingo(fresh, pts) {
1269
+ function bingo(fresh, pts, total) {
991
1270
  const nl = fresh.length;
992
1271
  const seq = [...new Set(fresh.flatMap((k) => cellLines[k]))].map((c) => cellTiles[c]);
993
1272
  const step = Math.min(70, 300 / seq.length);
@@ -1001,7 +1280,7 @@ function createPackingBingo(container, options = {}) {
1001
1280
  );
1002
1281
  later(() => {
1003
1282
  $("bT").textContent = nl >= 3 ? M.bannerTriple : nl === 2 ? M.bannerDouble : M.bannerSingle;
1004
- $("bS").textContent = nl > 1 ? M.bannerMulti(nl) : M.bannerNth(cur.length);
1283
+ $("bS").textContent = nl > 1 ? M.bannerMulti(nl) : M.bannerNth(total);
1005
1284
  replay($("banner"), "pkb-show");
1006
1285
  if (!calm) {
1007
1286
  replay($("flash"), "pkb-go");
@@ -1018,12 +1297,16 @@ function createPackingBingo(container, options = {}) {
1018
1297
  perfectPending = true;
1019
1298
  perfectEl.classList.remove("pkb-settled");
1020
1299
  const bonus = gotP ? 0 : 1e3;
1300
+ const speed = gotP || finalMs == null ? 0 : speedBonus(N, finalMs);
1021
1301
  gotP = true;
1022
- score += bonus;
1302
+ score += bonus + speed;
1023
1303
  cellTiles.forEach((t, c) => hop(t, far(c) * 180 / Math.max(1, n - 1)));
1024
1304
  sfx.perfect();
1025
1305
  if (!calm) replay($("flash"), "pkb-go");
1306
+ setResult(false);
1026
1307
  $("pbody").textContent = perfectText(M, N);
1308
+ $("ptime").hidden = finalMs == null;
1309
+ $("pT").textContent = formatTime(finalMs ?? 0, 2);
1027
1310
  $("sScore").textContent = "0";
1028
1311
  $("sCombo").textContent = "\xD70";
1029
1312
  $("sBingo").textContent = "0";
@@ -1039,9 +1322,11 @@ function createPackingBingo(container, options = {}) {
1039
1322
  burst(BW, BH, 90, { angle: -Math.PI * 2 / 3, spread: 0.8, speed: 22, g: 0.32, star: 0.3 });
1040
1323
  rain(3500);
1041
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");
1042
1327
  countUp($("sScore"), 0, score, 1300, (v) => v.toLocaleString(), true);
1043
1328
  countUp($("sCombo"), 0, maxCombo, 700, (v) => "\xD7" + v);
1044
- countUp($("sBingo"), 0, cur.length, 700, String);
1329
+ countUp($("sBingo"), 0, resultLines(), 700, String);
1045
1330
  countScore();
1046
1331
  }, PERFECT_STAMP_DELAY_MS);
1047
1332
  later(() => emojiRain(3e3), 1500);
@@ -1052,10 +1337,67 @@ function createPackingBingo(container, options = {}) {
1052
1337
  beat?.(pulse);
1053
1338
  }, at)
1054
1339
  );
1055
- const e = { score, maxCombo };
1340
+ const e = { score, maxCombo, time: finalMs == null ? null : Math.round(finalMs), speedBonus: speed };
1056
1341
  events.push(() => options.onPerfect?.(e));
1057
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");
1058
1399
  function cancelFx() {
1400
+ if (pending.size) clearNow(false);
1059
1401
  gen++;
1060
1402
  perfectPending = false;
1061
1403
  timers.forEach((id) => clearTimeout(id));
@@ -1076,21 +1418,32 @@ function createPackingBingo(container, options = {}) {
1076
1418
  $("flash").classList.remove("pkb-go");
1077
1419
  $("combo").classList.remove("pkb-on");
1078
1420
  $("cbar").classList.remove("pkb-draining");
1079
- cellTiles.forEach((t) => t.classList.remove("pkb-squash", "pkb-hop"));
1421
+ cellTiles.forEach((t) => t.classList.remove("pkb-squash", "pkb-hop", "pkb-clr"));
1080
1422
  stamps.forEach((s) => s.classList.remove("pkb-hit"));
1081
- board.classList.remove("pkb-punch", "pkb-shake");
1423
+ board.classList.remove("pkb-punch", "pkb-shake", "pkb-switching");
1424
+ mfx.classList.remove("pkb-show");
1082
1425
  if (!keepPerfect) perfectEl.classList.remove("pkb-show", "pkb-settled");
1083
1426
  }
1084
1427
  const completeLines = () => lines.flatMap((l, k) => l.every((i) => checked[i]) ? [k] : []);
1085
1428
  function render(src, quiet) {
1086
1429
  const events = [];
1087
1430
  const on = checked, c = on.filter(Boolean).length;
1431
+ const endless = inf();
1432
+ const all = c === N && !endless;
1088
1433
  cur = completeLines();
1089
1434
  const fresh = cur.filter((k) => !prevLines.has(k));
1090
1435
  if ([...prevLines].some((k) => !cur.includes(k))) {
1091
1436
  cancelFx();
1092
1437
  if (c < N) perfectEl.classList.remove("pkb-show");
1093
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
+ }
1094
1447
  const hit = new Set(cur.flatMap((k) => cellLines[k]));
1095
1448
  const reach = /* @__PURE__ */ new Set();
1096
1449
  for (const l of lines) {
@@ -1099,26 +1452,31 @@ function createPackingBingo(container, options = {}) {
1099
1452
  }
1100
1453
  tiles.forEach((t, i) => {
1101
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");
1102
1459
  t.classList.toggle("pkb-line", hit.has(L.itemCell[slotOf[i]]));
1103
1460
  t.classList.toggle("pkb-reach", reach.has(i));
1104
1461
  });
1105
1462
  freeTile?.classList.toggle("pkb-line", hit.has(L.center));
1106
- board.classList.toggle("pkb-all", c === N);
1107
- kick.classList.toggle("pkb-gold", c === N);
1108
- 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;
1109
1466
  $("cnt").textContent = `${c}/${N}`;
1110
- $("bng").textContent = String(cur.length);
1467
+ const total = endless ? cleared : cur.length;
1468
+ $("bng").textContent = String(total);
1111
1469
  const bp = $("bp");
1112
- if (cur.length > lastN) {
1470
+ if (total > lastN) {
1113
1471
  bp.classList.remove("pkb-hot");
1114
1472
  void bp.offsetWidth;
1115
1473
  }
1116
- bp.classList.toggle("pkb-hot", cur.length > 0);
1117
- lastN = cur.length;
1474
+ bp.classList.toggle("pkb-hot", total > 0);
1475
+ lastN = total;
1118
1476
  drawLines(fresh);
1119
1477
  if (src !== null) {
1120
1478
  const newReach = [...reach].some((i) => !prevReach.has(i));
1121
- const moment = !quiet && c === N && !prevAll ? "perfect" : !quiet && fresh.length ? "bingo" : !quiet && newReach ? "reach" : "tap";
1479
+ const moment = !quiet && all && !prevAll ? "perfect" : !quiet && fresh.length ? "bingo" : !quiet && newReach ? "reach" : "tap";
1122
1480
  tapFx(src, on[src], moment, fresh);
1123
1481
  }
1124
1482
  const newL = fresh.map((k) => lineKey(lines[k])).filter((key) => !gotL.has(key));
@@ -1127,10 +1485,10 @@ function createPackingBingo(container, options = {}) {
1127
1485
  score += linePts;
1128
1486
  if (quiet) {
1129
1487
  if (linePts) countScore();
1130
- } else if (c === N && !prevAll) perfect(events);
1488
+ } else if (all && !prevAll) perfect(events);
1131
1489
  else if (fresh.length) {
1132
- bingo(fresh, linePts);
1133
- const e = { newLines: fresh, totalLines: cur.length, score };
1490
+ bingo(fresh, linePts, total);
1491
+ const e = { newLines: fresh, totalLines: total, score, mode };
1134
1492
  events.push(() => options.onBingo?.(e));
1135
1493
  } else if (src !== null && on[src]) {
1136
1494
  const nr = [...reach].filter((i) => !prevReach.has(i));
@@ -1138,9 +1496,9 @@ function createPackingBingo(container, options = {}) {
1138
1496
  }
1139
1497
  prevLines = new Set(cur);
1140
1498
  prevReach = reach;
1141
- prevAll = c === N;
1499
+ prevAll = all;
1142
1500
  const labels = [...reach].map((i) => items[i].label);
1143
- $("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);
1144
1502
  return events;
1145
1503
  }
1146
1504
  const flush = (events) => {
@@ -1150,9 +1508,11 @@ function createPackingBingo(container, options = {}) {
1150
1508
  }
1151
1509
  };
1152
1510
  const save = () => {
1153
- if (storageKey) writeStore(storageKey, JSON.stringify(checked));
1511
+ if (storageKey && !inf()) writeStore(storageKey, JSON.stringify(checked));
1154
1512
  };
1155
1513
  function apply(next, src, quiet) {
1514
+ if (phase === "idle" && src !== null && next[src]) startWatch();
1515
+ if (!inf() && next.every(Boolean)) finish();
1156
1516
  checked = next;
1157
1517
  ins.forEach((x, i) => x.checked = checked[i]);
1158
1518
  const events = render(src, quiet);
@@ -1160,8 +1520,12 @@ function createPackingBingo(container, options = {}) {
1160
1520
  return events;
1161
1521
  }
1162
1522
  function baseline(next) {
1523
+ dropClear();
1524
+ cleared = 0;
1163
1525
  cancelFx();
1164
1526
  clearVisuals();
1527
+ if (!inf() && next.every(Boolean)) finish();
1528
+ else if ((phase === "play" || phase === "done") && !next.some(Boolean)) toReady();
1165
1529
  checked = next;
1166
1530
  ins.forEach((x, i) => x.checked = checked[i]);
1167
1531
  got.clear();
@@ -1171,7 +1535,7 @@ function createPackingBingo(container, options = {}) {
1171
1535
  done.forEach((k) => gotL.add(lineKey(lines[k])));
1172
1536
  prevLines = new Set(done);
1173
1537
  prevReach = /* @__PURE__ */ new Set();
1174
- prevAll = checked.every(Boolean);
1538
+ prevAll = !inf() && checked.every(Boolean);
1175
1539
  gotP = prevAll;
1176
1540
  score = shown = got.size * 10 + gotL.size * 100;
1177
1541
  combo = 0;
@@ -1181,7 +1545,9 @@ function createPackingBingo(container, options = {}) {
1181
1545
  save();
1182
1546
  }
1183
1547
  function request(next, src, opts = {}) {
1184
- if (controlled) {
1548
+ const game = inf();
1549
+ if (controlled && !game) {
1550
+ resetAsked = !!opts.reset && resetAsked;
1185
1551
  ins.forEach((x, i) => x.checked = checked[i]);
1186
1552
  options.onChange?.(next.slice());
1187
1553
  return;
@@ -1189,7 +1555,7 @@ function createPackingBingo(container, options = {}) {
1189
1555
  let events = [];
1190
1556
  if (opts.reset) baseline(next);
1191
1557
  else events = apply(next, src, !!opts.quiet);
1192
- options.onChange?.(checked.slice());
1558
+ if (!game) options.onChange?.(checked.slice());
1193
1559
  flush(events);
1194
1560
  }
1195
1561
  function place(next) {
@@ -1208,12 +1574,12 @@ function createPackingBingo(container, options = {}) {
1208
1574
  focused.focus({ preventScroll: true });
1209
1575
  }
1210
1576
  const saveOrder = () => {
1211
- if (!orderKey) return;
1577
+ if (!orderKey || inf()) return;
1212
1578
  writeStore(orderKey, JSON.stringify(order));
1213
1579
  };
1214
1580
  let flights = [];
1215
1581
  function arrange(next) {
1216
- if (!checked.every(Boolean)) cancelFx();
1582
+ if (inf() || !checked.every(Boolean)) cancelFx();
1217
1583
  const from = calm ? null : tiles.map((t) => t.getBoundingClientRect());
1218
1584
  flights.forEach((a) => a.cancel());
1219
1585
  flights = [];
@@ -1249,6 +1615,7 @@ function createPackingBingo(container, options = {}) {
1249
1615
  svg.innerHTML = "";
1250
1616
  }
1251
1617
  function shuffleCard() {
1618
+ if (inf() && phase === "done") return;
1252
1619
  let next = order.slice();
1253
1620
  do {
1254
1621
  for (let s = N - 1; s > 0; s--) {
@@ -1263,26 +1630,93 @@ function createPackingBingo(container, options = {}) {
1263
1630
  const unshuffle = () => {
1264
1631
  if (!isIdentity(order)) arrange(identity());
1265
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
+ });
1266
1638
  grid.addEventListener("change", (e) => {
1267
1639
  const i = ins.indexOf(e.target);
1268
1640
  if (i < 0) return;
1641
+ if (locked(i)) {
1642
+ ins[i].checked = true;
1643
+ return;
1644
+ }
1269
1645
  const next = checked.slice();
1270
1646
  next[i] = !next[i];
1271
1647
  request(next, i);
1272
1648
  });
1273
1649
  container.querySelector('[data-r="shuf"]')?.addEventListener("click", shuffleCard);
1650
+ const restartInfinite = () => {
1651
+ baseline(normalize(void 0));
1652
+ toReady();
1653
+ };
1274
1654
  $("reset").addEventListener("click", () => {
1655
+ if (inf()) return restartInfinite();
1275
1656
  unshuffle();
1657
+ restartGame();
1276
1658
  request(normalize(void 0), null, { reset: true });
1277
1659
  });
1278
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);
1279
1713
  perfectEl.addEventListener("click", (e) => {
1280
1714
  if (e.target === perfectEl) perfectEl.classList.remove("pkb-show");
1281
1715
  });
1282
1716
  if (demo) {
1283
1717
  let busy = false;
1284
1718
  const prepare = () => {
1285
- if (checked.every(Boolean)) request(normalize(void 0), null, { reset: true });
1719
+ if (!inf() && checked.every(Boolean)) request(normalize(void 0), null, { reset: true });
1286
1720
  else perfectEl.classList.remove("pkb-show");
1287
1721
  };
1288
1722
  const run = async (steps) => {
@@ -1290,6 +1724,7 @@ function createPackingBingo(container, options = {}) {
1290
1724
  busy = true;
1291
1725
  try {
1292
1726
  prepare();
1727
+ skipReady();
1293
1728
  const g = gen;
1294
1729
  for (const s of steps) {
1295
1730
  await wait(s.delay);
@@ -1303,21 +1738,100 @@ function createPackingBingo(container, options = {}) {
1303
1738
  }
1304
1739
  };
1305
1740
  $("dB").addEventListener("click", () => {
1741
+ if (inf() && phase === "done") return;
1306
1742
  const open = () => lines.map((l) => l.filter((i) => !checked[i])).filter((m) => m.length).sort((a, b) => a.length - b.length)[0] ?? [];
1307
- const pick = checked.every(Boolean) ? [...lines[0]] : open();
1743
+ const pick = !inf() && checked.every(Boolean) ? [...lines[0]] : open();
1308
1744
  void run(pick.map((i) => ({ i, delay: 320 })));
1309
1745
  });
1310
1746
  $("dP").addEventListener("click", () => {
1747
+ if (inf()) return;
1311
1748
  const all = items.map((_, i) => i);
1312
1749
  const miss = checked.every(Boolean) ? all : all.filter((i) => !checked[i]);
1313
1750
  void run(miss.map((i, k) => k < miss.length - 1 ? { i, delay: 170, quiet: true } : { i, delay: 450 }));
1314
1751
  });
1315
1752
  $("dR").addEventListener("click", () => {
1316
- if (busy) return;
1753
+ if (busy || inf()) return;
1317
1754
  unshuffle();
1755
+ restartGame();
1318
1756
  request(demoStart(), null, { reset: true });
1319
1757
  });
1320
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
+ });
1321
1835
  let startOrder = null;
1322
1836
  if (orderKey) {
1323
1837
  try {
@@ -1339,7 +1853,10 @@ function createPackingBingo(container, options = {}) {
1339
1853
  if (!initial && options.defaultChecked) initial = normalize(options.defaultChecked);
1340
1854
  if (!initial && demo) initial = demoStart();
1341
1855
  fit();
1342
- 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();
1343
1860
  const ro = typeof ResizeObserver === "function" ? new ResizeObserver(() => {
1344
1861
  fit();
1345
1862
  drawLines();
@@ -1348,7 +1865,7 @@ function createPackingBingo(container, options = {}) {
1348
1865
  ro?.observe(gw);
1349
1866
  const onVisibility = () => {
1350
1867
  if (!document.hidden) {
1351
- if (restorePerfectFocus && perfectEl.classList.contains("pkb-show")) $("close").focus({ preventScroll: true });
1868
+ if (restorePerfectFocus && perfectEl.classList.contains("pkb-show")) resultFocus().focus({ preventScroll: true });
1352
1869
  restorePerfectFocus = false;
1353
1870
  return;
1354
1871
  }
@@ -1359,7 +1876,7 @@ function createPackingBingo(container, options = {}) {
1359
1876
  if (keepPerfect) {
1360
1877
  $("sScore").textContent = score.toLocaleString();
1361
1878
  $("sCombo").textContent = "\xD7" + maxCombo;
1362
- $("sBingo").textContent = String(cur.length);
1879
+ $("sBingo").textContent = String(resultLines());
1363
1880
  perfectEl.classList.add("pkb-show", "pkb-settled");
1364
1881
  }
1365
1882
  combo = 0;
@@ -1369,6 +1886,14 @@ function createPackingBingo(container, options = {}) {
1369
1886
  setChecked(next, opts = {}) {
1370
1887
  if (dead) return;
1371
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;
1372
1897
  const changed = n2.flatMap((v, i) => v !== checked[i] ? [i] : []);
1373
1898
  if (!changed.length) return;
1374
1899
  if (!opts.animate || changed.length !== 1) {
@@ -1379,12 +1904,20 @@ function createPackingBingo(container, options = {}) {
1379
1904
  },
1380
1905
  reset() {
1381
1906
  if (dead) return;
1907
+ if (inf()) return restartInfinite();
1382
1908
  unshuffle();
1909
+ restartGame();
1383
1910
  request(normalize(void 0), null, { reset: true });
1384
1911
  },
1385
1912
  shuffle() {
1386
1913
  if (!dead) shuffleCard();
1387
1914
  },
1915
+ setMode(next) {
1916
+ setMode(next);
1917
+ },
1918
+ finish() {
1919
+ finishGame();
1920
+ },
1388
1921
  refresh() {
1389
1922
  if (!dead) render(null, true);
1390
1923
  },
@@ -1394,20 +1927,25 @@ function createPackingBingo(container, options = {}) {
1394
1927
  lines: cur.slice(),
1395
1928
  score,
1396
1929
  maxCombo,
1397
- 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() },
1398
1935
  size: n,
1399
1936
  free: L.free,
1400
- order: order.slice()
1937
+ order: order.slice(),
1938
+ time: Math.round(elapsed())
1401
1939
  };
1402
1940
  },
1403
1941
  destroy() {
1404
1942
  if (dead) return;
1405
1943
  dead = true;
1406
- grid.removeEventListener("click", beforePlay, true);
1407
- if (playDialog?.open) playDialog.close();
1944
+ stopTimers();
1408
1945
  timers.forEach((id) => clearTimeout(id));
1409
1946
  timers.clear();
1410
1947
  clearTimeout(landing);
1948
+ clearTimeout(clearTimer);
1411
1949
  flights.forEach((a) => a.cancel());
1412
1950
  cancelAnimationFrame(raf);
1413
1951
  cancelAnimationFrame(tw);
@@ -1423,11 +1961,12 @@ function createPackingBingo(container, options = {}) {
1423
1961
  // src/react.tsx
1424
1962
  var import_jsx_runtime = require("react/jsx-runtime");
1425
1963
  var PackingBingo = (0, import_react.forwardRef)(function PackingBingo2(props, ref) {
1426
- 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;
1427
1965
  const host = (0, import_react.useRef)(null);
1428
1966
  const inst = (0, import_react.useRef)(null);
1429
- const callbacks = (0, import_react.useRef)({ onChange, onBingo, onPerfect });
1430
- 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);
1431
1970
  const messagesRef = (0, import_react.useRef)(messages);
1432
1971
  messagesRef.current = messages;
1433
1972
  const controlled = checked !== void 0;
@@ -1457,13 +1996,20 @@ var PackingBingo = (0, import_react.forwardRef)(function PackingBingo2(props, re
1457
1996
  callbacks.current.onChange?.(next);
1458
1997
  },
1459
1998
  onBingo: (e) => callbacks.current.onBingo?.(e),
1460
- 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
+ }
1461
2006
  });
1462
2007
  inst.current = instance;
1463
2008
  return () => {
1464
2009
  const state = instance.getState();
1465
- if (!controlled) latest.current = state.checked;
1466
- latestOrder.current = state.order;
2010
+ const packed = state.packing ?? state;
2011
+ if (!controlled) latest.current = packed.checked;
2012
+ latestOrder.current = packed.order;
1467
2013
  latestKey.current = storageKey;
1468
2014
  instance.destroy();
1469
2015
  if (inst.current === instance) inst.current = null;
@@ -1475,11 +2021,16 @@ var PackingBingo = (0, import_react.forwardRef)(function PackingBingo2(props, re
1475
2021
  (0, import_react.useEffect)(() => {
1476
2022
  if (controlled && checked) inst.current?.setChecked(checked, { animate: true });
1477
2023
  }, [controlled, checked]);
2024
+ (0, import_react.useEffect)(() => {
2025
+ if (mode) inst.current?.setMode(mode);
2026
+ }, [mode]);
1478
2027
  (0, import_react.useImperativeHandle)(
1479
2028
  ref,
1480
2029
  () => ({
1481
2030
  reset: () => inst.current?.reset(),
1482
2031
  shuffle: () => inst.current?.shuffle(),
2032
+ setMode: (m) => inst.current?.setMode(m),
2033
+ finish: () => inst.current?.finish(),
1483
2034
  setChecked: (next) => inst.current?.setChecked(next, { animate: true }),
1484
2035
  getState: () => inst.current?.getState() ?? null
1485
2036
  }),
@@ -1495,5 +2046,6 @@ var PackingBingo = (0, import_react.forwardRef)(function PackingBingo2(props, re
1495
2046
  bingoLines,
1496
2047
  createPackingBingo,
1497
2048
  en,
1498
- ja
2049
+ ja,
2050
+ speedBonus
1499
2051
  });