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