packing-bingo 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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,6 +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",
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",
136
159
  stamp: "OK",
137
160
  free: "FREE",
138
161
  reachBadge: "\u30EA\u30FC\u30C1",
@@ -143,15 +166,15 @@ var ja = {
143
166
  bannerNth: (n) => `${n} \u30E9\u30A4\u30F3\u76EE\uFF01`,
144
167
  bannerMulti: (n) => `\u4E00\u6C17\u306B ${n} \u30E9\u30A4\u30F3\uFF01`,
145
168
  statusStart: () => "",
146
- statusLines: (lines, left) => `\u30D3\u30F3\u30B4 ${lines} \u30E9\u30A4\u30F3\uFF01 \u306E\u3053\u308A ${left} \u3053`,
169
+ statusLines: () => "",
147
170
  statusReach: () => "",
148
- statusDone: "\u{1F389} \u30D1\u30FC\u30D5\u30A7\u30AF\u30C8\uFF01\u5FD8\u308C\u7269\u30BC\u30ED\u3067\u51FA\u767A\u3067\u304D\u307E\u3059",
171
+ statusDone: "",
149
172
  perfectTag: "\u{1F3C6} PERFECT",
150
173
  perfectStamp: "\u6E96\u5099\n\u5B8C\u4E86",
151
174
  perfectTitle: "\u30D1\u30FC\u30D5\u30A7\u30AF\u30C8\uFF01",
152
- perfectBody: (n) => `${n}\u3053 \u305C\u3093\u3076\u305D\u308D\u3063\u305F\uFF01
153
- \u5FD8\u308C\u7269\u30BC\u30ED\u3067\u3001\u3044\u3063\u3066\u3089\u3063\u3057\u3083\u3044\uFF01`,
175
+ perfectBody: "",
154
176
  perfectBonus: (n) => `PERFECT BONUS +${n}`,
177
+ speedBonus: (n) => `SPEED BONUS +${n.toLocaleString()}`,
155
178
  statScore: "SCORE",
156
179
  statCombo: "MAX COMBO",
157
180
  statLines: "BINGO",
@@ -174,6 +197,27 @@ var en = {
174
197
  shuffle: "\u{1F500} Shuffle",
175
198
  soundOn: "\u{1F50A} Sound on",
176
199
  soundOff: "\u{1F507} Muted",
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",
177
221
  stamp: "OK",
178
222
  free: "FREE",
179
223
  reachBadge: "REACH",
@@ -184,15 +228,15 @@ var en = {
184
228
  bannerNth: (n) => `Line ${n}!`,
185
229
  bannerMulti: (n) => `${n} lines at once!`,
186
230
  statusStart: () => "",
187
- statusLines: (lines, left) => `${lines} bingo line${lines === 1 ? "" : "s"}! ${left} to go`,
231
+ statusLines: () => "",
188
232
  statusReach: () => "",
189
- statusDone: "\u{1F389} Perfect! Nothing left behind.",
233
+ statusDone: "",
190
234
  perfectTag: "\u{1F3C6} PERFECT",
191
235
  perfectStamp: "ALL\nSET",
192
236
  perfectTitle: "Perfect!",
193
- perfectBody: (n) => `All ${n} packed!
194
- Have a great trip!`,
237
+ perfectBody: "",
195
238
  perfectBonus: (n) => `PERFECT BONUS +${n}`,
239
+ speedBonus: (n) => `SPEED BONUS +${n.toLocaleString()}`,
196
240
  statScore: "SCORE",
197
241
  statCombo: "MAX COMBO",
198
242
  statLines: "BINGO",
@@ -209,8 +253,40 @@ var hz = (semitones) => 523.25 * Math.pow(2, semitones / 12);
209
253
  function createSound(isMuted) {
210
254
  let ac = null;
211
255
  let out = null;
256
+ const voices = /* @__PURE__ */ new Map();
257
+ function track(source, gain, start, nodes) {
258
+ voices.set(source, { gain, start });
259
+ source.onended = () => {
260
+ voices.delete(source);
261
+ source.disconnect();
262
+ nodes.forEach((n) => n.disconnect());
263
+ };
264
+ }
265
+ function stop() {
266
+ const now = ac?.currentTime ?? 0;
267
+ voices.forEach(({ gain, start }, source) => {
268
+ try {
269
+ if (start > now) {
270
+ gain.gain.cancelScheduledValues(now);
271
+ gain.gain.setValueAtTime(0, now);
272
+ source.stop(now);
273
+ } else {
274
+ if (typeof gain.gain.cancelAndHoldAtTime === "function") gain.gain.cancelAndHoldAtTime(now);
275
+ else {
276
+ const value = gain.gain.value;
277
+ gain.gain.cancelScheduledValues(now);
278
+ gain.gain.setValueAtTime(value, now);
279
+ }
280
+ gain.gain.linearRampToValueAtTime(0, now + 0.01);
281
+ source.stop(now + 0.01);
282
+ }
283
+ } catch {
284
+ }
285
+ });
286
+ voices.clear();
287
+ }
212
288
  function audio() {
213
- if (isMuted() || typeof window === "undefined") return null;
289
+ if (isMuted() || typeof window === "undefined" || document.hidden) return null;
214
290
  const ua = navigator.userActivation;
215
291
  if (!ac && ua && !ua.hasBeenActive) return null;
216
292
  if (!ac) {
@@ -238,6 +314,7 @@ function createSound(isMuted) {
238
314
  g.gain.exponentialRampToValueAtTime(vol, t + 0.01);
239
315
  g.gain.exponentialRampToValueAtTime(1e-4, t + dur);
240
316
  o.connect(g).connect(out);
317
+ track(o, g, t, [g]);
241
318
  o.start(t);
242
319
  o.stop(t + dur + 0.05);
243
320
  }
@@ -258,15 +335,19 @@ function createSound(isMuted) {
258
335
  f.Q.value = 0.7;
259
336
  g.gain.value = vol;
260
337
  s.connect(f).connect(g).connect(out);
338
+ track(s, g, t, [f, g]);
261
339
  s.start(t);
262
340
  }
263
341
  const step = (k) => hz(SCALE[Math.min(Math.max(k, 0), SCALE.length - 1)]);
264
342
  return {
265
343
  pop(k) {
266
344
  const f = step(k);
345
+ const energy = Math.min(3, Math.floor((k + 1) / 3));
267
346
  tone(f, 0, 0.16, "triangle", 0.22);
268
347
  tone(f * 2, 0, 0.08, "sine", 0.06);
269
- tone(150, 0, 0.09, "sine", 0.25, 60);
348
+ tone(170 + energy * 15, 0, 0.11, "sine", 0.28, 48);
349
+ noise(0, 0.035, 0.1 + energy * 0.025, 1800);
350
+ if (energy) tone(f * 1.5, 0.035, 0.11, "triangle", 0.04 + energy * 0.015);
270
351
  },
271
352
  off: () => tone(392, 0, 0.1, "sine", 0.1, 262),
272
353
  reach() {
@@ -296,7 +377,39 @@ function createSound(isMuted) {
296
377
  tone(hz(7), 0.34, 0.12, "triangle", 0.14);
297
378
  tone(hz(12), 0.42, 0.2, "triangle", 0.14);
298
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
+ },
410
+ stop,
299
411
  close() {
412
+ stop();
300
413
  if (ac) void ac.close().catch(() => {
301
414
  });
302
415
  ac = null;
@@ -305,6 +418,64 @@ function createSound(isMuted) {
305
418
  };
306
419
  }
307
420
 
421
+ // src/haptics.ts
422
+ var owner = null;
423
+ function createHaptics(enabled) {
424
+ const token = {};
425
+ let generation = 0;
426
+ const stop = () => {
427
+ generation++;
428
+ if (owner !== token) return;
429
+ owner = null;
430
+ try {
431
+ navigator.vibrate?.(0);
432
+ } catch {
433
+ }
434
+ };
435
+ const send = (pattern) => {
436
+ if (!enabled || document.hidden || typeof navigator.vibrate !== "function") return false;
437
+ if (navigator.userActivation && !navigator.userActivation.hasBeenActive) return false;
438
+ try {
439
+ if (navigator.vibrate(pattern)) {
440
+ owner = token;
441
+ return true;
442
+ }
443
+ } catch {
444
+ }
445
+ return false;
446
+ };
447
+ return {
448
+ play(moment, combo = 1, lines = 1, impactAt = 360) {
449
+ const current = ++generation;
450
+ const tier = Math.min(3, Math.floor(combo / 3));
451
+ const lead = Math.max(80, impactAt - 40);
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
+ );
456
+ if (!send(pattern)) return;
457
+ return (beat) => {
458
+ if (generation === current && owner === token) send([...beat]);
459
+ };
460
+ },
461
+ stop
462
+ };
463
+ }
464
+
465
+ // src/celebration.ts
466
+ var PERFECT_REVEAL_MS = 520;
467
+ var PERFECT_STAMP_DELAY_MS = 720;
468
+ var PERFECT_STAMP_BEAT = [260, 45, 160];
469
+ var PERFECT_FIREWORKS = [
470
+ { at: 1820, beat: [200] },
471
+ { at: 2170, beat: [100, 35, 160] },
472
+ { at: 2520, beat: [260] },
473
+ { at: 3e3, beat: [110, 40, 110] },
474
+ { at: 3430, beat: [320] },
475
+ { at: 3950, beat: [140, 40, 200] },
476
+ { at: 4540, beat: [460] }
477
+ ];
478
+
308
479
  // src/storage-key.ts
309
480
  function resolveStorageKey(options) {
310
481
  if (options.controlled) return null;
@@ -332,13 +503,17 @@ var CSS = `
332
503
  .pkb-board.pkb-punch{animation:pkb-punch .4s cubic-bezier(.3,1.6,.5,1)}
333
504
  @keyframes pkb-punch{30%{transform:scale(1.018)}}
334
505
  .pkb-head{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:12px 16px;margin-bottom:18px}
335
- .pkb-tags{display:flex;gap:8px;align-items:center}
506
+ .pkb-tags{display:flex;flex-wrap:wrap;gap:8px;align-items:center}
336
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}
337
508
  .pkb-kicker:empty{display:none}
338
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}
339
510
  @keyframes pkb-rb{to{background-position:-200% 0}}
340
511
  .pkb button{font-family:var(--pk-body)}
341
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}
342
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}
343
518
  .pkb-sub{font-weight:800;font-size:13px;margin-top:4px;word-break:auto-phrase}
344
519
  .pkb-side{display:flex;flex-direction:column;gap:8px;flex:none}
@@ -356,12 +531,12 @@ var CSS = `
356
531
  .pkb-combo b{font-family:var(--pk-display);font-weight:400;font-size:13px;color:var(--pk-red);white-space:nowrap}
357
532
  .pkb-bar{flex:1;height:9px;border:2px solid var(--pk-ink);border-radius:999px;background:var(--pk-paper);overflow:hidden}
358
533
  .pkb-bar i{display:block;height:100%;background:var(--pk-red);transform-origin:left}
359
- .pkb-combo.pkb-on .pkb-bar i{animation:pkb-drain 2.5s linear forwards}
534
+ .pkb-combo.pkb-on .pkb-bar i.pkb-draining{animation:pkb-drain 2.5s linear forwards}
360
535
  @keyframes pkb-drain{from{transform:scaleX(1)}to{transform:scaleX(0)}}
361
536
  .pkb-gw{position:relative}
362
537
  .pkb-grid{display:grid;grid-template-columns:repeat(var(--pk-n),minmax(0,1fr));gap:calc(12px*var(--pk-d))}
363
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}
364
- .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)}
365
540
  .pkb-tile input{position:absolute;opacity:0;pointer-events:none;width:1px;height:1px}
366
541
  .pkb-tile:has(input:focus-visible){outline:3px solid var(--pk-ink);outline-offset:3px}
367
542
  .pkb-emo{font-size:max(14px,calc(32px*var(--pk-s)));line-height:1;transition:opacity .2s}
@@ -369,6 +544,7 @@ var CSS = `
369
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}
370
545
  .pkb-tile.pkb-on .pkb-stamp{opacity:.9;transform:translate(-50%,-50%) rotate(-16deg) scale(1)}
371
546
  .pkb-tile.pkb-on .pkb-emo,.pkb-tile.pkb-on .pkb-lbl{opacity:.35}
547
+ .pkb-tile.pkb-lock{cursor:default}
372
548
  .pkb-tile.pkb-free{cursor:default;container-type:inline-size}
373
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)}
374
550
  .pkb-tile.pkb-line{background-color:var(--pk-mint)}
@@ -378,12 +554,14 @@ var CSS = `
378
554
  @keyframes pkb-aura{50%{box-shadow:4px 4px 0 var(--pk-ink),0 0 0 7px rgba(232,71,59,.35)}}
379
555
  .pkb-tile.pkb-reach::after{content:attr(data-reach);position:absolute;top:-11px;right:-8px;z-index:3;background:var(--pk-red);color:#fff;font-size:10.5px;font-weight:800;letter-spacing:.06em;padding:3px 8px;border-radius:999px;border:2px solid var(--pk-ink);transform:rotate(6deg);animation:pkb-bob 1.1s ease-in-out infinite}
380
556
  @keyframes pkb-bob{50%{transform:rotate(6deg) translateY(-4px)}}
381
- .pkb-tile.pkb-squash{animation:pkb-squash .34s cubic-bezier(.3,1.6,.5,1)}
382
- @keyframes pkb-squash{0%{transform:scale(.88)}55%{transform:scale(1.05)}}
557
+ .pkb-tile.pkb-squash{z-index:1;animation:pkb-squash .38s cubic-bezier(.16,1,.3,1)}
558
+ @keyframes pkb-squash{0%{transform:translate(3px,4px) scale(calc(.9 - var(--pkb-impact,0)*.018));box-shadow:0 0 0 var(--pk-ink)}38%{transform:translateY(calc(-2px - var(--pkb-impact,0)*1px)) scale(calc(1.045 + var(--pkb-impact,0)*.012))}70%{transform:translateY(1px) scale(.985)}100%{transform:none}}
559
+ .pkb-tile.pkb-on .pkb-stamp.pkb-hit{animation:pkb-stamp-hit .32s cubic-bezier(.16,1,.3,1) both}
560
+ @keyframes pkb-stamp-hit{0%{opacity:0;transform:translate(-50%,-50%) rotate(-26deg) scale(2.1)}28%{opacity:1;transform:translate(-50%,-50%) rotate(-12deg) scale(.88)}52%{transform:translate(-50%,-50%) rotate(-18deg) scale(1.08)}100%{opacity:.9;transform:translate(-50%,-50%) rotate(-16deg) scale(1)}}
383
561
  .pkb-tile.pkb-hop{animation:pkb-hop .55s cubic-bezier(.3,1.6,.5,1) var(--pkb-d,0ms) both}
384
562
  @keyframes pkb-hop{35%{transform:translateY(-14px) scale(1.07)}}
385
563
  .pkb-ring{position:absolute;left:50%;top:50%;width:70%;aspect-ratio:1;border:4px solid var(--pk-red);border-radius:50%;pointer-events:none;z-index:4;animation:pkb-ring .5s ease-out forwards}
386
- @keyframes pkb-ring{from{transform:translate(-50%,-50%) scale(.3);opacity:.9}to{transform:translate(-50%,-50%) scale(1.9);opacity:0}}
564
+ @keyframes pkb-ring{from{transform:translate(-50%,-50%) scale(.3);opacity:.9}to{transform:translate(-50%,-50%) scale(calc(1.9 + var(--pkb-impact,0)*.18));opacity:0}}
387
565
  .pkb-lines{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;overflow:visible;z-index:2;transition:opacity .4s}
388
566
  .pkb-board.pkb-all .pkb-lines{opacity:0}
389
567
  .pkb-lines line{fill:none;stroke-linecap:round;stroke-dasharray:1;stroke-dashoffset:0}
@@ -394,10 +572,11 @@ var CSS = `
394
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}
395
573
  .pkb-foot>span{flex:1 1 14em}
396
574
  .pkb-foot>span:empty{flex-basis:0}
397
- .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}
398
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}
399
577
  .pkb-shuf{background:var(--pk-paper);box-shadow:2px 2px 0 var(--pk-ink)}
400
- .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}
401
580
  .pkb-flash{position:absolute;inset:0;z-index:7;background:#fff;opacity:0;pointer-events:none}
402
581
  .pkb-flash.pkb-go{animation:pkb-flash .4s ease-out}
403
582
  @keyframes pkb-flash{0%{opacity:.8}100%{opacity:0}}
@@ -414,15 +593,43 @@ var CSS = `
414
593
  .pkb-float.pkb-big{font-size:clamp(14px,5.6cqi,34px);color:#FFE066}
415
594
  .pkb-float.pkb-rch{font-size:20px;color:var(--pk-red);-webkit-text-stroke:4px var(--pk-paper)}
416
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}
417
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}
418
- .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}
419
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)}
420
624
  .pkb-perfect.pkb-show .pkb-card{transform:none}
421
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)}
422
626
  .pkb-perfect.pkb-show .pkb-medal{animation:pkb-slam .38s cubic-bezier(.5,0,.75,0) .35s forwards}
627
+ .pkb-perfect.pkb-settled,.pkb-perfect.pkb-settled .pkb-card{transition:none}
628
+ .pkb-perfect.pkb-settled .pkb-medal{animation:none;opacity:1;transform:rotate(-14deg) scale(1)}
423
629
  @keyframes pkb-slam{to{opacity:1;transform:rotate(-14deg) scale(1)}}
424
630
  .pkb-ptitle{font-family:var(--pk-display);font-weight:400;font-size:30px;line-height:1.3;color:inherit}
425
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}
426
633
  .pkb-stats{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-bottom:16px}
427
634
  .pkb-stats>div{border:2px solid var(--pk-ink);border-radius:12px;padding:6px 4px 5px;background:#fff}
428
635
  .pkb-stats small{display:block;font-size:9.5px;font-weight:800;letter-spacing:.1em}
@@ -432,10 +639,72 @@ var CSS = `
432
639
  .pkb-fx{position:absolute;inset:0;width:100%;height:100%;z-index:10;pointer-events:none}
433
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}
434
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}
435
- @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}}
436
- @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}}
437
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}}
438
- @media (prefers-reduced-motion:reduce){.pkb-board.pkb-shake,.pkb-board.pkb-punch,.pkb-flash.pkb-go,.pkb-tile.pkb-hop,.pkb-tile.pkb-squash,.pkb-tile.pkb-reach,.pkb-tile.pkb-line::before,.pkb-pill.pkb-hot,.pkb-bump,.pkb-tile.pkb-reach::after,.pkb-banner::before,.pkb-lines .pkb-draw line,.pkb-kicker.pkb-gold{animation:none}.pkb-banner.pkb-show{animation:pkb-fade 1.8s forwards}.pkb-float{animation:pkb-fade 1s both}.pkb-perfect.pkb-show .pkb-medal{animation:none;opacity:1;transform:rotate(-14deg)}.pkb-card{transition:none}}
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}}
439
708
  @keyframes pkb-fade{0%,100%{opacity:0}15%,80%{opacity:1}}
440
709
  `;
441
710
 
@@ -462,7 +731,22 @@ function writeStore(key, value) {
462
731
  }
463
732
  var glintDelay = (c, n) => `${+((Math.floor(c / n) + c % n) * 0.36 / Math.max(1, n - 1)).toFixed(3)}s`;
464
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
+ }
465
747
  var FLY_MS = 560;
748
+ var CLEAR_MS = 1e3;
749
+ var SPARK = ["#FFFFFF", "#FFE066", "#FF9F1C", "#E8473B", "#9B5DE5", "#2F6FED", "#1FA971"];
466
750
  function ensureStyles() {
467
751
  if (document.getElementById("pkb-styles")) return;
468
752
  const s = document.createElement("style");
@@ -470,7 +754,8 @@ function ensureStyles() {
470
754
  s.textContent = CSS;
471
755
  document.head.appendChild(s);
472
756
  }
473
- function template(L, M, { demo, shuffle, vibrate }) {
757
+ var opt = (v, fallback) => v ?? fallback ?? "";
758
+ function template(L, M, { demo, shuffle, vibrate, ready, modes }) {
474
759
  const n = L.size;
475
760
  const input = vibrate ? `<input type="checkbox" switch role="checkbox">` : `<input type="checkbox">`;
476
761
  const tiles = Array.from({ length: n * n }, (_, c) => {
@@ -482,9 +767,15 @@ function template(L, M, { demo, shuffle, vibrate }) {
482
767
  }).join("");
483
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)}`;
484
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>` : "";
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>` : "";
485
776
  return `<div class="pkb" style="${scale}"><div class="pkb-board">
486
777
  <div class="pkb-head"><div>
487
- <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>
488
779
  <h2 class="pkb-title">${esc(M.title)}</h2>${M.subtitle ? `<p class="pkb-sub">${esc(M.subtitle)}</p>` : ""}</div>
489
780
  <div class="pkb-side"><div class="pkb-score">
490
781
  <div class="pkb-pill"><small>${esc(M.packed)}</small><b data-r="cnt"></b></div>
@@ -493,13 +784,14 @@ function template(L, M, { demo, shuffle, vibrate }) {
493
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>
494
785
  <div class="pkb-gw"><div class="pkb-grid">${tiles}</div><svg class="pkb-lines" aria-hidden="true"></svg>
495
786
  <div class="pkb-banner" data-r="banner" aria-hidden="true"><b data-r="bT"></b><span data-r="bS"></span></div></div>
496
- <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>
497
788
  <div class="pkb-flash" data-r="flash"></div>
498
- <div class="pkb-perfect" data-r="perfect"><div class="pkb-card" role="dialog" aria-label="${esc(M.perfectTitle)}">
499
- <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>
500
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>
501
- <button type="button" class="pkb-close" data-r="close">${esc(M.close)}</button></div></div>
502
- <canvas class="pkb-fx"></canvas></div>${demoBar}</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>`;
503
795
  }
504
796
  var perfectText = (M, total) => typeof M.perfectBody === "function" ? M.perfectBody(total) : M.perfectBody;
505
797
  function createPackingBingo(container, options = {}) {
@@ -531,7 +823,15 @@ function createPackingBingo(container, options = {}) {
531
823
  const orderKey = storageKey && ORDER_PREFIX + storageKey;
532
824
  const demo = !!options.demo;
533
825
  const vibrate = options.vibrate !== false;
826
+ const uncheck = options.uncheck === true;
534
827
  const calm = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
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;
535
835
  let dead = false;
536
836
  const timers = /* @__PURE__ */ new Set();
537
837
  const later = (fn, ms) => {
@@ -544,12 +844,19 @@ function createPackingBingo(container, options = {}) {
544
844
  const wait = (ms) => new Promise((r) => window.setTimeout(r, ms));
545
845
  let gen = 0;
546
846
  ensureStyles();
547
- 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
+ });
548
854
  const $ = (r) => {
549
855
  const el = container.querySelector(`[data-r="${r}"]`);
550
856
  if (!el) throw new Error(`[packing-bingo] internal: missing element "${r}"`);
551
857
  return el;
552
858
  };
859
+ const root = container.querySelector(".pkb");
553
860
  const board = container.querySelector(".pkb-board");
554
861
  const gw = container.querySelector(".pkb-gw");
555
862
  const grid = container.querySelector(".pkb-grid");
@@ -560,12 +867,15 @@ function createPackingBingo(container, options = {}) {
560
867
  const tiles = L.itemCell.map((c) => cellTiles[c]);
561
868
  const freeTile = L.center >= 0 ? cellTiles[L.center] : null;
562
869
  const ins = tiles.map((t) => t.querySelector("input"));
563
- const kick = $("kick"), snd = $("snd"), perfectEl = $("perfect");
870
+ const stamps = tiles.map((t) => t.querySelector(".pkb-stamp"));
871
+ const kick = $("kick"), snd = $("snd"), perfectEl = $("perfect"), mfx = $("mfx");
564
872
  let checked = normalize(void 0);
565
873
  let cur = [];
566
874
  let prevLines = /* @__PURE__ */ new Set();
567
875
  let prevReach = /* @__PURE__ */ new Set();
568
876
  let prevAll = false;
877
+ let perfectPending = false, restorePerfectFocus = false;
878
+ let perfectBeat;
569
879
  let lastN = 0;
570
880
  let score = 0, shown = 0, combo = 0, maxCombo = 0, lastT = 0, gotP = false;
571
881
  const got = /* @__PURE__ */ new Set(), gotL = /* @__PURE__ */ new Set();
@@ -579,6 +889,7 @@ function createPackingBingo(container, options = {}) {
579
889
  snd.addEventListener("click", () => {
580
890
  muted = !muted;
581
891
  writeStore(MUTE_KEY, muted ? "1" : "0");
892
+ if (muted) sfx.stop();
582
893
  setSnd();
583
894
  if (!muted) sfx.pop(4);
584
895
  });
@@ -593,7 +904,7 @@ function createPackingBingo(container, options = {}) {
593
904
  cv.width = Math.round(BW * dpr);
594
905
  cv.height = Math.round(BH * dpr);
595
906
  };
596
- const piece = (x, y, vx, vy, g, s, life, shape2) => ({
907
+ const piece = (x, y, vx, vy, g, s, life, shape2, colors = CONFETTI) => ({
597
908
  x,
598
909
  y,
599
910
  vx,
@@ -605,7 +916,7 @@ function createPackingBingo(container, options = {}) {
605
916
  h: (3 + Math.random() * 5) * s,
606
917
  r: Math.random() * 6,
607
918
  vr: (Math.random() - 0.5) * 0.35,
608
- c: CONFETTI[Math.random() * CONFETTI.length | 0]
919
+ c: colors[Math.random() * colors.length | 0]
609
920
  });
610
921
  const go = () => {
611
922
  if (!raf && ctx) raf = requestAnimationFrame(tick);
@@ -616,7 +927,7 @@ function createPackingBingo(container, options = {}) {
616
927
  const a = (o.angle ?? -Math.PI / 2) + (Math.random() - 0.5) * (o.spread ?? Math.PI * 2);
617
928
  const v = (o.speed ?? 10) * (0.35 + Math.random() * 0.85);
618
929
  const sh = Math.random() < (o.star ?? 0.15) ? "star" : Math.random() < 0.3 ? "round" : "rect";
619
- 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));
620
931
  }
621
932
  go();
622
933
  }
@@ -711,8 +1022,10 @@ function createPackingBingo(container, options = {}) {
711
1022
  };
712
1023
  grid.addEventListener("animationend", (e) => {
713
1024
  const t = e.target;
1025
+ if (e.animationName === "pkb-stamp-hit") t.classList.remove("pkb-hit");
714
1026
  if (t.classList.contains("pkb-tile") && (e.animationName === "pkb-hop" || e.animationName === "pkb-squash"))
715
1027
  t.classList.remove(e.animationName);
1028
+ if (t.classList.contains("pkb-tile") && e.animationName === "pkb-clr") t.classList.remove("pkb-clr");
716
1029
  });
717
1030
  const ring = (t) => {
718
1031
  const r = document.createElement("span");
@@ -720,15 +1033,6 @@ function createPackingBingo(container, options = {}) {
720
1033
  t.appendChild(r);
721
1034
  r.addEventListener("animationend", () => r.remove());
722
1035
  };
723
- const buzz = (pattern) => {
724
- if (!vibrate) return;
725
- const ua = navigator.userActivation;
726
- if (ua && !ua.hasBeenActive) return;
727
- try {
728
- navigator.vibrate?.(pattern);
729
- } catch {
730
- }
731
- };
732
1036
  const floatText = (txt, x, y, cls, delay = 0, color) => {
733
1037
  const el = document.createElement("div");
734
1038
  el.className = "pkb-float pkb-" + cls;
@@ -740,20 +1044,20 @@ function createPackingBingo(container, options = {}) {
740
1044
  board.appendChild(el);
741
1045
  el.addEventListener("animationend", () => el.remove());
742
1046
  };
743
- 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));
744
1048
  const ease = (k) => 1 - Math.pow(1 - k, 3);
745
1049
  let tw = 0;
746
1050
  function countScore() {
747
1051
  cancelAnimationFrame(tw);
748
- const a = shown, b = score, t0 = performance.now();
1052
+ const a = shown, b = score, t02 = performance.now();
749
1053
  const st = (t) => {
750
1054
  if (dead) return;
751
- const k = prog(t, t0, 450);
1055
+ const k = prog(t, t02, 450);
752
1056
  shown = Math.round(a + (b - a) * ease(k));
753
1057
  $("sc").textContent = shown.toLocaleString();
754
1058
  if (k < 1) tw = requestAnimationFrame(st);
755
1059
  };
756
- st(t0);
1060
+ st(t02);
757
1061
  if (!calm) replay($("sp"), "pkb-bump");
758
1062
  }
759
1063
  const showGain = (pts, x, y, cls = "pt") => {
@@ -761,12 +1065,12 @@ function createPackingBingo(container, options = {}) {
761
1065
  countScore();
762
1066
  };
763
1067
  function countUp(el, a, b, d, fmt, ticks = false) {
764
- const t0 = performance.now();
1068
+ const t02 = performance.now();
765
1069
  const g = gen;
766
1070
  let lt = 0;
767
1071
  const st = (t) => {
768
1072
  if (dead || g !== gen) return;
769
- const k = prog(t, t0, d);
1073
+ const k = prog(t, t02, d);
770
1074
  el.textContent = fmt(Math.round(a + (b - a) * ease(k)));
771
1075
  if (ticks && k < 1 && t - lt > 55) {
772
1076
  sfx.tick();
@@ -775,7 +1079,7 @@ function createPackingBingo(container, options = {}) {
775
1079
  if (k < 1) requestAnimationFrame(st);
776
1080
  else if (!calm) replay(el, "pkb-bump");
777
1081
  };
778
- st(t0);
1082
+ st(t02);
779
1083
  }
780
1084
  const ctr = (t) => {
781
1085
  return { x: t.offsetLeft + t.offsetWidth / 2, y: t.offsetTop + t.offsetHeight / 2 };
@@ -794,25 +1098,147 @@ function createPackingBingo(container, options = {}) {
794
1098
  return `<g${fresh.includes(k) ? ' class="pkb-draw"' : ""}><line class="pkb-u" ${at}/><line class="pkb-o" ${at}/></g>`;
795
1099
  }).join("");
796
1100
  }
797
- function tapFx(i, on) {
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);
1215
+ function tapFx(i, on, moment, fresh) {
798
1216
  const p = rel(tiles[i]);
799
1217
  if (!on) {
1218
+ stamps[i].classList.remove("pkb-hit");
800
1219
  combo = 0;
801
1220
  $("combo").classList.remove("pkb-on");
802
1221
  sfx.off();
803
- buzz(8);
1222
+ haptics.play("off");
804
1223
  return;
805
1224
  }
806
1225
  const now = performance.now();
807
1226
  combo = now - lastT < COMBO_MS ? combo + 1 : 1;
808
1227
  lastT = now;
809
1228
  maxCombo = Math.max(maxCombo, combo);
1229
+ const seqLength = new Set(fresh.flatMap((k) => cellLines[k])).size;
1230
+ const impactAt = Math.min(70 * seqLength, 300) + 60;
1231
+ const beat = haptics.play(moment, combo, fresh.length, impactAt);
1232
+ perfectBeat = moment === "perfect" ? beat : void 0;
1233
+ const energy = Math.min(3, Math.floor(combo / 3));
810
1234
  sfx.pop(combo - 1);
811
1235
  if (!calm) {
1236
+ tiles[i].style.setProperty("--pkb-impact", String(energy));
812
1237
  replay(tiles[i], "pkb-squash");
1238
+ replay(stamps[i], "pkb-hit");
813
1239
  ring(tiles[i]);
814
1240
  }
815
- burst(p.x, p.y, 18, { speed: 6, g: 0.2, life: 30, size: 0.6, star: 0.5 });
1241
+ burst(p.x, p.y, 18 + energy * 6, { speed: 6 + energy, g: 0.2, life: 30, size: 0.6, star: 0.5 });
816
1242
  if (!got.has(i)) {
817
1243
  got.add(i);
818
1244
  score += 10 * combo;
@@ -821,9 +1247,10 @@ function createPackingBingo(container, options = {}) {
821
1247
  if (combo >= 2) {
822
1248
  floatText(M.comboPop(combo), p.x, p.y + 22, "cmb", 90, COMBO_COLORS[(combo - 2) % COMBO_COLORS.length]);
823
1249
  $("cmbN").textContent = M.combo(combo);
824
- replay($("combo"), "pkb-on");
1250
+ $("combo").classList.add("pkb-on");
1251
+ replay($("cbar"), "pkb-draining");
1252
+ if (!calm) replay($("cmbN"), "pkb-bump");
825
1253
  }
826
- buzz(18);
827
1254
  }
828
1255
  $("cbar").addEventListener("animationend", () => {
829
1256
  $("combo").classList.remove("pkb-on");
@@ -836,7 +1263,7 @@ function createPackingBingo(container, options = {}) {
836
1263
  });
837
1264
  later(() => sfx.reach(), 150);
838
1265
  }
839
- function bingo(fresh, pts) {
1266
+ function bingo(fresh, pts, total) {
840
1267
  const nl = fresh.length;
841
1268
  const seq = [...new Set(fresh.flatMap((k) => cellLines[k]))].map((c) => cellTiles[c]);
842
1269
  const step = Math.min(70, 300 / seq.length);
@@ -850,7 +1277,7 @@ function createPackingBingo(container, options = {}) {
850
1277
  );
851
1278
  later(() => {
852
1279
  $("bT").textContent = nl >= 3 ? M.bannerTriple : nl === 2 ? M.bannerDouble : M.bannerSingle;
853
- $("bS").textContent = nl > 1 ? M.bannerMulti(nl) : M.bannerNth(cur.length);
1280
+ $("bS").textContent = nl > 1 ? M.bannerMulti(nl) : M.bannerNth(total);
854
1281
  replay($("banner"), "pkb-show");
855
1282
  if (!calm) {
856
1283
  replay($("flash"), "pkb-go");
@@ -859,54 +1286,126 @@ function createPackingBingo(container, options = {}) {
859
1286
  const c = rel(gw);
860
1287
  burst(c.x, c.y, nl > 1 ? 180 : 110, { speed: nl > 1 ? 16 : 13, star: 0.3 });
861
1288
  sfx.bingo(nl);
862
- buzz(nl > 1 ? [40, 60, 40, 60, 90] : [40, 60, 40]);
863
1289
  if (pts) later(() => showGain(pts, c.x, c.y + 105, "big"), 350);
864
1290
  }, seq.length * step + 60);
865
1291
  }
866
1292
  function perfect(events) {
1293
+ const beat = perfectBeat;
1294
+ perfectPending = true;
1295
+ perfectEl.classList.remove("pkb-settled");
867
1296
  const bonus = gotP ? 0 : 1e3;
1297
+ const speed = gotP || finalMs == null ? 0 : speedBonus(N, finalMs);
868
1298
  gotP = true;
869
- score += bonus;
1299
+ score += bonus + speed;
870
1300
  cellTiles.forEach((t, c) => hop(t, far(c) * 180 / Math.max(1, n - 1)));
871
1301
  sfx.perfect();
872
1302
  if (!calm) replay($("flash"), "pkb-go");
1303
+ setResult(false);
873
1304
  $("pbody").textContent = perfectText(M, N);
1305
+ $("ptime").hidden = finalMs == null;
1306
+ $("pT").textContent = formatTime(finalMs ?? 0, 2);
874
1307
  $("sScore").textContent = "0";
875
1308
  $("sCombo").textContent = "\xD70";
876
1309
  $("sBingo").textContent = "0";
877
1310
  later(() => {
1311
+ perfectPending = false;
878
1312
  replay(perfectEl, "pkb-show");
879
1313
  $("close").focus({ preventScroll: true });
880
1314
  later(() => {
881
1315
  if (!calm) replay(board, "pkb-shake");
882
1316
  sfx.boom();
1317
+ beat?.(PERFECT_STAMP_BEAT);
883
1318
  burst(0, BH, 90, { angle: -Math.PI / 3, spread: 0.8, speed: 22, g: 0.32, star: 0.3 });
884
1319
  burst(BW, BH, 90, { angle: -Math.PI * 2 / 3, spread: 0.8, speed: 22, g: 0.32, star: 0.3 });
885
1320
  rain(3500);
886
- buzz([60, 80, 60, 80, 200]);
887
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");
888
1324
  countUp($("sScore"), 0, score, 1300, (v) => v.toLocaleString(), true);
889
1325
  countUp($("sCombo"), 0, maxCombo, 700, (v) => "\xD7" + v);
890
- countUp($("sBingo"), 0, cur.length, 700, String);
1326
+ countUp($("sBingo"), 0, resultLines(), 700, String);
891
1327
  countScore();
892
- }, 720);
893
- [1300, 1650, 2e3, 2350, 2700].forEach(
894
- (t) => later(() => firework(BW * (0.15 + Math.random() * 0.7), BH * (0.12 + Math.random() * 0.3)), t)
895
- );
1328
+ }, PERFECT_STAMP_DELAY_MS);
896
1329
  later(() => emojiRain(3e3), 1500);
897
- }, 520);
898
- const e = { score, maxCombo };
1330
+ }, PERFECT_REVEAL_MS);
1331
+ PERFECT_FIREWORKS.forEach(
1332
+ ({ at, beat: pulse }) => later(() => {
1333
+ firework(BW * (0.15 + Math.random() * 0.7), BH * (0.12 + Math.random() * 0.3));
1334
+ beat?.(pulse);
1335
+ }, at)
1336
+ );
1337
+ const e = { score, maxCombo, time: finalMs == null ? null : Math.round(finalMs), speedBonus: speed };
899
1338
  events.push(() => options.onPerfect?.(e));
900
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");
901
1396
  function cancelFx() {
1397
+ if (pending.size) clearNow(false);
902
1398
  gen++;
1399
+ perfectPending = false;
903
1400
  timers.forEach((id) => clearTimeout(id));
904
1401
  timers.clear();
1402
+ haptics.stop();
1403
+ sfx.stop();
905
1404
  cancelAnimationFrame(tw);
906
1405
  shown = score;
907
1406
  $("sc").textContent = score.toLocaleString();
908
1407
  }
909
- function clearVisuals() {
1408
+ function clearVisuals(keepPerfect = false) {
910
1409
  P = [];
911
1410
  cancelAnimationFrame(raf);
912
1411
  raf = 0;
@@ -915,18 +1414,33 @@ function createPackingBingo(container, options = {}) {
915
1414
  $("banner").classList.remove("pkb-show");
916
1415
  $("flash").classList.remove("pkb-go");
917
1416
  $("combo").classList.remove("pkb-on");
918
- perfectEl.classList.remove("pkb-show");
1417
+ $("cbar").classList.remove("pkb-draining");
1418
+ cellTiles.forEach((t) => t.classList.remove("pkb-squash", "pkb-hop", "pkb-clr"));
1419
+ stamps.forEach((s) => s.classList.remove("pkb-hit"));
1420
+ board.classList.remove("pkb-punch", "pkb-shake", "pkb-switching");
1421
+ mfx.classList.remove("pkb-show");
1422
+ if (!keepPerfect) perfectEl.classList.remove("pkb-show", "pkb-settled");
919
1423
  }
920
1424
  const completeLines = () => lines.flatMap((l, k) => l.every((i) => checked[i]) ? [k] : []);
921
1425
  function render(src, quiet) {
922
1426
  const events = [];
923
1427
  const on = checked, c = on.filter(Boolean).length;
1428
+ const endless = inf();
1429
+ const all = c === N && !endless;
924
1430
  cur = completeLines();
925
1431
  const fresh = cur.filter((k) => !prevLines.has(k));
926
1432
  if ([...prevLines].some((k) => !cur.includes(k))) {
927
1433
  cancelFx();
928
1434
  if (c < N) perfectEl.classList.remove("pkb-show");
929
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
+ }
930
1444
  const hit = new Set(cur.flatMap((k) => cellLines[k]));
931
1445
  const reach = /* @__PURE__ */ new Set();
932
1446
  for (const l of lines) {
@@ -935,34 +1449,43 @@ function createPackingBingo(container, options = {}) {
935
1449
  }
936
1450
  tiles.forEach((t, i) => {
937
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");
938
1456
  t.classList.toggle("pkb-line", hit.has(L.itemCell[slotOf[i]]));
939
1457
  t.classList.toggle("pkb-reach", reach.has(i));
940
1458
  });
941
1459
  freeTile?.classList.toggle("pkb-line", hit.has(L.center));
942
- board.classList.toggle("pkb-all", c === N);
943
- kick.classList.toggle("pkb-gold", c === N);
944
- 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;
945
1463
  $("cnt").textContent = `${c}/${N}`;
946
- $("bng").textContent = String(cur.length);
1464
+ const total = endless ? cleared : cur.length;
1465
+ $("bng").textContent = String(total);
947
1466
  const bp = $("bp");
948
- if (cur.length > lastN) {
1467
+ if (total > lastN) {
949
1468
  bp.classList.remove("pkb-hot");
950
1469
  void bp.offsetWidth;
951
1470
  }
952
- bp.classList.toggle("pkb-hot", cur.length > 0);
953
- lastN = cur.length;
1471
+ bp.classList.toggle("pkb-hot", total > 0);
1472
+ lastN = total;
954
1473
  drawLines(fresh);
955
- if (src !== null) tapFx(src, on[src]);
1474
+ if (src !== null) {
1475
+ const newReach = [...reach].some((i) => !prevReach.has(i));
1476
+ const moment = !quiet && all && !prevAll ? "perfect" : !quiet && fresh.length ? "bingo" : !quiet && newReach ? "reach" : "tap";
1477
+ tapFx(src, on[src], moment, fresh);
1478
+ }
956
1479
  const newL = fresh.map((k) => lineKey(lines[k])).filter((key) => !gotL.has(key));
957
1480
  newL.forEach((key) => gotL.add(key));
958
1481
  const linePts = 100 * newL.length * newL.length;
959
1482
  score += linePts;
960
1483
  if (quiet) {
961
1484
  if (linePts) countScore();
962
- } else if (c === N && !prevAll) perfect(events);
1485
+ } else if (all && !prevAll) perfect(events);
963
1486
  else if (fresh.length) {
964
- bingo(fresh, linePts);
965
- const e = { newLines: fresh, totalLines: cur.length, score };
1487
+ bingo(fresh, linePts, total);
1488
+ const e = { newLines: fresh, totalLines: total, score, mode };
966
1489
  events.push(() => options.onBingo?.(e));
967
1490
  } else if (src !== null && on[src]) {
968
1491
  const nr = [...reach].filter((i) => !prevReach.has(i));
@@ -970,9 +1493,9 @@ function createPackingBingo(container, options = {}) {
970
1493
  }
971
1494
  prevLines = new Set(cur);
972
1495
  prevReach = reach;
973
- prevAll = c === N;
1496
+ prevAll = all;
974
1497
  const labels = [...reach].map((i) => items[i].label);
975
- $("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);
976
1499
  return events;
977
1500
  }
978
1501
  const flush = (events) => {
@@ -982,9 +1505,11 @@ function createPackingBingo(container, options = {}) {
982
1505
  }
983
1506
  };
984
1507
  const save = () => {
985
- if (storageKey) writeStore(storageKey, JSON.stringify(checked));
1508
+ if (storageKey && !inf()) writeStore(storageKey, JSON.stringify(checked));
986
1509
  };
987
1510
  function apply(next, src, quiet) {
1511
+ if (phase === "idle" && src !== null && next[src]) startWatch();
1512
+ if (!inf() && next.every(Boolean)) finish();
988
1513
  checked = next;
989
1514
  ins.forEach((x, i) => x.checked = checked[i]);
990
1515
  const events = render(src, quiet);
@@ -992,8 +1517,12 @@ function createPackingBingo(container, options = {}) {
992
1517
  return events;
993
1518
  }
994
1519
  function baseline(next) {
1520
+ dropClear();
1521
+ cleared = 0;
995
1522
  cancelFx();
996
1523
  clearVisuals();
1524
+ if (!inf() && next.every(Boolean)) finish();
1525
+ else if ((phase === "play" || phase === "done") && !next.some(Boolean)) toReady();
997
1526
  checked = next;
998
1527
  ins.forEach((x, i) => x.checked = checked[i]);
999
1528
  got.clear();
@@ -1003,7 +1532,7 @@ function createPackingBingo(container, options = {}) {
1003
1532
  done.forEach((k) => gotL.add(lineKey(lines[k])));
1004
1533
  prevLines = new Set(done);
1005
1534
  prevReach = /* @__PURE__ */ new Set();
1006
- prevAll = checked.every(Boolean);
1535
+ prevAll = !inf() && checked.every(Boolean);
1007
1536
  gotP = prevAll;
1008
1537
  score = shown = got.size * 10 + gotL.size * 100;
1009
1538
  combo = 0;
@@ -1013,7 +1542,9 @@ function createPackingBingo(container, options = {}) {
1013
1542
  save();
1014
1543
  }
1015
1544
  function request(next, src, opts = {}) {
1016
- if (controlled) {
1545
+ const game = inf();
1546
+ if (controlled && !game) {
1547
+ resetAsked = !!opts.reset && resetAsked;
1017
1548
  ins.forEach((x, i) => x.checked = checked[i]);
1018
1549
  options.onChange?.(next.slice());
1019
1550
  return;
@@ -1021,7 +1552,7 @@ function createPackingBingo(container, options = {}) {
1021
1552
  let events = [];
1022
1553
  if (opts.reset) baseline(next);
1023
1554
  else events = apply(next, src, !!opts.quiet);
1024
- options.onChange?.(checked.slice());
1555
+ if (!game) options.onChange?.(checked.slice());
1025
1556
  flush(events);
1026
1557
  }
1027
1558
  function place(next) {
@@ -1040,12 +1571,12 @@ function createPackingBingo(container, options = {}) {
1040
1571
  focused.focus({ preventScroll: true });
1041
1572
  }
1042
1573
  const saveOrder = () => {
1043
- if (!orderKey) return;
1574
+ if (!orderKey || inf()) return;
1044
1575
  writeStore(orderKey, JSON.stringify(order));
1045
1576
  };
1046
1577
  let flights = [];
1047
1578
  function arrange(next) {
1048
- if (!checked.every(Boolean)) cancelFx();
1579
+ if (inf() || !checked.every(Boolean)) cancelFx();
1049
1580
  const from = calm ? null : tiles.map((t) => t.getBoundingClientRect());
1050
1581
  flights.forEach((a) => a.cancel());
1051
1582
  flights = [];
@@ -1081,6 +1612,7 @@ function createPackingBingo(container, options = {}) {
1081
1612
  svg.innerHTML = "";
1082
1613
  }
1083
1614
  function shuffleCard() {
1615
+ if (inf() && phase === "done") return;
1084
1616
  let next = order.slice();
1085
1617
  do {
1086
1618
  for (let s = N - 1; s > 0; s--) {
@@ -1090,31 +1622,98 @@ function createPackingBingo(container, options = {}) {
1090
1622
  } while (next.every((v, s) => v === order[s]));
1091
1623
  arrange(next);
1092
1624
  sfx.shuffle();
1093
- buzz([10, 40, 10, 40, 10, 40, 24]);
1625
+ haptics.play("shuffle");
1094
1626
  }
1095
1627
  const unshuffle = () => {
1096
1628
  if (!isIdentity(order)) arrange(identity());
1097
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
+ });
1098
1635
  grid.addEventListener("change", (e) => {
1099
1636
  const i = ins.indexOf(e.target);
1100
1637
  if (i < 0) return;
1638
+ if (locked(i)) {
1639
+ ins[i].checked = true;
1640
+ return;
1641
+ }
1101
1642
  const next = checked.slice();
1102
1643
  next[i] = !next[i];
1103
1644
  request(next, i);
1104
1645
  });
1105
1646
  container.querySelector('[data-r="shuf"]')?.addEventListener("click", shuffleCard);
1647
+ const restartInfinite = () => {
1648
+ baseline(normalize(void 0));
1649
+ toReady();
1650
+ };
1106
1651
  $("reset").addEventListener("click", () => {
1652
+ if (inf()) return restartInfinite();
1107
1653
  unshuffle();
1654
+ restartGame();
1108
1655
  request(normalize(void 0), null, { reset: true });
1109
1656
  });
1110
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);
1111
1710
  perfectEl.addEventListener("click", (e) => {
1112
1711
  if (e.target === perfectEl) perfectEl.classList.remove("pkb-show");
1113
1712
  });
1114
1713
  if (demo) {
1115
1714
  let busy = false;
1116
1715
  const prepare = () => {
1117
- if (checked.every(Boolean)) request(normalize(void 0), null, { reset: true });
1716
+ if (!inf() && checked.every(Boolean)) request(normalize(void 0), null, { reset: true });
1118
1717
  else perfectEl.classList.remove("pkb-show");
1119
1718
  };
1120
1719
  const run = async (steps) => {
@@ -1122,6 +1721,7 @@ function createPackingBingo(container, options = {}) {
1122
1721
  busy = true;
1123
1722
  try {
1124
1723
  prepare();
1724
+ skipReady();
1125
1725
  const g = gen;
1126
1726
  for (const s of steps) {
1127
1727
  await wait(s.delay);
@@ -1135,21 +1735,100 @@ function createPackingBingo(container, options = {}) {
1135
1735
  }
1136
1736
  };
1137
1737
  $("dB").addEventListener("click", () => {
1738
+ if (inf() && phase === "done") return;
1138
1739
  const open = () => lines.map((l) => l.filter((i) => !checked[i])).filter((m) => m.length).sort((a, b) => a.length - b.length)[0] ?? [];
1139
- const pick = checked.every(Boolean) ? [...lines[0]] : open();
1740
+ const pick = !inf() && checked.every(Boolean) ? [...lines[0]] : open();
1140
1741
  void run(pick.map((i) => ({ i, delay: 320 })));
1141
1742
  });
1142
1743
  $("dP").addEventListener("click", () => {
1744
+ if (inf()) return;
1143
1745
  const all = items.map((_, i) => i);
1144
1746
  const miss = checked.every(Boolean) ? all : all.filter((i) => !checked[i]);
1145
1747
  void run(miss.map((i, k) => k < miss.length - 1 ? { i, delay: 170, quiet: true } : { i, delay: 450 }));
1146
1748
  });
1147
1749
  $("dR").addEventListener("click", () => {
1148
- if (busy) return;
1750
+ if (busy || inf()) return;
1149
1751
  unshuffle();
1752
+ restartGame();
1150
1753
  request(demoStart(), null, { reset: true });
1151
1754
  });
1152
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
+ });
1153
1832
  let startOrder = null;
1154
1833
  if (orderKey) {
1155
1834
  try {
@@ -1171,17 +1850,47 @@ function createPackingBingo(container, options = {}) {
1171
1850
  if (!initial && options.defaultChecked) initial = normalize(options.defaultChecked);
1172
1851
  if (!initial && demo) initial = demoStart();
1173
1852
  fit();
1174
- 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();
1175
1857
  const ro = typeof ResizeObserver === "function" ? new ResizeObserver(() => {
1176
1858
  fit();
1177
1859
  drawLines();
1178
1860
  }) : null;
1179
1861
  ro?.observe(board);
1180
1862
  ro?.observe(gw);
1863
+ const onVisibility = () => {
1864
+ if (!document.hidden) {
1865
+ if (restorePerfectFocus && perfectEl.classList.contains("pkb-show")) resultFocus().focus({ preventScroll: true });
1866
+ restorePerfectFocus = false;
1867
+ return;
1868
+ }
1869
+ const keepPerfect = perfectPending || perfectEl.classList.contains("pkb-show");
1870
+ restorePerfectFocus = keepPerfect;
1871
+ cancelFx();
1872
+ clearVisuals(keepPerfect);
1873
+ if (keepPerfect) {
1874
+ $("sScore").textContent = score.toLocaleString();
1875
+ $("sCombo").textContent = "\xD7" + maxCombo;
1876
+ $("sBingo").textContent = String(resultLines());
1877
+ perfectEl.classList.add("pkb-show", "pkb-settled");
1878
+ }
1879
+ combo = 0;
1880
+ };
1881
+ document.addEventListener("visibilitychange", onVisibility);
1181
1882
  return {
1182
1883
  setChecked(next, opts = {}) {
1183
1884
  if (dead) return;
1184
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;
1185
1894
  const changed = n2.flatMap((v, i) => v !== checked[i] ? [i] : []);
1186
1895
  if (!changed.length) return;
1187
1896
  if (!opts.animate || changed.length !== 1) {
@@ -1192,12 +1901,20 @@ function createPackingBingo(container, options = {}) {
1192
1901
  },
1193
1902
  reset() {
1194
1903
  if (dead) return;
1904
+ if (inf()) return restartInfinite();
1195
1905
  unshuffle();
1906
+ restartGame();
1196
1907
  request(normalize(void 0), null, { reset: true });
1197
1908
  },
1198
1909
  shuffle() {
1199
1910
  if (!dead) shuffleCard();
1200
1911
  },
1912
+ setMode(next) {
1913
+ setMode(next);
1914
+ },
1915
+ finish() {
1916
+ finishGame();
1917
+ },
1201
1918
  refresh() {
1202
1919
  if (!dead) render(null, true);
1203
1920
  },
@@ -1207,22 +1924,31 @@ function createPackingBingo(container, options = {}) {
1207
1924
  lines: cur.slice(),
1208
1925
  score,
1209
1926
  maxCombo,
1210
- 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() },
1211
1932
  size: n,
1212
1933
  free: L.free,
1213
- order: order.slice()
1934
+ order: order.slice(),
1935
+ time: Math.round(elapsed())
1214
1936
  };
1215
1937
  },
1216
1938
  destroy() {
1217
1939
  if (dead) return;
1218
1940
  dead = true;
1941
+ stopTimers();
1219
1942
  timers.forEach((id) => clearTimeout(id));
1220
1943
  timers.clear();
1221
1944
  clearTimeout(landing);
1945
+ clearTimeout(clearTimer);
1222
1946
  flights.forEach((a) => a.cancel());
1223
1947
  cancelAnimationFrame(raf);
1224
1948
  cancelAnimationFrame(tw);
1225
1949
  ro?.disconnect();
1950
+ document.removeEventListener("visibilitychange", onVisibility);
1951
+ haptics.stop();
1226
1952
  sfx.close();
1227
1953
  container.innerHTML = "";
1228
1954
  }
@@ -1235,5 +1961,7 @@ function createPackingBingo(container, options = {}) {
1235
1961
  bingoLines,
1236
1962
  createPackingBingo,
1237
1963
  en,
1238
- ja
1964
+ formatTime,
1965
+ ja,
1966
+ speedBonus
1239
1967
  });