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/core.cjs CHANGED
@@ -26,7 +26,9 @@ __export(core_exports, {
26
26
  bingoLines: () => bingoLines,
27
27
  createPackingBingo: () => createPackingBingo,
28
28
  en: () => en,
29
- ja: () => ja
29
+ formatTime: () => formatTime,
30
+ ja: () => ja,
31
+ speedBonus: () => speedBonus
30
32
  });
31
33
  module.exports = __toCommonJS(core_exports);
32
34
 
@@ -133,11 +135,27 @@ var ja = {
133
135
  shuffle: "\u{1F500} \u30B7\u30E3\u30C3\u30D5\u30EB",
134
136
  soundOn: "\u{1F50A} \u97F3\u3042\u308A",
135
137
  soundOff: "\u{1F507} \u97F3\u306A\u3057",
136
- soundHint: "\u7AEF\u672B\u306E\u97F3\u91CF\u3092\u4E0A\u3052\u3066\u306D\u3002",
137
- 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",
138
- playTitle: "\u97F3\u3068\u632F\u52D5\u306E\u6E96\u5099\u306FOK\uFF1F",
139
- playStart: "\u6E96\u5099OK\uFF01\u306F\u3058\u3081\u308B",
140
- playSkip: "\u3053\u306E\u307E\u307E\u904A\u3076",
138
+ readyTitle: "Are you ready?",
139
+ readyPrompt: "",
140
+ soundHint: "\u7AEF\u672B\u306E\u97F3\u91CF\u3092MAX\u306B",
141
+ vibrationHint: "\u304A\u3084\u3059\u307F\u30E2\u30FC\u30C9\u3092\u30AA\u30D5",
142
+ readyTap: "\u30BF\u30C3\u30D7\u3067\u30B9\u30BF\u30FC\u30C8",
143
+ readyGo: "START!",
144
+ time: "\u30BF\u30A4\u30E0",
145
+ statTime: "\u30AF\u30EA\u30A2\u30BF\u30A4\u30E0",
146
+ modeLabel: "\u30E2\u30FC\u30C9",
147
+ modeNormal: "\u901A\u5E38",
148
+ modeInfinite: "\u221E \u7121\u9650",
149
+ normalTitle: "NORMAL",
150
+ normalSub: "\u901A\u5E38\u30E2\u30FC\u30C9",
151
+ infiniteTitle: "INFINITE",
152
+ infiniteSub: "\u7121\u9650\u30D3\u30F3\u30B4",
153
+ 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",
154
+ finish: "\u{1F3C1} FINISH",
155
+ finishTitle: "\u30D5\u30A3\u30CB\u30C3\u30B7\u30E5\uFF01",
156
+ finishStamp: "\u221E",
157
+ finishTime: "\u30D7\u30EC\u30A4\u30BF\u30A4\u30E0",
158
+ again: "\u3082\u3046\u4E00\u5EA6",
141
159
  stamp: "OK",
142
160
  free: "FREE",
143
161
  reachBadge: "\u30EA\u30FC\u30C1",
@@ -148,15 +166,15 @@ var ja = {
148
166
  bannerNth: (n) => `${n} \u30E9\u30A4\u30F3\u76EE\uFF01`,
149
167
  bannerMulti: (n) => `\u4E00\u6C17\u306B ${n} \u30E9\u30A4\u30F3\uFF01`,
150
168
  statusStart: () => "",
151
- statusLines: (lines, left) => `\u30D3\u30F3\u30B4 ${lines} \u30E9\u30A4\u30F3\uFF01 \u306E\u3053\u308A ${left} \u3053`,
169
+ statusLines: () => "",
152
170
  statusReach: () => "",
153
- statusDone: "\u{1F389} \u30D1\u30FC\u30D5\u30A7\u30AF\u30C8\uFF01\u5FD8\u308C\u7269\u30BC\u30ED\u3067\u51FA\u767A\u3067\u304D\u307E\u3059",
171
+ statusDone: "",
154
172
  perfectTag: "\u{1F3C6} PERFECT",
155
173
  perfectStamp: "\u6E96\u5099\n\u5B8C\u4E86",
156
174
  perfectTitle: "\u30D1\u30FC\u30D5\u30A7\u30AF\u30C8\uFF01",
157
- perfectBody: (n) => `${n}\u3053 \u305C\u3093\u3076\u305D\u308D\u3063\u305F\uFF01
158
- \u5FD8\u308C\u7269\u30BC\u30ED\u3067\u3001\u3044\u3063\u3066\u3089\u3063\u3057\u3083\u3044\uFF01`,
175
+ perfectBody: "",
159
176
  perfectBonus: (n) => `PERFECT BONUS +${n}`,
177
+ speedBonus: (n) => `SPEED BONUS +${n.toLocaleString()}`,
160
178
  statScore: "SCORE",
161
179
  statCombo: "MAX COMBO",
162
180
  statLines: "BINGO",
@@ -179,11 +197,27 @@ var en = {
179
197
  shuffle: "\u{1F500} Shuffle",
180
198
  soundOn: "\u{1F50A} Sound on",
181
199
  soundOff: "\u{1F507} Muted",
182
- soundHint: "Turn up your device volume before playing.",
183
- vibrationHint: "On Android, turn off Do Not Disturb to enjoy vibration.",
184
- playTitle: "Ready for sound and vibration?",
185
- playStart: "Ready! Let's play",
186
- playSkip: "Play as is",
200
+ readyTitle: "Are you ready?",
201
+ readyPrompt: "",
202
+ soundHint: "Turn your volume to MAX",
203
+ vibrationHint: "Turn off Do Not Disturb",
204
+ readyTap: "Tap to start",
205
+ readyGo: "GO!",
206
+ time: "Time",
207
+ statTime: "Time",
208
+ modeLabel: "Mode",
209
+ modeNormal: "Normal",
210
+ modeInfinite: "\u221E Infinite",
211
+ normalTitle: "NORMAL",
212
+ normalSub: "Normal mode",
213
+ infiniteTitle: "INFINITE",
214
+ infiniteSub: "Infinite bingo",
215
+ infiniteRule: "Completed lines clear, so you can fill them again. Bingo forever!",
216
+ finish: "\u{1F3C1} FINISH",
217
+ finishTitle: "Finish!",
218
+ finishStamp: "\u221E",
219
+ finishTime: "Time played",
220
+ again: "Play again",
187
221
  stamp: "OK",
188
222
  free: "FREE",
189
223
  reachBadge: "REACH",
@@ -194,15 +228,15 @@ var en = {
194
228
  bannerNth: (n) => `Line ${n}!`,
195
229
  bannerMulti: (n) => `${n} lines at once!`,
196
230
  statusStart: () => "",
197
- statusLines: (lines, left) => `${lines} bingo line${lines === 1 ? "" : "s"}! ${left} to go`,
231
+ statusLines: () => "",
198
232
  statusReach: () => "",
199
- statusDone: "\u{1F389} Perfect! Nothing left behind.",
233
+ statusDone: "",
200
234
  perfectTag: "\u{1F3C6} PERFECT",
201
235
  perfectStamp: "ALL\nSET",
202
236
  perfectTitle: "Perfect!",
203
- perfectBody: (n) => `All ${n} packed!
204
- Have a great trip!`,
237
+ perfectBody: "",
205
238
  perfectBonus: (n) => `PERFECT BONUS +${n}`,
239
+ speedBonus: (n) => `SPEED BONUS +${n.toLocaleString()}`,
206
240
  statScore: "SCORE",
207
241
  statCombo: "MAX COMBO",
208
242
  statLines: "BINGO",
@@ -343,6 +377,36 @@ function createSound(isMuted) {
343
377
  tone(hz(7), 0.34, 0.12, "triangle", 0.14);
344
378
  tone(hz(12), 0.42, 0.2, "triangle", 0.14);
345
379
  },
380
+ count(go) {
381
+ if (go) {
382
+ tone(hz(12), 0, 0.5, "square", 0.07);
383
+ tone(hz(24), 0, 0.5, "triangle", 0.14);
384
+ noise(0, 0.2, 0.12, 3e3);
385
+ } else {
386
+ tone(hz(5), 0, 0.14, "square", 0.07);
387
+ tone(hz(17), 0, 0.14, "triangle", 0.12);
388
+ }
389
+ },
390
+ mode(infinite) {
391
+ if (infinite) {
392
+ tone(196, 0, 0.42, "sine", 0.12, 784);
393
+ [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));
394
+ [12, 16, 19, 24].forEach((s) => tone(hz(s), 0.38, 1.1, "sine", 0.07));
395
+ tone(hz(31), 0.38, 0.9, "triangle", 0.03);
396
+ noise(0, 0.5, 0.07, 6500);
397
+ noise(0.36, 0.7, 0.05, 9e3);
398
+ } else {
399
+ tone(hz(7), 0, 0.1, "square", 0.05);
400
+ tone(hz(0), 0.1, 0.28, "triangle", 0.14);
401
+ tone(160, 0.1, 0.2, "sine", 0.26, 55);
402
+ noise(0.1, 0.12, 0.14, 1400);
403
+ }
404
+ },
405
+ clear(squares) {
406
+ const n = Math.max(1, Math.min(8, squares));
407
+ for (let i = 0; i < n; i++) tone(step(SCALE.length - 1 - i * 2), i * 0.035, 0.18, "sine", 0.07);
408
+ noise(0, 0.3, 0.08, 7e3);
409
+ },
346
410
  stop,
347
411
  close() {
348
412
  stop();
@@ -385,7 +449,10 @@ function createHaptics(enabled) {
385
449
  const current = ++generation;
386
450
  const tier = Math.min(3, Math.floor(combo / 3));
387
451
  const lead = Math.max(80, impactAt - 40);
388
- 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;
452
+ 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 : (
453
+ // Switching modes: a quick rising flutter into a hold, or one firm stamp.
454
+ 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
455
+ );
389
456
  if (!send(pattern)) return;
390
457
  return (beat) => {
391
458
  if (generation === current && owner === token) send([...beat]);
@@ -436,24 +503,17 @@ var CSS = `
436
503
  .pkb-board.pkb-punch{animation:pkb-punch .4s cubic-bezier(.3,1.6,.5,1)}
437
504
  @keyframes pkb-punch{30%{transform:scale(1.018)}}
438
505
  .pkb-head{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:12px 16px;margin-bottom:18px}
439
- .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}
440
- .pkb-play-dialog:not([open]){display:none}
441
- .pkb-play-dialog::backdrop{background:rgba(42,31,20,.6)}
442
- .pkb-play-dialog h3{margin-bottom:16px;font-size:22px;font-weight:800;line-height:1.4}
443
- .pkb-play-dialog p{margin-bottom:12px;font-size:15px;font-weight:700;line-height:1.7;overflow-wrap:anywhere}
444
- .pkb-play-actions{display:grid;gap:10px;margin-top:22px}
445
- .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}
446
- .pkb-play-start{background:var(--pk-sun)}
447
- .pkb-play-skip{background:transparent}
448
- .pkb-play-dialog button:focus-visible{outline:3px solid var(--pk-ink);outline-offset:3px}
449
- .pkb-play-dialog button:hover{filter:brightness(.96)}
450
- .pkb-tags{display:flex;gap:8px;align-items:center}
506
+ .pkb-tags{display:flex;flex-wrap:wrap;gap:8px;align-items:center}
451
507
  .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}
452
508
  .pkb-kicker:empty{display:none}
453
509
  .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}
454
510
  @keyframes pkb-rb{to{background-position:-200% 0}}
455
511
  .pkb button{font-family:var(--pk-body)}
456
512
  .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}
513
+ .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}
514
+ .pkb-watch::before{content:"\u23F1 ";font-family:var(--pk-body)}
515
+ .pkb-watch.pkb-run{color:var(--pk-sun)}
516
+ .pkb-watch.pkb-done{background:var(--pk-red);color:#fff}
457
517
  .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}
458
518
  .pkb-sub{font-weight:800;font-size:13px;margin-top:4px;word-break:auto-phrase}
459
519
  .pkb-side{display:flex;flex-direction:column;gap:8px;flex:none}
@@ -476,7 +536,7 @@ var CSS = `
476
536
  .pkb-gw{position:relative}
477
537
  .pkb-grid{display:grid;grid-template-columns:repeat(var(--pk-n),minmax(0,1fr));gap:calc(12px*var(--pk-d))}
478
538
  .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}
479
- .pkb-tile:active:not(.pkb-free){transform:translate(3px,3px);box-shadow:1px 1px 0 var(--pk-ink)}
539
+ .pkb-tile:active:not(.pkb-free):not(.pkb-lock){transform:translate(3px,3px);box-shadow:1px 1px 0 var(--pk-ink)}
480
540
  .pkb-tile input{position:absolute;opacity:0;pointer-events:none;width:1px;height:1px}
481
541
  .pkb-tile:has(input:focus-visible){outline:3px solid var(--pk-ink);outline-offset:3px}
482
542
  .pkb-emo{font-size:max(14px,calc(32px*var(--pk-s)));line-height:1;transition:opacity .2s}
@@ -484,6 +544,7 @@ var CSS = `
484
544
  .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}
485
545
  .pkb-tile.pkb-on .pkb-stamp{opacity:.9;transform:translate(-50%,-50%) rotate(-16deg) scale(1)}
486
546
  .pkb-tile.pkb-on .pkb-emo,.pkb-tile.pkb-on .pkb-lbl{opacity:.35}
547
+ .pkb-tile.pkb-lock{cursor:default}
487
548
  .pkb-tile.pkb-free{cursor:default;container-type:inline-size}
488
549
  .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)}
489
550
  .pkb-tile.pkb-line{background-color:var(--pk-mint)}
@@ -511,10 +572,11 @@ var CSS = `
511
572
  .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}
512
573
  .pkb-foot>span{flex:1 1 14em}
513
574
  .pkb-foot>span:empty{flex-basis:0}
514
- .pkb-acts{display:flex;gap:8px;flex:none;margin-left:auto}
575
+ .pkb-acts{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:8px;flex:0 1 auto;max-width:100%;margin-left:auto}
515
576
  .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}
516
577
  .pkb-shuf{background:var(--pk-paper);box-shadow:2px 2px 0 var(--pk-ink)}
517
- .pkb-shuf:active{transform:translate(2px,2px);box-shadow:none}
578
+ .pkb-shuf:active:not(:disabled){transform:translate(2px,2px);box-shadow:none}
579
+ .pkb-shuf:disabled{cursor:default;opacity:.4}
518
580
  .pkb-flash{position:absolute;inset:0;z-index:7;background:#fff;opacity:0;pointer-events:none}
519
581
  .pkb-flash.pkb-go{animation:pkb-flash .4s ease-out}
520
582
  @keyframes pkb-flash{0%{opacity:.8}100%{opacity:0}}
@@ -531,8 +593,33 @@ var CSS = `
531
593
  .pkb-float.pkb-big{font-size:clamp(14px,5.6cqi,34px);color:#FFE066}
532
594
  .pkb-float.pkb-rch{font-size:20px;color:var(--pk-red);-webkit-text-stroke:4px var(--pk-paper)}
533
595
  @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)}}
596
+ .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}
597
+ .pkb-ready[hidden]{display:none}
598
+ .pkb-ready:focus-visible{outline:3px solid var(--pk-ink);outline-offset:-6px}
599
+ .pkb-ready.pkb-gone{pointer-events:none;background-color:transparent}
600
+ .pkb-board.pkb-waiting .pkb-snd{position:relative;z-index:13}
601
+ .pkb-ready-in,.pkb-count{grid-area:1/1}
602
+ .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)}
603
+ .pkb-ready.pkb-counting .pkb-ready-in{visibility:hidden}
604
+ .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)}
605
+ .pkb-ready-q{margin-top:6px;font-weight:800;font-size:18px;line-height:1.4}
606
+ .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}
607
+ .pkb-ready-l>span::before{content:"\u30FB"}
608
+ .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}
609
+ @keyframes pkb-beat{50%{transform:scale(1.06)}}
610
+ .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}
611
+ .pkb-count.pkb-go{font-size:clamp(48px,17cqi,120px);color:var(--pk-red);white-space:nowrap}
612
+ .pkb-count.pkb-tick{animation:pkb-cnt 1s cubic-bezier(.2,1.4,.4,1) forwards}
613
+ .pkb-count.pkb-go.pkb-tick{animation:pkb-cnt-go .75s cubic-bezier(.2,1.4,.4,1) forwards}
614
+ @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)}}
615
+ @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)}}
616
+ .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)}
617
+ .pkb-ptime[hidden]{display:none}
618
+ .pkb-ptime small{font-size:11px;font-weight:800;letter-spacing:.08em;color:#fff}
619
+ .pkb-ptime b{font-family:var(--pk-display);font-weight:400;font-size:22px;line-height:1.2;font-variant-numeric:tabular-nums}
534
620
  .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}
535
- .pkb-perfect.pkb-show{opacity:1;visibility:visible}
621
+ /* Visible at once when opening (focus can't land on a hidden dialog); hidden only after the fade when closing. */
622
+ .pkb-perfect.pkb-show{opacity:1;visibility:visible;transition:opacity .3s,visibility 0s}
536
623
  .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)}
537
624
  .pkb-perfect.pkb-show .pkb-card{transform:none}
538
625
  .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)}
@@ -542,6 +629,7 @@ var CSS = `
542
629
  @keyframes pkb-slam{to{opacity:1;transform:rotate(-14deg) scale(1)}}
543
630
  .pkb-ptitle{font-family:var(--pk-display);font-weight:400;font-size:30px;line-height:1.3;color:inherit}
544
631
  .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}
632
+ .pkb-pbody:empty{margin:0 0 14px}
545
633
  .pkb-stats{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-bottom:16px}
546
634
  .pkb-stats>div{border:2px solid var(--pk-ink);border-radius:12px;padding:6px 4px 5px;background:#fff}
547
635
  .pkb-stats small{display:block;font-size:9.5px;font-weight:800;letter-spacing:.1em}
@@ -551,10 +639,72 @@ var CSS = `
551
639
  .pkb-fx{position:absolute;inset:0;width:100%;height:100%;z-index:10;pointer-events:none}
552
640
  .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}
553
641
  .pkb-demo button{appearance:none;border:1px solid #ccc;background:#f3f3f3;color:#222;border-radius:999px;padding:5px 12px;font-size:12px;cursor:pointer}
554
- @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}}
555
- @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}}
642
+ .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}
643
+ .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}
644
+ .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}
645
+ .pkb-mode button[aria-checked="true"]{opacity:1;color:var(--pk-ink);cursor:default}
646
+ .pkb-mode button:focus-visible{outline:2px solid var(--pk-paper);outline-offset:-4px}
647
+ .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)}
648
+ .pkb.pkb-inf .pkb-mode{box-shadow:0 0 14px 1px rgba(155,93,229,.6)}
649
+ .pkb.pkb-inf .pkb-mode::after{opacity:1;animation:pkb-glint 3.2s ease-in-out infinite}
650
+ .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}
651
+ .pkb.pkb-inf .pkb-mode button[data-m="infinite"]{color:#fff;text-shadow:0 1px 1px rgba(42,31,20,.55)}
652
+ .pkb-board.pkb-waiting .pkb-mode,.pkb-board.pkb-switching .pkb-mode{z-index:15}
653
+ .pkb-board.pkb-switching .pkb-fx{z-index:16}
654
+ .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)}
655
+ .pkb.pkb-inf .pkb-demo [data-r="dP"],.pkb.pkb-inf .pkb-demo [data-r="dR"]{display:none}
656
+ .pkb-ready-b,.pkb-ready-r{display:none}
657
+ .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)}
658
+ .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}
659
+ .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}
660
+ .pkb-tile.pkb-clr{animation:pkb-clr .62s var(--pkb-cd,0ms) both}
661
+ @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)}}
662
+ .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}
663
+ .pkb-mfx.pkb-show{visibility:visible;animation:pkb-mfx 1.7s ease-out forwards}
664
+ @keyframes pkb-mfx{0%{opacity:0}9%{opacity:1}72%{opacity:1}100%{opacity:0}}
665
+ .pkb-mfx-inf{background:radial-gradient(120% 95% at 50% 42%,#33205c 0%,#170f2e 52%,#08060f 100%)}
666
+ .pkb-mfx-n{background:var(--pk-sun) radial-gradient(rgba(42,31,20,.16) 1.3px,transparent 1.6px) 0 0/16px 16px}
667
+ .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}
668
+ .pkb-mfx.pkb-show::after{animation:pkb-beam 1s cubic-bezier(.4,0,.2,1) .22s both}
669
+ @keyframes pkb-beam{0%{opacity:1;transform:skewX(-18deg) translateX(0)}100%{opacity:1;transform:skewX(-18deg) translateX(400%)}}
670
+ .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}
671
+ .pkb-mfx.pkb-show .pkb-mfx-glow{animation:pkb-spin 3s linear infinite,pkb-glow 1.7s ease-out both}
672
+ @keyframes pkb-glow{0%{opacity:0;scale:.35}30%{opacity:.8;scale:1}100%{opacity:.5;scale:1.12}}
673
+ .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}
674
+ .pkb-mfx.pkb-show i{animation:pkb-twk .85s ease-in-out var(--t) both}
675
+ @keyframes pkb-twk{0%{opacity:0;scale:0;rotate:0deg}45%{opacity:1;scale:1;rotate:70deg}100%{opacity:0;scale:.2;rotate:160deg}}
676
+ .pkb-mfx-in{position:relative;display:flex;flex-direction:column;align-items:center;padding:0 16px}
677
+ .pkb-mfx-sym{display:block;font-family:var(--pk-display);font-weight:400;line-height:1}
678
+ .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))}
679
+ .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}
680
+ @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)}}
681
+ .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)}
682
+ .pkb-mfx.pkb-show.pkb-mfx-n .pkb-mfx-sym{animation:pkb-mslam .42s cubic-bezier(.5,0,.75,0) .08s both}
683
+ @keyframes pkb-mslam{from{opacity:0;transform:rotate(-14deg) scale(2.6)}to{opacity:1;transform:rotate(-14deg) scale(1)}}
684
+ .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}
685
+ .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)}
686
+ .pkb-mfx-n .pkb-mfx-t{margin-top:16px;color:var(--pk-ink)}
687
+ .pkb-mfx.pkb-show .pkb-mfx-t{animation:pkb-track 1.7s cubic-bezier(.16,1,.3,1) .12s both}
688
+ @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}}
689
+ .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}
690
+ .pkb-mfx-inf .pkb-mfx-s{color:#fff;background:rgba(255,255,255,.12);border:1.5px solid rgba(255,255,255,.4)}
691
+ .pkb-mfx-n .pkb-mfx-s{background:var(--pk-ink);color:var(--pk-sun)}
692
+ .pkb-mfx.pkb-show .pkb-mfx-s{animation:pkb-up .6s cubic-bezier(.2,1.3,.4,1) .4s both}
693
+ @keyframes pkb-up{from{opacity:0;transform:translateY(12px)}}
694
+ .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}
695
+ .pkb.pkb-inf .pkb-finbtn{display:inline-block}
696
+ .pkb-finbtn:active:not(:disabled){transform:translate(2px,2px);box-shadow:none}
697
+ .pkb-finbtn:disabled{cursor:default;opacity:.4;animation:none;box-shadow:2px 2px 0 var(--pk-ink)}
698
+ .pkb-board.pkb-over .pkb-grid{opacity:.55;filter:saturate(.5);transition:opacity .4s,filter .4s}
699
+ .pkb-pbtns{display:flex;flex-wrap:wrap;justify-content:center;gap:10px}
700
+ .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)}
701
+ .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)}
702
+ .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}
703
+ .pkb-perfect.pkb-fin .pkb-close:not(.pkb-again){background:var(--pk-paper)}
704
+ @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}}
705
+ @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}}
556
706
  @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}}
557
- @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}}
707
+ @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}}
558
708
  @keyframes pkb-fade{0%,100%{opacity:0}15%,80%{opacity:1}}
559
709
  `;
560
710
 
@@ -564,7 +714,6 @@ var CONFETTI = ["#E8473B", "#2F6FED", "#1FA971", "#FFFDF6", "#2A1F14", "#FF9F1C"
564
714
  var COMBO_COLORS = ["#FF9F1C", "#1FA971", "#2F6FED", "#9B5DE5", "#E8473B"];
565
715
  var COMBO_MS = 2500;
566
716
  var MUTE_KEY = "packing-bingo-muted";
567
- var playReady = /* @__PURE__ */ new WeakSet();
568
717
  var ORDER_PREFIX = "packing-bingo-order:";
569
718
  var esc = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
570
719
  function readStore(key) {
@@ -582,7 +731,22 @@ function writeStore(key, value) {
582
731
  }
583
732
  var glintDelay = (c, n) => `${+((Math.floor(c / n) + c % n) * 0.36 / Math.max(1, n - 1)).toFixed(3)}s`;
584
733
  var lineKey = (l) => l.slice().sort((a, b) => a - b).join(",");
734
+ var COUNT_MS = 1e3;
735
+ var GO_MS = 750;
736
+ function formatTime(ms, digits = 1) {
737
+ const unit = 10 ** digits;
738
+ const t = Math.floor(Math.max(0, ms) / (1e3 / unit));
739
+ const frac = digits ? "." + String(t % unit).padStart(digits, "0") : "";
740
+ const sec = Math.floor(t / unit), s = sec % 60, m = Math.floor(sec / 60) % 60, h = Math.floor(sec / 3600);
741
+ const pad = (v) => String(v).padStart(2, "0");
742
+ return (h ? `${h}:${pad(m)}` : String(m)) + `:${pad(s)}${frac}`;
743
+ }
744
+ function speedBonus(items, ms) {
745
+ return Math.round(items * 1e6 / Math.max(ms, 1));
746
+ }
585
747
  var FLY_MS = 560;
748
+ var CLEAR_MS = 1e3;
749
+ var SPARK = ["#FFFFFF", "#FFE066", "#FF9F1C", "#E8473B", "#9B5DE5", "#2F6FED", "#1FA971"];
586
750
  function ensureStyles() {
587
751
  if (document.getElementById("pkb-styles")) return;
588
752
  const s = document.createElement("style");
@@ -590,7 +754,8 @@ function ensureStyles() {
590
754
  s.textContent = CSS;
591
755
  document.head.appendChild(s);
592
756
  }
593
- function template(L, M, { demo, shuffle, vibrate }) {
757
+ var opt = (v, fallback) => v ?? fallback ?? "";
758
+ function template(L, M, { demo, shuffle, vibrate, ready, modes }) {
594
759
  const n = L.size;
595
760
  const input = vibrate ? `<input type="checkbox" switch role="checkbox">` : `<input type="checkbox">`;
596
761
  const tiles = Array.from({ length: n * n }, (_, c) => {
@@ -602,16 +767,15 @@ function template(L, M, { demo, shuffle, vibrate }) {
602
767
  }).join("");
603
768
  const scale = `--pk-n:${n};--pk-s:${+Math.min(1.35, 4 / n).toFixed(3)};--pk-d:${+Math.min(1, 4 / n).toFixed(3)}`;
604
769
  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>` : "";
605
- const hints = [M.soundHint ?? "", vibrate ? M.vibrationHint ?? "" : ""].filter(Boolean);
606
- const playTitle = M.playTitle?.trim() || ja.playTitle || M.title;
607
- const playDialog = hints.length ? `<dialog class="pkb-play-dialog" data-r="play" aria-label="${esc(playTitle)}">
608
- <h3>${esc(playTitle)}</h3>
609
- ${hints.map((hint) => `<p>${esc(hint)}</p>`).join("")}
610
- <div class="pkb-play-actions"><button type="button" class="pkb-play-start">${esc(M.playStart?.trim() || ja.playStart || M.close)}</button>
611
- <button type="button" class="pkb-play-skip">${esc(M.playSkip?.trim() || ja.playSkip || M.close)}</button></div></dialog>` : "";
770
+ const hints = [opt(M.soundHint, ja.soundHint), vibrate ? opt(M.vibrationHint, ja.vibrationHint) : ""].filter((h) => h.trim());
771
+ const prompt = opt(M.readyPrompt, ja.readyPrompt);
772
+ const rule = opt(M.infiniteRule, ja.infiniteRule);
773
+ 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>` : "";
774
+ const timeLabel = M.time?.trim() || ja.time;
775
+ 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>` : "";
612
776
  return `<div class="pkb" style="${scale}"><div class="pkb-board">
613
777
  <div class="pkb-head"><div>
614
- <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>
778
+ <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>
615
779
  <h2 class="pkb-title">${esc(M.title)}</h2>${M.subtitle ? `<p class="pkb-sub">${esc(M.subtitle)}</p>` : ""}</div>
616
780
  <div class="pkb-side"><div class="pkb-score">
617
781
  <div class="pkb-pill"><small>${esc(M.packed)}</small><b data-r="cnt"></b></div>
@@ -620,13 +784,14 @@ ${hints.map((hint) => `<p>${esc(hint)}</p>`).join("")}
620
784
  <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>
621
785
  <div class="pkb-gw"><div class="pkb-grid">${tiles}</div><svg class="pkb-lines" aria-hidden="true"></svg>
622
786
  <div class="pkb-banner" data-r="banner" aria-hidden="true"><b data-r="bT"></b><span data-r="bS"></span></div></div>
623
- <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>
787
+ <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>
624
788
  <div class="pkb-flash" data-r="flash"></div>
625
- <div class="pkb-perfect" data-r="perfect"><div class="pkb-card" role="dialog" aria-label="${esc(M.perfectTitle)}">
626
- <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>
789
+ <div class="pkb-perfect" data-r="perfect"><div class="pkb-card" data-r="pcard" role="dialog" aria-label="${esc(M.perfectTitle)}">
790
+ <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>
791
+ <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>
627
792
  <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>
628
- <button type="button" class="pkb-close" data-r="close">${esc(M.close)}</button></div></div>
629
- <canvas class="pkb-fx"></canvas></div>${demoBar}${playDialog}</div>`;
793
+ <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>
794
+ <canvas class="pkb-fx"></canvas>${readyScreen}<div class="pkb-mfx" data-r="mfx" aria-hidden="true"></div></div>${demoBar}</div>`;
630
795
  }
631
796
  var perfectText = (M, total) => typeof M.perfectBody === "function" ? M.perfectBody(total) : M.perfectBody;
632
797
  function createPackingBingo(container, options = {}) {
@@ -658,8 +823,15 @@ function createPackingBingo(container, options = {}) {
658
823
  const orderKey = storageKey && ORDER_PREFIX + storageKey;
659
824
  const demo = !!options.demo;
660
825
  const vibrate = options.vibrate !== false;
826
+ const uncheck = options.uncheck === true;
661
827
  const calm = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
662
828
  const haptics = createHaptics(vibrate);
829
+ let mode = options.mode === "infinite" ? "infinite" : "normal";
830
+ const inf = () => mode === "infinite";
831
+ let packing = null;
832
+ let cleared = 0;
833
+ const pending = /* @__PURE__ */ new Set();
834
+ let clearTimer = 0, clearAt = 0;
663
835
  let dead = false;
664
836
  const timers = /* @__PURE__ */ new Set();
665
837
  const later = (fn, ms) => {
@@ -672,12 +844,19 @@ function createPackingBingo(container, options = {}) {
672
844
  const wait = (ms) => new Promise((r) => window.setTimeout(r, ms));
673
845
  let gen = 0;
674
846
  ensureStyles();
675
- container.innerHTML = template(L, M, { demo, shuffle: options.shuffle !== false, vibrate });
847
+ container.innerHTML = template(L, M, {
848
+ demo,
849
+ shuffle: options.shuffle !== false,
850
+ vibrate,
851
+ ready: options.ready !== false,
852
+ modes: options.modeSwitch !== false
853
+ });
676
854
  const $ = (r) => {
677
855
  const el = container.querySelector(`[data-r="${r}"]`);
678
856
  if (!el) throw new Error(`[packing-bingo] internal: missing element "${r}"`);
679
857
  return el;
680
858
  };
859
+ const root = container.querySelector(".pkb");
681
860
  const board = container.querySelector(".pkb-board");
682
861
  const gw = container.querySelector(".pkb-gw");
683
862
  const grid = container.querySelector(".pkb-grid");
@@ -689,21 +868,7 @@ function createPackingBingo(container, options = {}) {
689
868
  const freeTile = L.center >= 0 ? cellTiles[L.center] : null;
690
869
  const ins = tiles.map((t) => t.querySelector("input"));
691
870
  const stamps = tiles.map((t) => t.querySelector(".pkb-stamp"));
692
- const kick = $("kick"), snd = $("snd"), perfectEl = $("perfect");
693
- const playDialog = container.querySelector('[data-r="play"]');
694
- const beforePlay = (event) => {
695
- if (!playDialog || playReady.has(container.ownerDocument) || typeof playDialog.showModal !== "function" || !(event.target instanceof Element) || !event.target.closest(".pkb-tile:not(.pkb-free)")) return;
696
- event.preventDefault();
697
- event.stopImmediatePropagation();
698
- playDialog.showModal();
699
- };
700
- grid.addEventListener("click", beforePlay, true);
701
- playDialog?.querySelectorAll("button").forEach((button) => {
702
- button.addEventListener("click", () => playDialog.close());
703
- });
704
- playDialog?.addEventListener("close", () => {
705
- if (!dead) playReady.add(container.ownerDocument);
706
- });
871
+ const kick = $("kick"), snd = $("snd"), perfectEl = $("perfect"), mfx = $("mfx");
707
872
  let checked = normalize(void 0);
708
873
  let cur = [];
709
874
  let prevLines = /* @__PURE__ */ new Set();
@@ -739,7 +904,7 @@ function createPackingBingo(container, options = {}) {
739
904
  cv.width = Math.round(BW * dpr);
740
905
  cv.height = Math.round(BH * dpr);
741
906
  };
742
- const piece = (x, y, vx, vy, g, s, life, shape2) => ({
907
+ const piece = (x, y, vx, vy, g, s, life, shape2, colors = CONFETTI) => ({
743
908
  x,
744
909
  y,
745
910
  vx,
@@ -751,7 +916,7 @@ function createPackingBingo(container, options = {}) {
751
916
  h: (3 + Math.random() * 5) * s,
752
917
  r: Math.random() * 6,
753
918
  vr: (Math.random() - 0.5) * 0.35,
754
- c: CONFETTI[Math.random() * CONFETTI.length | 0]
919
+ c: colors[Math.random() * colors.length | 0]
755
920
  });
756
921
  const go = () => {
757
922
  if (!raf && ctx) raf = requestAnimationFrame(tick);
@@ -762,7 +927,7 @@ function createPackingBingo(container, options = {}) {
762
927
  const a = (o.angle ?? -Math.PI / 2) + (Math.random() - 0.5) * (o.spread ?? Math.PI * 2);
763
928
  const v = (o.speed ?? 10) * (0.35 + Math.random() * 0.85);
764
929
  const sh = Math.random() < (o.star ?? 0.15) ? "star" : Math.random() < 0.3 ? "round" : "rect";
765
- 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));
930
+ 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));
766
931
  }
767
932
  go();
768
933
  }
@@ -860,6 +1025,7 @@ function createPackingBingo(container, options = {}) {
860
1025
  if (e.animationName === "pkb-stamp-hit") t.classList.remove("pkb-hit");
861
1026
  if (t.classList.contains("pkb-tile") && (e.animationName === "pkb-hop" || e.animationName === "pkb-squash"))
862
1027
  t.classList.remove(e.animationName);
1028
+ if (t.classList.contains("pkb-tile") && e.animationName === "pkb-clr") t.classList.remove("pkb-clr");
863
1029
  });
864
1030
  const ring = (t) => {
865
1031
  const r = document.createElement("span");
@@ -878,20 +1044,20 @@ function createPackingBingo(container, options = {}) {
878
1044
  board.appendChild(el);
879
1045
  el.addEventListener("animationend", () => el.remove());
880
1046
  };
881
- const prog = (t, t0, d) => Math.max(0, Math.min(1, (t - t0) / d));
1047
+ const prog = (t, t02, d) => Math.max(0, Math.min(1, (t - t02) / d));
882
1048
  const ease = (k) => 1 - Math.pow(1 - k, 3);
883
1049
  let tw = 0;
884
1050
  function countScore() {
885
1051
  cancelAnimationFrame(tw);
886
- const a = shown, b = score, t0 = performance.now();
1052
+ const a = shown, b = score, t02 = performance.now();
887
1053
  const st = (t) => {
888
1054
  if (dead) return;
889
- const k = prog(t, t0, 450);
1055
+ const k = prog(t, t02, 450);
890
1056
  shown = Math.round(a + (b - a) * ease(k));
891
1057
  $("sc").textContent = shown.toLocaleString();
892
1058
  if (k < 1) tw = requestAnimationFrame(st);
893
1059
  };
894
- st(t0);
1060
+ st(t02);
895
1061
  if (!calm) replay($("sp"), "pkb-bump");
896
1062
  }
897
1063
  const showGain = (pts, x, y, cls = "pt") => {
@@ -899,12 +1065,12 @@ function createPackingBingo(container, options = {}) {
899
1065
  countScore();
900
1066
  };
901
1067
  function countUp(el, a, b, d, fmt, ticks = false) {
902
- const t0 = performance.now();
1068
+ const t02 = performance.now();
903
1069
  const g = gen;
904
1070
  let lt = 0;
905
1071
  const st = (t) => {
906
1072
  if (dead || g !== gen) return;
907
- const k = prog(t, t0, d);
1073
+ const k = prog(t, t02, d);
908
1074
  el.textContent = fmt(Math.round(a + (b - a) * ease(k)));
909
1075
  if (ticks && k < 1 && t - lt > 55) {
910
1076
  sfx.tick();
@@ -913,7 +1079,7 @@ function createPackingBingo(container, options = {}) {
913
1079
  if (k < 1) requestAnimationFrame(st);
914
1080
  else if (!calm) replay(el, "pkb-bump");
915
1081
  };
916
- st(t0);
1082
+ st(t02);
917
1083
  }
918
1084
  const ctr = (t) => {
919
1085
  return { x: t.offsetLeft + t.offsetWidth / 2, y: t.offsetTop + t.offsetHeight / 2 };
@@ -932,6 +1098,120 @@ function createPackingBingo(container, options = {}) {
932
1098
  return `<g${fresh.includes(k) ? ' class="pkb-draw"' : ""}><line class="pkb-u" ${at}/><line class="pkb-o" ${at}/></g>`;
933
1099
  }).join("");
934
1100
  }
1101
+ const readyEl = container.querySelector('[data-r="ready"]');
1102
+ const watch = $("watch");
1103
+ let phase = "idle";
1104
+ let t0 = 0, watchTimer = 0;
1105
+ let finalMs = null;
1106
+ const cdTimers = /* @__PURE__ */ new Set();
1107
+ const cd = (fn, ms) => {
1108
+ const id = window.setTimeout(() => {
1109
+ cdTimers.delete(id);
1110
+ if (!dead) fn();
1111
+ }, ms);
1112
+ cdTimers.add(id);
1113
+ };
1114
+ const stopTimers = () => {
1115
+ cdTimers.forEach((id) => clearTimeout(id));
1116
+ cdTimers.clear();
1117
+ clearInterval(watchTimer);
1118
+ };
1119
+ const elapsed = () => phase === "play" ? performance.now() - t0 : finalMs ?? 0;
1120
+ const showWatch = () => {
1121
+ watch.textContent = formatTime(elapsed());
1122
+ };
1123
+ function startWatch() {
1124
+ phase = "play";
1125
+ t0 = performance.now();
1126
+ finalMs = null;
1127
+ clearInterval(watchTimer);
1128
+ watchTimer = window.setInterval(showWatch, 100);
1129
+ watch.classList.remove("pkb-done");
1130
+ watch.classList.add("pkb-run");
1131
+ showWatch();
1132
+ syncFin();
1133
+ }
1134
+ const finBtn = $("fin");
1135
+ const shufBtn = container.querySelector('[data-r="shuf"]');
1136
+ const syncFin = () => {
1137
+ finBtn.disabled = !(inf() && phase === "play");
1138
+ if (shufBtn) shufBtn.disabled = inf() && phase === "done";
1139
+ };
1140
+ function finish() {
1141
+ if (phase === "play") {
1142
+ finalMs = performance.now() - t0;
1143
+ watch.classList.replace("pkb-run", "pkb-done");
1144
+ } else if (phase !== "done") {
1145
+ stopTimers();
1146
+ openGrid(false);
1147
+ }
1148
+ phase = "done";
1149
+ clearInterval(watchTimer);
1150
+ showWatch();
1151
+ }
1152
+ function openGrid(fade) {
1153
+ grid.removeAttribute("inert");
1154
+ if (!readyEl || readyEl.hidden) return;
1155
+ const focused = document.activeElement === readyEl;
1156
+ readyEl.classList.add("pkb-gone");
1157
+ board.classList.remove("pkb-waiting");
1158
+ readyEl.tabIndex = -1;
1159
+ if (focused) grid.querySelector("input")?.focus({ preventScroll: true });
1160
+ if (fade) cd(() => readyEl.hidden = true, GO_MS);
1161
+ else readyEl.hidden = true;
1162
+ }
1163
+ function toReady() {
1164
+ stopTimers();
1165
+ finalMs = null;
1166
+ watch.classList.remove("pkb-run", "pkb-done");
1167
+ phase = readyEl ? "ready" : "idle";
1168
+ showWatch();
1169
+ syncFin();
1170
+ board.classList.remove("pkb-over");
1171
+ grid.removeAttribute("inert");
1172
+ if (!readyEl) return;
1173
+ readyEl.hidden = false;
1174
+ readyEl.removeAttribute("tabindex");
1175
+ readyEl.classList.remove("pkb-gone", "pkb-counting");
1176
+ board.classList.add("pkb-waiting");
1177
+ $("count").textContent = "";
1178
+ grid.setAttribute("inert", "");
1179
+ }
1180
+ let resetAsked = false;
1181
+ const restartGame = () => {
1182
+ if (!controlled || !checked.some(Boolean)) toReady();
1183
+ else resetAsked = true;
1184
+ };
1185
+ function skipReady() {
1186
+ if (phase === "play" || phase === "done") return;
1187
+ stopTimers();
1188
+ openGrid(false);
1189
+ startWatch();
1190
+ }
1191
+ function countdown() {
1192
+ if (phase !== "ready") return;
1193
+ phase = "count";
1194
+ readyEl.classList.add("pkb-counting");
1195
+ const count = $("count");
1196
+ const words = ["3", "2", "1", M.readyGo?.trim() || ja.readyGo];
1197
+ words.forEach((word, k) => {
1198
+ const go2 = k === words.length - 1;
1199
+ const step = () => {
1200
+ count.textContent = word;
1201
+ count.classList.toggle("pkb-go", go2);
1202
+ replay(count, "pkb-tick");
1203
+ sfx.count(go2);
1204
+ haptics.play(go2 ? "go" : "count");
1205
+ if (go2) {
1206
+ startWatch();
1207
+ openGrid(true);
1208
+ }
1209
+ };
1210
+ if (k) cd(step, k * COUNT_MS);
1211
+ else step();
1212
+ });
1213
+ }
1214
+ readyEl?.addEventListener("click", countdown);
935
1215
  function tapFx(i, on, moment, fresh) {
936
1216
  const p = rel(tiles[i]);
937
1217
  if (!on) {
@@ -983,7 +1263,7 @@ function createPackingBingo(container, options = {}) {
983
1263
  });
984
1264
  later(() => sfx.reach(), 150);
985
1265
  }
986
- function bingo(fresh, pts) {
1266
+ function bingo(fresh, pts, total) {
987
1267
  const nl = fresh.length;
988
1268
  const seq = [...new Set(fresh.flatMap((k) => cellLines[k]))].map((c) => cellTiles[c]);
989
1269
  const step = Math.min(70, 300 / seq.length);
@@ -997,7 +1277,7 @@ function createPackingBingo(container, options = {}) {
997
1277
  );
998
1278
  later(() => {
999
1279
  $("bT").textContent = nl >= 3 ? M.bannerTriple : nl === 2 ? M.bannerDouble : M.bannerSingle;
1000
- $("bS").textContent = nl > 1 ? M.bannerMulti(nl) : M.bannerNth(cur.length);
1280
+ $("bS").textContent = nl > 1 ? M.bannerMulti(nl) : M.bannerNth(total);
1001
1281
  replay($("banner"), "pkb-show");
1002
1282
  if (!calm) {
1003
1283
  replay($("flash"), "pkb-go");
@@ -1014,12 +1294,16 @@ function createPackingBingo(container, options = {}) {
1014
1294
  perfectPending = true;
1015
1295
  perfectEl.classList.remove("pkb-settled");
1016
1296
  const bonus = gotP ? 0 : 1e3;
1297
+ const speed = gotP || finalMs == null ? 0 : speedBonus(N, finalMs);
1017
1298
  gotP = true;
1018
- score += bonus;
1299
+ score += bonus + speed;
1019
1300
  cellTiles.forEach((t, c) => hop(t, far(c) * 180 / Math.max(1, n - 1)));
1020
1301
  sfx.perfect();
1021
1302
  if (!calm) replay($("flash"), "pkb-go");
1303
+ setResult(false);
1022
1304
  $("pbody").textContent = perfectText(M, N);
1305
+ $("ptime").hidden = finalMs == null;
1306
+ $("pT").textContent = formatTime(finalMs ?? 0, 2);
1023
1307
  $("sScore").textContent = "0";
1024
1308
  $("sCombo").textContent = "\xD70";
1025
1309
  $("sBingo").textContent = "0";
@@ -1035,9 +1319,11 @@ function createPackingBingo(container, options = {}) {
1035
1319
  burst(BW, BH, 90, { angle: -Math.PI * 2 / 3, spread: 0.8, speed: 22, g: 0.32, star: 0.3 });
1036
1320
  rain(3500);
1037
1321
  if (bonus) floatText(M.perfectBonus(bonus), BW / 2, BH * 0.16, "big");
1322
+ const gap = Math.min(34, Math.max(14, BW * 0.056)) * 1.3 + 6;
1323
+ if (speed) floatText((M.speedBonus ?? ja.speedBonus)(speed), BW / 2, Math.max(gap / 2 + 4, BH * 0.16 - gap), "big");
1038
1324
  countUp($("sScore"), 0, score, 1300, (v) => v.toLocaleString(), true);
1039
1325
  countUp($("sCombo"), 0, maxCombo, 700, (v) => "\xD7" + v);
1040
- countUp($("sBingo"), 0, cur.length, 700, String);
1326
+ countUp($("sBingo"), 0, resultLines(), 700, String);
1041
1327
  countScore();
1042
1328
  }, PERFECT_STAMP_DELAY_MS);
1043
1329
  later(() => emojiRain(3e3), 1500);
@@ -1048,10 +1334,67 @@ function createPackingBingo(container, options = {}) {
1048
1334
  beat?.(pulse);
1049
1335
  }, at)
1050
1336
  );
1051
- const e = { score, maxCombo };
1337
+ const e = { score, maxCombo, time: finalMs == null ? null : Math.round(finalMs), speedBonus: speed };
1052
1338
  events.push(() => options.onPerfect?.(e));
1053
1339
  }
1340
+ function clearNow(animate) {
1341
+ clearTimeout(clearTimer);
1342
+ clearTimer = 0;
1343
+ if (!pending.size) return;
1344
+ const gone = [...pending].filter((i) => checked[i]);
1345
+ pending.clear();
1346
+ checked = checked.slice();
1347
+ gone.forEach((i) => {
1348
+ checked[i] = false;
1349
+ ins[i].checked = false;
1350
+ got.delete(i);
1351
+ });
1352
+ gotL.clear();
1353
+ const left = completeLines();
1354
+ left.forEach((k) => gotL.add(lineKey(lines[k])));
1355
+ prevLines = new Set(left);
1356
+ render(null, true);
1357
+ if (!animate || !gone.length) return;
1358
+ sfx.clear(gone.length);
1359
+ gone.forEach((i, j) => {
1360
+ const t = tiles[i];
1361
+ if (!calm) {
1362
+ t.style.setProperty("--pkb-cd", Math.min(j * 28, 280) + "ms");
1363
+ t.classList.remove("pkb-squash", "pkb-hop");
1364
+ replay(t, "pkb-clr");
1365
+ }
1366
+ const p = rel(t);
1367
+ burst(p.x, p.y, 9, { speed: 4.5, g: 0.06, life: 45, size: 0.75, star: 1, colors: SPARK.slice(1) });
1368
+ });
1369
+ }
1370
+ function scheduleClear(ms) {
1371
+ const at = performance.now() + ms;
1372
+ if (clearTimer && at <= clearAt) return;
1373
+ clearTimeout(clearTimer);
1374
+ clearAt = at;
1375
+ clearTimer = window.setTimeout(() => {
1376
+ clearTimer = 0;
1377
+ if (!dead) clearNow(true);
1378
+ }, ms);
1379
+ }
1380
+ const dropClear = () => {
1381
+ clearTimeout(clearTimer);
1382
+ clearTimer = 0;
1383
+ pending.clear();
1384
+ };
1385
+ function setResult(fin) {
1386
+ const title = fin ? M.finishTitle?.trim() || ja.finishTitle : M.perfectTitle;
1387
+ perfectEl.classList.toggle("pkb-fin", fin);
1388
+ $("pcard").setAttribute("aria-label", title);
1389
+ $("medal").textContent = fin ? M.finishStamp?.trim() || ja.finishStamp : M.perfectStamp;
1390
+ $("ptitle").textContent = title;
1391
+ $("pTl").textContent = fin ? M.finishTime?.trim() || ja.finishTime : M.statTime?.trim() || ja.statTime;
1392
+ $("again").hidden = !fin;
1393
+ }
1394
+ const resultLines = () => inf() ? cleared : cur.length;
1395
+ const resultFocus = () => $(perfectEl.classList.contains("pkb-fin") ? "again" : "close");
1054
1396
  function cancelFx() {
1397
+ if (pending.size) clearNow(false);
1055
1398
  gen++;
1056
1399
  perfectPending = false;
1057
1400
  timers.forEach((id) => clearTimeout(id));
@@ -1072,21 +1415,32 @@ function createPackingBingo(container, options = {}) {
1072
1415
  $("flash").classList.remove("pkb-go");
1073
1416
  $("combo").classList.remove("pkb-on");
1074
1417
  $("cbar").classList.remove("pkb-draining");
1075
- cellTiles.forEach((t) => t.classList.remove("pkb-squash", "pkb-hop"));
1418
+ cellTiles.forEach((t) => t.classList.remove("pkb-squash", "pkb-hop", "pkb-clr"));
1076
1419
  stamps.forEach((s) => s.classList.remove("pkb-hit"));
1077
- board.classList.remove("pkb-punch", "pkb-shake");
1420
+ board.classList.remove("pkb-punch", "pkb-shake", "pkb-switching");
1421
+ mfx.classList.remove("pkb-show");
1078
1422
  if (!keepPerfect) perfectEl.classList.remove("pkb-show", "pkb-settled");
1079
1423
  }
1080
1424
  const completeLines = () => lines.flatMap((l, k) => l.every((i) => checked[i]) ? [k] : []);
1081
1425
  function render(src, quiet) {
1082
1426
  const events = [];
1083
1427
  const on = checked, c = on.filter(Boolean).length;
1428
+ const endless = inf();
1429
+ const all = c === N && !endless;
1084
1430
  cur = completeLines();
1085
1431
  const fresh = cur.filter((k) => !prevLines.has(k));
1086
1432
  if ([...prevLines].some((k) => !cur.includes(k))) {
1087
1433
  cancelFx();
1088
1434
  if (c < N) perfectEl.classList.remove("pkb-show");
1089
1435
  }
1436
+ if (endless) {
1437
+ if (!quiet) cleared += fresh.length;
1438
+ if (cur.length) {
1439
+ const known = pending.size;
1440
+ cur.forEach((k) => lines[k].forEach((i) => pending.add(i)));
1441
+ if (pending.size > known || !clearTimer) scheduleClear(!quiet && fresh.length ? CLEAR_MS : FLY_MS + 200);
1442
+ }
1443
+ }
1090
1444
  const hit = new Set(cur.flatMap((k) => cellLines[k]));
1091
1445
  const reach = /* @__PURE__ */ new Set();
1092
1446
  for (const l of lines) {
@@ -1095,26 +1449,31 @@ function createPackingBingo(container, options = {}) {
1095
1449
  }
1096
1450
  tiles.forEach((t, i) => {
1097
1451
  t.classList.toggle("pkb-on", on[i]);
1452
+ const lock = on[i] && (!uncheck || pending.has(i));
1453
+ t.classList.toggle("pkb-lock", lock);
1454
+ if (lock) ins[i].setAttribute("aria-readonly", "true");
1455
+ else ins[i].removeAttribute("aria-readonly");
1098
1456
  t.classList.toggle("pkb-line", hit.has(L.itemCell[slotOf[i]]));
1099
1457
  t.classList.toggle("pkb-reach", reach.has(i));
1100
1458
  });
1101
1459
  freeTile?.classList.toggle("pkb-line", hit.has(L.center));
1102
- board.classList.toggle("pkb-all", c === N);
1103
- kick.classList.toggle("pkb-gold", c === N);
1104
- kick.textContent = c === N ? M.perfectTag : M.tag;
1460
+ board.classList.toggle("pkb-all", all);
1461
+ kick.classList.toggle("pkb-gold", all);
1462
+ kick.textContent = all ? M.perfectTag : M.tag;
1105
1463
  $("cnt").textContent = `${c}/${N}`;
1106
- $("bng").textContent = String(cur.length);
1464
+ const total = endless ? cleared : cur.length;
1465
+ $("bng").textContent = String(total);
1107
1466
  const bp = $("bp");
1108
- if (cur.length > lastN) {
1467
+ if (total > lastN) {
1109
1468
  bp.classList.remove("pkb-hot");
1110
1469
  void bp.offsetWidth;
1111
1470
  }
1112
- bp.classList.toggle("pkb-hot", cur.length > 0);
1113
- lastN = cur.length;
1471
+ bp.classList.toggle("pkb-hot", total > 0);
1472
+ lastN = total;
1114
1473
  drawLines(fresh);
1115
1474
  if (src !== null) {
1116
1475
  const newReach = [...reach].some((i) => !prevReach.has(i));
1117
- const moment = !quiet && c === N && !prevAll ? "perfect" : !quiet && fresh.length ? "bingo" : !quiet && newReach ? "reach" : "tap";
1476
+ const moment = !quiet && all && !prevAll ? "perfect" : !quiet && fresh.length ? "bingo" : !quiet && newReach ? "reach" : "tap";
1118
1477
  tapFx(src, on[src], moment, fresh);
1119
1478
  }
1120
1479
  const newL = fresh.map((k) => lineKey(lines[k])).filter((key) => !gotL.has(key));
@@ -1123,10 +1482,10 @@ function createPackingBingo(container, options = {}) {
1123
1482
  score += linePts;
1124
1483
  if (quiet) {
1125
1484
  if (linePts) countScore();
1126
- } else if (c === N && !prevAll) perfect(events);
1485
+ } else if (all && !prevAll) perfect(events);
1127
1486
  else if (fresh.length) {
1128
- bingo(fresh, linePts);
1129
- const e = { newLines: fresh, totalLines: cur.length, score };
1487
+ bingo(fresh, linePts, total);
1488
+ const e = { newLines: fresh, totalLines: total, score, mode };
1130
1489
  events.push(() => options.onBingo?.(e));
1131
1490
  } else if (src !== null && on[src]) {
1132
1491
  const nr = [...reach].filter((i) => !prevReach.has(i));
@@ -1134,9 +1493,9 @@ function createPackingBingo(container, options = {}) {
1134
1493
  }
1135
1494
  prevLines = new Set(cur);
1136
1495
  prevReach = reach;
1137
- prevAll = c === N;
1496
+ prevAll = all;
1138
1497
  const labels = [...reach].map((i) => items[i].label);
1139
- $("msg").textContent = c === N ? M.statusDone : labels.length ? M.statusReach(labels) : cur.length ? M.statusLines(cur.length, N - c) : M.statusStart(N - c);
1498
+ $("msg").textContent = all ? M.statusDone : labels.length ? M.statusReach(labels) : cur.length ? M.statusLines(cur.length, N - c) : M.statusStart(N - c);
1140
1499
  return events;
1141
1500
  }
1142
1501
  const flush = (events) => {
@@ -1146,9 +1505,11 @@ function createPackingBingo(container, options = {}) {
1146
1505
  }
1147
1506
  };
1148
1507
  const save = () => {
1149
- if (storageKey) writeStore(storageKey, JSON.stringify(checked));
1508
+ if (storageKey && !inf()) writeStore(storageKey, JSON.stringify(checked));
1150
1509
  };
1151
1510
  function apply(next, src, quiet) {
1511
+ if (phase === "idle" && src !== null && next[src]) startWatch();
1512
+ if (!inf() && next.every(Boolean)) finish();
1152
1513
  checked = next;
1153
1514
  ins.forEach((x, i) => x.checked = checked[i]);
1154
1515
  const events = render(src, quiet);
@@ -1156,8 +1517,12 @@ function createPackingBingo(container, options = {}) {
1156
1517
  return events;
1157
1518
  }
1158
1519
  function baseline(next) {
1520
+ dropClear();
1521
+ cleared = 0;
1159
1522
  cancelFx();
1160
1523
  clearVisuals();
1524
+ if (!inf() && next.every(Boolean)) finish();
1525
+ else if ((phase === "play" || phase === "done") && !next.some(Boolean)) toReady();
1161
1526
  checked = next;
1162
1527
  ins.forEach((x, i) => x.checked = checked[i]);
1163
1528
  got.clear();
@@ -1167,7 +1532,7 @@ function createPackingBingo(container, options = {}) {
1167
1532
  done.forEach((k) => gotL.add(lineKey(lines[k])));
1168
1533
  prevLines = new Set(done);
1169
1534
  prevReach = /* @__PURE__ */ new Set();
1170
- prevAll = checked.every(Boolean);
1535
+ prevAll = !inf() && checked.every(Boolean);
1171
1536
  gotP = prevAll;
1172
1537
  score = shown = got.size * 10 + gotL.size * 100;
1173
1538
  combo = 0;
@@ -1177,7 +1542,9 @@ function createPackingBingo(container, options = {}) {
1177
1542
  save();
1178
1543
  }
1179
1544
  function request(next, src, opts = {}) {
1180
- if (controlled) {
1545
+ const game = inf();
1546
+ if (controlled && !game) {
1547
+ resetAsked = !!opts.reset && resetAsked;
1181
1548
  ins.forEach((x, i) => x.checked = checked[i]);
1182
1549
  options.onChange?.(next.slice());
1183
1550
  return;
@@ -1185,7 +1552,7 @@ function createPackingBingo(container, options = {}) {
1185
1552
  let events = [];
1186
1553
  if (opts.reset) baseline(next);
1187
1554
  else events = apply(next, src, !!opts.quiet);
1188
- options.onChange?.(checked.slice());
1555
+ if (!game) options.onChange?.(checked.slice());
1189
1556
  flush(events);
1190
1557
  }
1191
1558
  function place(next) {
@@ -1204,12 +1571,12 @@ function createPackingBingo(container, options = {}) {
1204
1571
  focused.focus({ preventScroll: true });
1205
1572
  }
1206
1573
  const saveOrder = () => {
1207
- if (!orderKey) return;
1574
+ if (!orderKey || inf()) return;
1208
1575
  writeStore(orderKey, JSON.stringify(order));
1209
1576
  };
1210
1577
  let flights = [];
1211
1578
  function arrange(next) {
1212
- if (!checked.every(Boolean)) cancelFx();
1579
+ if (inf() || !checked.every(Boolean)) cancelFx();
1213
1580
  const from = calm ? null : tiles.map((t) => t.getBoundingClientRect());
1214
1581
  flights.forEach((a) => a.cancel());
1215
1582
  flights = [];
@@ -1245,6 +1612,7 @@ function createPackingBingo(container, options = {}) {
1245
1612
  svg.innerHTML = "";
1246
1613
  }
1247
1614
  function shuffleCard() {
1615
+ if (inf() && phase === "done") return;
1248
1616
  let next = order.slice();
1249
1617
  do {
1250
1618
  for (let s = N - 1; s > 0; s--) {
@@ -1259,26 +1627,93 @@ function createPackingBingo(container, options = {}) {
1259
1627
  const unshuffle = () => {
1260
1628
  if (!isIdentity(order)) arrange(identity());
1261
1629
  };
1630
+ const locked = (i) => checked[i] && (!uncheck || pending.has(i));
1631
+ grid.addEventListener("click", (e) => {
1632
+ const i = ins.indexOf(e.target);
1633
+ if (i >= 0 && locked(i)) e.preventDefault();
1634
+ });
1262
1635
  grid.addEventListener("change", (e) => {
1263
1636
  const i = ins.indexOf(e.target);
1264
1637
  if (i < 0) return;
1638
+ if (locked(i)) {
1639
+ ins[i].checked = true;
1640
+ return;
1641
+ }
1265
1642
  const next = checked.slice();
1266
1643
  next[i] = !next[i];
1267
1644
  request(next, i);
1268
1645
  });
1269
1646
  container.querySelector('[data-r="shuf"]')?.addEventListener("click", shuffleCard);
1647
+ const restartInfinite = () => {
1648
+ baseline(normalize(void 0));
1649
+ toReady();
1650
+ };
1270
1651
  $("reset").addEventListener("click", () => {
1652
+ if (inf()) return restartInfinite();
1271
1653
  unshuffle();
1654
+ restartGame();
1272
1655
  request(normalize(void 0), null, { reset: true });
1273
1656
  });
1274
1657
  $("close").addEventListener("click", () => perfectEl.classList.remove("pkb-show"));
1658
+ $("again").addEventListener("click", () => {
1659
+ if (!inf()) return;
1660
+ restartInfinite();
1661
+ (readyEl && !readyEl.hidden ? readyEl : grid.querySelector("input"))?.focus({ preventScroll: true });
1662
+ });
1663
+ function finishGame() {
1664
+ if (dead || !inf() || phase !== "play") return;
1665
+ cancelFx();
1666
+ clearVisuals();
1667
+ finalMs = performance.now() - t0;
1668
+ phase = "done";
1669
+ clearInterval(watchTimer);
1670
+ watch.classList.replace("pkb-run", "pkb-done");
1671
+ showWatch();
1672
+ syncFin();
1673
+ combo = 0;
1674
+ grid.setAttribute("inert", "");
1675
+ board.classList.add("pkb-over");
1676
+ setResult(true);
1677
+ $("pbody").textContent = "";
1678
+ $("ptime").hidden = false;
1679
+ $("pT").textContent = formatTime(finalMs, 2);
1680
+ $("sScore").textContent = "0";
1681
+ $("sCombo").textContent = "\xD70";
1682
+ $("sBingo").textContent = "0";
1683
+ perfectEl.classList.remove("pkb-settled");
1684
+ replay(perfectEl, "pkb-show");
1685
+ resultFocus().focus({ preventScroll: true });
1686
+ sfx.perfect();
1687
+ const beat = haptics.play("finish");
1688
+ if (!calm) replay($("flash"), "pkb-go");
1689
+ later(() => {
1690
+ if (!calm) replay(board, "pkb-shake");
1691
+ sfx.boom();
1692
+ beat?.(PERFECT_STAMP_BEAT);
1693
+ burst(0, BH, 80, { angle: -Math.PI / 3, spread: 0.8, speed: 21, g: 0.32, star: 0.5, colors: SPARK });
1694
+ burst(BW, BH, 80, { angle: -Math.PI * 2 / 3, spread: 0.8, speed: 21, g: 0.32, star: 0.5, colors: SPARK });
1695
+ rain(2500);
1696
+ countUp($("sScore"), 0, score, 1300, (v) => v.toLocaleString(), true);
1697
+ countUp($("sCombo"), 0, maxCombo, 700, (v) => "\xD7" + v);
1698
+ countUp($("sBingo"), 0, cleared, 700, String);
1699
+ }, PERFECT_STAMP_DELAY_MS);
1700
+ [1250, 1700, 2200].forEach(
1701
+ (at, k) => later(() => {
1702
+ firework(BW * (0.15 + Math.random() * 0.7), BH * (0.12 + Math.random() * 0.3));
1703
+ beat?.(PERFECT_FIREWORKS[k * 2].beat);
1704
+ }, at)
1705
+ );
1706
+ const e = { score, maxCombo, lines: cleared, time: Math.round(finalMs) };
1707
+ options.onFinish?.(e);
1708
+ }
1709
+ finBtn.addEventListener("click", finishGame);
1275
1710
  perfectEl.addEventListener("click", (e) => {
1276
1711
  if (e.target === perfectEl) perfectEl.classList.remove("pkb-show");
1277
1712
  });
1278
1713
  if (demo) {
1279
1714
  let busy = false;
1280
1715
  const prepare = () => {
1281
- if (checked.every(Boolean)) request(normalize(void 0), null, { reset: true });
1716
+ if (!inf() && checked.every(Boolean)) request(normalize(void 0), null, { reset: true });
1282
1717
  else perfectEl.classList.remove("pkb-show");
1283
1718
  };
1284
1719
  const run = async (steps) => {
@@ -1286,6 +1721,7 @@ function createPackingBingo(container, options = {}) {
1286
1721
  busy = true;
1287
1722
  try {
1288
1723
  prepare();
1724
+ skipReady();
1289
1725
  const g = gen;
1290
1726
  for (const s of steps) {
1291
1727
  await wait(s.delay);
@@ -1299,21 +1735,100 @@ function createPackingBingo(container, options = {}) {
1299
1735
  }
1300
1736
  };
1301
1737
  $("dB").addEventListener("click", () => {
1738
+ if (inf() && phase === "done") return;
1302
1739
  const open = () => lines.map((l) => l.filter((i) => !checked[i])).filter((m) => m.length).sort((a, b) => a.length - b.length)[0] ?? [];
1303
- const pick = checked.every(Boolean) ? [...lines[0]] : open();
1740
+ const pick = !inf() && checked.every(Boolean) ? [...lines[0]] : open();
1304
1741
  void run(pick.map((i) => ({ i, delay: 320 })));
1305
1742
  });
1306
1743
  $("dP").addEventListener("click", () => {
1744
+ if (inf()) return;
1307
1745
  const all = items.map((_, i) => i);
1308
1746
  const miss = checked.every(Boolean) ? all : all.filter((i) => !checked[i]);
1309
1747
  void run(miss.map((i, k) => k < miss.length - 1 ? { i, delay: 170, quiet: true } : { i, delay: 450 }));
1310
1748
  });
1311
1749
  $("dR").addEventListener("click", () => {
1312
- if (busy) return;
1750
+ if (busy || inf()) return;
1313
1751
  unshuffle();
1752
+ restartGame();
1314
1753
  request(demoStart(), null, { reset: true });
1315
1754
  });
1316
1755
  }
1756
+ const modeEl = container.querySelector('[data-r="mode"]');
1757
+ const radios = modeEl ? Array.from(modeEl.querySelectorAll("button[data-m]")) : [];
1758
+ const syncMode = () => {
1759
+ root.classList.toggle("pkb-inf", inf());
1760
+ radios.forEach((b) => {
1761
+ const on = b.dataset.m === mode;
1762
+ b.setAttribute("aria-checked", String(on));
1763
+ b.tabIndex = on ? 0 : -1;
1764
+ });
1765
+ };
1766
+ function modeFx() {
1767
+ const endless = inf();
1768
+ const title = endless ? M.infiniteTitle?.trim() || ja.infiniteTitle : M.normalTitle?.trim() || ja.normalTitle;
1769
+ const sub = endless ? M.infiniteSub?.trim() || ja.infiniteSub : M.normalSub?.trim() || ja.normalSub;
1770
+ const sparks = endless && !calm ? Array.from({ length: 18 }, (_, k) => {
1771
+ const x = (5 + Math.random() * 90).toFixed(1), y = (6 + Math.random() * 88).toFixed(1);
1772
+ const s = (0.45 + Math.random() * 0.95).toFixed(2), t = Math.round(100 + Math.random() * 950);
1773
+ return `<i style="--x:${x}%;--y:${y}%;--s:${s};--t:${t}ms;--c:${SPARK[k % SPARK.length]}"></i>`;
1774
+ }).join("") : "";
1775
+ mfx.className = "pkb-mfx " + (endless ? "pkb-mfx-inf" : "pkb-mfx-n");
1776
+ 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>`;
1777
+ replay(mfx, "pkb-show");
1778
+ board.classList.add("pkb-switching");
1779
+ if (modeEl && !calm) replay(modeEl, "pkb-bump");
1780
+ sfx.mode(endless);
1781
+ haptics.play(endless ? "infinite" : "normal");
1782
+ const picked = radios.find((b) => b.dataset.m === mode);
1783
+ if (picked) {
1784
+ const p = rel(picked);
1785
+ 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 });
1786
+ }
1787
+ later(() => {
1788
+ const c = rel(gw);
1789
+ if (endless) burst(c.x, c.y, 90, { speed: 12, g: 0.07, life: 90, star: 0.75, colors: SPARK });
1790
+ else {
1791
+ if (!calm) replay(board, "pkb-punch");
1792
+ burst(c.x, c.y, 70, { speed: 11, star: 0.2 });
1793
+ }
1794
+ }, endless ? 260 : 430);
1795
+ }
1796
+ mfx.addEventListener("animationend", (e) => {
1797
+ if (e.target !== mfx) return;
1798
+ mfx.classList.remove("pkb-show");
1799
+ board.classList.remove("pkb-switching");
1800
+ mfx.innerHTML = "";
1801
+ });
1802
+ function setMode(next) {
1803
+ if (dead || next === mode || next !== "normal" && next !== "infinite") return;
1804
+ const back = packing;
1805
+ if (next === "infinite") packing = { checked: checked.slice(), order: order.slice() };
1806
+ else packing = null;
1807
+ mode = next;
1808
+ resetAsked = false;
1809
+ if (back && back.order.some((v, s) => v !== order[s])) {
1810
+ flights.forEach((a) => a.cancel());
1811
+ flights = [];
1812
+ clearTimeout(landing);
1813
+ landing = 0;
1814
+ place(back.order);
1815
+ }
1816
+ syncMode();
1817
+ toReady();
1818
+ baseline(back ? back.checked : normalize(void 0));
1819
+ modeFx();
1820
+ options.onModeChange?.(mode);
1821
+ }
1822
+ modeEl?.addEventListener("click", (e) => {
1823
+ const b = e.target.closest("button[data-m]");
1824
+ if (b) setMode(b.dataset.m);
1825
+ });
1826
+ modeEl?.addEventListener("keydown", (e) => {
1827
+ if (!["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(e.key)) return;
1828
+ e.preventDefault();
1829
+ setMode(inf() ? "normal" : "infinite");
1830
+ radios.find((b) => b.dataset.m === mode)?.focus();
1831
+ });
1317
1832
  let startOrder = null;
1318
1833
  if (orderKey) {
1319
1834
  try {
@@ -1335,7 +1850,10 @@ function createPackingBingo(container, options = {}) {
1335
1850
  if (!initial && options.defaultChecked) initial = normalize(options.defaultChecked);
1336
1851
  if (!initial && demo) initial = demoStart();
1337
1852
  fit();
1338
- baseline(initial ?? normalize(void 0));
1853
+ if (inf()) packing = { checked: initial ?? normalize(void 0), order: order.slice() };
1854
+ syncMode();
1855
+ baseline(inf() ? normalize(void 0) : initial ?? normalize(void 0));
1856
+ if (phase !== "done") toReady();
1339
1857
  const ro = typeof ResizeObserver === "function" ? new ResizeObserver(() => {
1340
1858
  fit();
1341
1859
  drawLines();
@@ -1344,7 +1862,7 @@ function createPackingBingo(container, options = {}) {
1344
1862
  ro?.observe(gw);
1345
1863
  const onVisibility = () => {
1346
1864
  if (!document.hidden) {
1347
- if (restorePerfectFocus && perfectEl.classList.contains("pkb-show")) $("close").focus({ preventScroll: true });
1865
+ if (restorePerfectFocus && perfectEl.classList.contains("pkb-show")) resultFocus().focus({ preventScroll: true });
1348
1866
  restorePerfectFocus = false;
1349
1867
  return;
1350
1868
  }
@@ -1355,7 +1873,7 @@ function createPackingBingo(container, options = {}) {
1355
1873
  if (keepPerfect) {
1356
1874
  $("sScore").textContent = score.toLocaleString();
1357
1875
  $("sCombo").textContent = "\xD7" + maxCombo;
1358
- $("sBingo").textContent = String(cur.length);
1876
+ $("sBingo").textContent = String(resultLines());
1359
1877
  perfectEl.classList.add("pkb-show", "pkb-settled");
1360
1878
  }
1361
1879
  combo = 0;
@@ -1365,6 +1883,14 @@ function createPackingBingo(container, options = {}) {
1365
1883
  setChecked(next, opts = {}) {
1366
1884
  if (dead) return;
1367
1885
  const n2 = normalize(next);
1886
+ if (packing) {
1887
+ packing.checked = n2;
1888
+ resetAsked = false;
1889
+ if (storageKey) writeStore(storageKey, JSON.stringify(n2));
1890
+ return;
1891
+ }
1892
+ if (resetAsked && !n2.some(Boolean)) toReady();
1893
+ resetAsked = false;
1368
1894
  const changed = n2.flatMap((v, i) => v !== checked[i] ? [i] : []);
1369
1895
  if (!changed.length) return;
1370
1896
  if (!opts.animate || changed.length !== 1) {
@@ -1375,12 +1901,20 @@ function createPackingBingo(container, options = {}) {
1375
1901
  },
1376
1902
  reset() {
1377
1903
  if (dead) return;
1904
+ if (inf()) return restartInfinite();
1378
1905
  unshuffle();
1906
+ restartGame();
1379
1907
  request(normalize(void 0), null, { reset: true });
1380
1908
  },
1381
1909
  shuffle() {
1382
1910
  if (!dead) shuffleCard();
1383
1911
  },
1912
+ setMode(next) {
1913
+ setMode(next);
1914
+ },
1915
+ finish() {
1916
+ finishGame();
1917
+ },
1384
1918
  refresh() {
1385
1919
  if (!dead) render(null, true);
1386
1920
  },
@@ -1390,20 +1924,25 @@ function createPackingBingo(container, options = {}) {
1390
1924
  lines: cur.slice(),
1391
1925
  score,
1392
1926
  maxCombo,
1393
- complete: checked.every(Boolean),
1927
+ complete: !inf() && checked.every(Boolean),
1928
+ mode,
1929
+ cleared,
1930
+ finished: inf() && phase === "done",
1931
+ packing: packing && { checked: packing.checked.slice(), order: packing.order.slice() },
1394
1932
  size: n,
1395
1933
  free: L.free,
1396
- order: order.slice()
1934
+ order: order.slice(),
1935
+ time: Math.round(elapsed())
1397
1936
  };
1398
1937
  },
1399
1938
  destroy() {
1400
1939
  if (dead) return;
1401
1940
  dead = true;
1402
- grid.removeEventListener("click", beforePlay, true);
1403
- if (playDialog?.open) playDialog.close();
1941
+ stopTimers();
1404
1942
  timers.forEach((id) => clearTimeout(id));
1405
1943
  timers.clear();
1406
1944
  clearTimeout(landing);
1945
+ clearTimeout(clearTimer);
1407
1946
  flights.forEach((a) => a.cancel());
1408
1947
  cancelAnimationFrame(raf);
1409
1948
  cancelAnimationFrame(tw);
@@ -1422,5 +1961,7 @@ function createPackingBingo(container, options = {}) {
1422
1961
  bingoLines,
1423
1962
  createPackingBingo,
1424
1963
  en,
1425
- ja
1964
+ formatTime,
1965
+ ja,
1966
+ speedBonus
1426
1967
  });