@trolleroof/tui 0.2.5 → 0.2.6

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.
Files changed (91) hide show
  1. package/.claude-plugin/plugin.json +10 -0
  2. package/.codex-plugin/plugin.json +2 -2
  3. package/README.md +29 -7
  4. package/dist/index.js +284 -18
  5. package/overlay/dist/app.js +74 -0
  6. package/overlay/dist/index.html +1589 -0
  7. package/overlay-release.json +2 -2
  8. package/package.json +27 -4
  9. package/scripts/install-adapters.sh +6 -6
  10. package/scripts/install-adapters.test.sh +2 -2
  11. package/scripts/materialize-agents-plugin.sh +3 -3
  12. package/scripts/npm-pack.test.ts +51 -0
  13. package/scripts/overlay.ts +1 -1
  14. package/src/analytics/events.test.ts +77 -0
  15. package/src/analytics/events.ts +85 -0
  16. package/src/analytics/posthog.test.ts +85 -0
  17. package/src/analytics/posthog.ts +88 -0
  18. package/src/auth-pages.ts +108 -0
  19. package/src/auth.test.ts +255 -0
  20. package/src/auth.ts +511 -0
  21. package/src/core/game.ts +35 -0
  22. package/src/core/key.ts +12 -0
  23. package/src/core/rng.ts +41 -0
  24. package/src/data/bc-export.test.ts +153 -0
  25. package/src/data/bc-export.ts +125 -0
  26. package/src/data/trajectory.ts +103 -0
  27. package/src/games/connect4/connect4.ts +81 -0
  28. package/src/games/connect4/engine.test.ts +77 -0
  29. package/src/games/connect4/engine.ts +89 -0
  30. package/src/games/g2048/engine.test.ts +138 -0
  31. package/src/games/g2048/engine.ts +109 -0
  32. package/src/games/g2048/game2048.ts +104 -0
  33. package/src/games/minesweeper/engine.test.ts +83 -0
  34. package/src/games/minesweeper/engine.ts +112 -0
  35. package/src/games/minesweeper/minesweeper.ts +107 -0
  36. package/src/games/registry.test.ts +16 -0
  37. package/src/games/registry.ts +130 -0
  38. package/src/games/snake/engine.test.ts +89 -0
  39. package/src/games/snake/engine.ts +95 -0
  40. package/src/games/snake/snake.ts +77 -0
  41. package/src/games/sokoban/engine.test.ts +49 -0
  42. package/src/games/sokoban/engine.ts +152 -0
  43. package/src/games/sokoban/sokoban.ts +103 -0
  44. package/src/games/sudoku/engine.test.ts +88 -0
  45. package/src/games/sudoku/engine.ts +99 -0
  46. package/src/games/sudoku/sudoku.ts +100 -0
  47. package/src/games/tetris/engine.test.ts +378 -0
  48. package/src/games/tetris/engine.ts +236 -0
  49. package/src/games/tetris/tetris.ts +220 -0
  50. package/src/index.ts +82 -0
  51. package/src/recording/chunks.ts +93 -0
  52. package/src/recording/config.test.ts +31 -0
  53. package/src/recording/config.ts +37 -0
  54. package/src/recording/contracts.test.ts +71 -0
  55. package/src/recording/gameplay.test.ts +70 -0
  56. package/src/recording/gameplay.ts +18 -0
  57. package/src/recording/http-transport.test.ts +197 -0
  58. package/src/recording/identity.test.ts +26 -0
  59. package/src/recording/identity.ts +38 -0
  60. package/src/recording/ids.ts +27 -0
  61. package/src/recording/permissions.ts +10 -0
  62. package/src/recording/recorder.test.ts +388 -0
  63. package/src/recording/recorder.ts +464 -0
  64. package/src/recording/store.test.ts +231 -0
  65. package/src/recording/store.ts +305 -0
  66. package/src/recording/terminal-grid.test.ts +88 -0
  67. package/src/recording/terminal-grid.ts +248 -0
  68. package/src/recording/trace-recorder.test.ts +287 -0
  69. package/src/recording/trace-recorder.ts +560 -0
  70. package/src/recording/transport.ts +170 -0
  71. package/src/recording/types.test.ts +18 -0
  72. package/src/recording/types.ts +326 -0
  73. package/src/recording/uploader.test.ts +193 -0
  74. package/src/recording/uploader.ts +103 -0
  75. package/src/results/completion.ts +100 -0
  76. package/src/results/http-result-transport.test.ts +75 -0
  77. package/src/results/local-score.test.ts +32 -0
  78. package/src/results/local-score.ts +39 -0
  79. package/src/results/results.test.ts +383 -0
  80. package/src/results/store.ts +149 -0
  81. package/src/results/transport.ts +79 -0
  82. package/src/results/types.ts +20 -0
  83. package/src/results/uploader.ts +84 -0
  84. package/src/sync/config.test.ts +108 -0
  85. package/src/sync/config.ts +115 -0
  86. package/src/sync/coordinator.test.ts +423 -0
  87. package/src/sync/coordinator.ts +277 -0
  88. package/src/sync/game-start-queue.test.ts +93 -0
  89. package/src/sync/game-start-queue.ts +45 -0
  90. package/src/sync/identity-client.test.ts +34 -0
  91. package/src/sync/identity-client.ts +43 -0
@@ -0,0 +1,1589 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta name="theme-color" content="#eef2f8" />
7
+ <title>Tui Arcade</title>
8
+ <style>
9
+ :root {
10
+ color-scheme: light;
11
+ /* Warm paper shell — brand colors on boards & accents only */
12
+ --bg-0: #eef2f8;
13
+ --bg-1: #ffffff;
14
+ --bg-2: #ffffff;
15
+ --bg-3: #f3f6fb;
16
+ --line: #e2e8f0;
17
+ --line-strong: #cbd5e1;
18
+ --ink: #0f172a;
19
+ --muted: #64748b;
20
+ --shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.05), 0 1px 3px rgba(15, 23, 42, 0.04);
21
+ --shadow-md: 0 4px 14px rgba(15, 23, 42, 0.08), 0 2px 6px rgba(15, 23, 42, 0.04);
22
+ --shadow-window: 0 20px 50px rgba(15, 23, 42, 0.12), 0 8px 20px rgba(15, 23, 42, 0.06);
23
+
24
+ /* Arcade palette (canonical) */
25
+ --golden-pollen: #ffc857;
26
+ --crayola-blue: #3772ff;
27
+ --scarlet-rush: #df2935;
28
+ --jungle-green: #3bb273;
29
+
30
+ /* Deeper stops for tiles, hovers, and game pieces */
31
+ --golden-pollen-deep: #e6a830;
32
+ --crayola-blue-deep: #2955c9;
33
+ --scarlet-rush-deep: #b81f2a;
34
+ --jungle-green-deep: #2e8f5c;
35
+
36
+ /* Legacy aliases — gameplay CSS still references these names */
37
+ --blue: var(--crayola-blue);
38
+ --blue-deep: var(--crayola-blue-deep);
39
+ --banana: var(--golden-pollen);
40
+ --banana-deep: var(--golden-pollen-deep);
41
+ --pumpkin: var(--jungle-green);
42
+ --pumpkin-deep: var(--jungle-green-deep);
43
+ --cinnabar: var(--scarlet-rush);
44
+ --cinnabar-deep: var(--scarlet-rush-deep);
45
+
46
+ /* Snake reads against a sage field and a red apple, so it sits off the
47
+ arcade palette on purpose — violet clashes with neither. */
48
+ --snake: #6d3bd4;
49
+
50
+ /* roles */
51
+ --accent: var(--golden-pollen);
52
+ --accent-dim: rgba(255, 200, 87, .22);
53
+ --accent-ink: #3d2e00;
54
+ --title-gradient: linear-gradient(
55
+ 118deg,
56
+ var(--crayola-blue-deep) 0%,
57
+ var(--crayola-blue) 38%,
58
+ var(--jungle-green) 72%,
59
+ var(--golden-pollen-deep) 100%
60
+ );
61
+ --danger: var(--scarlet-rush);
62
+ --gold: var(--golden-pollen);
63
+ --success: var(--jungle-green);
64
+ --hover: var(--jungle-green);
65
+ --ink-on-pumpkin: #ffffff;
66
+ --ink-on-blue: #ffffff;
67
+ --ink-on-vivid: #ffffff;
68
+ --radius-s: 6px;
69
+ --radius-m: 10px;
70
+ --radius-l: 16px;
71
+ --window-radius: var(--radius-l);
72
+
73
+ /* Motion tokens — every board animates on the same clock.
74
+ --motion-travel is capped short because Snake ticks every 70ms. */
75
+ --motion-fast: 90ms;
76
+ --motion-travel: 120ms;
77
+ --motion-base: 160ms;
78
+ /* Board-wide gestures (a 2048 move slides many tiles at once) run
79
+ slower than single-piece feedback, or they read as a flicker. */
80
+ --motion-slide: 200ms;
81
+ --motion-slow: 280ms;
82
+ --motion-trail: 340ms;
83
+ --ease-out: cubic-bezier(.22, .68, .35, 1);
84
+ --ease-pop: cubic-bezier(.34, 1.56, .64, 1);
85
+ --ease-in: cubic-bezier(.55, .06, .68, .19);
86
+ --sans: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
87
+ --mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace;
88
+ }
89
+
90
+ * { box-sizing: border-box; }
91
+ html, body {
92
+ width: 100%;
93
+ height: 100%;
94
+ background: transparent;
95
+ }
96
+ body {
97
+ margin: 0;
98
+ overflow: hidden;
99
+ color: var(--ink);
100
+ font-family: var(--sans);
101
+ user-select: none;
102
+ -webkit-font-smoothing: antialiased;
103
+ }
104
+
105
+ button { font: inherit; }
106
+ button:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }
107
+
108
+ /* Flex column: hidden children (update banner) drop out cleanly. */
109
+ .app {
110
+ height: 100%;
111
+ display: flex;
112
+ flex-direction: column;
113
+ background: var(--bg-1);
114
+ border: 1px solid rgba(255, 255, 255, 0.65);
115
+ border-radius: var(--window-radius);
116
+ overflow: hidden;
117
+ box-shadow: var(--shadow-window);
118
+ }
119
+ .app > .titlebar, .app > .update-banner { flex: 0 0 auto; }
120
+ .app > main { flex: 1 1 auto; }
121
+
122
+ /* ── Titlebar ─────────────────────────────────────────────── */
123
+ .titlebar {
124
+ min-height: 48px;
125
+ padding: 8px 10px 8px 16px;
126
+ border-bottom: 1px solid var(--line);
127
+ background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
128
+ display: flex;
129
+ align-items: center;
130
+ gap: 10px;
131
+ -webkit-app-region: drag;
132
+ }
133
+
134
+ .window-actions { margin-left: auto; display: flex; gap: 6px; }
135
+ .window-actions, .window-actions button { -webkit-app-region: no-drag; }
136
+ .window-actions button {
137
+ min-width: 32px;
138
+ height: 28px;
139
+ padding: 0 9px;
140
+ border: 1px solid transparent;
141
+ border-radius: var(--radius-s);
142
+ background: transparent;
143
+ color: var(--muted);
144
+ cursor: pointer;
145
+ font-size: 13px;
146
+ transition: background .12s ease, color .12s ease;
147
+ }
148
+ .window-actions button:hover,
149
+ .window-actions button:focus-visible {
150
+ background: var(--bg-3);
151
+ color: var(--ink);
152
+ outline: none;
153
+ }
154
+ #shutdown { color: rgba(223, 41, 53, .55); }
155
+ #shutdown:hover, #shutdown:focus-visible { background: rgba(223, 41, 53, .14); color: var(--danger); }
156
+
157
+ /* ── Update banner ────────────────────────────────────────── */
158
+ .update-banner {
159
+ display: flex;
160
+ align-items: center;
161
+ justify-content: space-between;
162
+ gap: 12px;
163
+ padding: 10px 14px;
164
+ border-bottom: 1px solid var(--line);
165
+ background: #fffbeb;
166
+ }
167
+ .update-banner[hidden] { display: none !important; }
168
+ .update-copy { min-width: 0; }
169
+ .update-label {
170
+ margin: 0 0 2px;
171
+ font: 600 11px/1.2 var(--sans);
172
+ letter-spacing: .08em;
173
+ text-transform: uppercase;
174
+ color: var(--muted);
175
+ }
176
+ .update-versions {
177
+ margin: 0;
178
+ font: 500 13px/1.3 var(--mono);
179
+ color: var(--ink);
180
+ }
181
+ .update-versions #update-to { color: var(--crayola-blue-deep); }
182
+ .update-button {
183
+ flex: 0 0 auto;
184
+ height: 30px;
185
+ padding: 0 12px;
186
+ border: 1px solid var(--golden-pollen-deep);
187
+ border-radius: var(--radius-s);
188
+ background: var(--golden-pollen);
189
+ color: var(--accent-ink);
190
+ cursor: pointer;
191
+ font: 600 12px/1 var(--sans);
192
+ transition: background .12s ease, border-color .12s ease, box-shadow .12s ease;
193
+ box-shadow: var(--shadow-sm);
194
+ }
195
+ .update-button:hover:not(:disabled),
196
+ .update-button:focus-visible:not(:disabled) {
197
+ background: var(--golden-pollen-deep);
198
+ border-color: var(--golden-pollen-deep);
199
+ outline: none;
200
+ }
201
+ .update-button:disabled { opacity: .55; cursor: default; }
202
+ .update-status {
203
+ margin: 6px 0 0;
204
+ font-size: 12px;
205
+ line-height: 1.35;
206
+ color: var(--muted);
207
+ }
208
+ .update-status[data-state="error"] { color: var(--scarlet-rush); }
209
+ .update-status[data-state="done"] { color: var(--blue); }
210
+ .update-banner[data-state="running"] .update-button { opacity: .7; }
211
+
212
+ /* ── Account chip (titlebar, top-left, persistent) ────────── */
213
+ .account-chip {
214
+ -webkit-app-region: no-drag;
215
+ display: inline-flex;
216
+ align-items: center;
217
+ gap: 7px;
218
+ max-width: 220px;
219
+ height: 28px;
220
+ padding: 0 10px;
221
+ border: 1px solid var(--line);
222
+ border-radius: 999px;
223
+ background: var(--bg-1);
224
+ color: var(--ink);
225
+ cursor: pointer;
226
+ font: 600 12px/1 var(--sans);
227
+ box-shadow: var(--shadow-sm);
228
+ transition: border-color .12s ease, background .12s ease, box-shadow .12s ease;
229
+ }
230
+ .account-chip[hidden] { display: none !important; }
231
+ .account-chip:hover, .account-chip:focus-visible {
232
+ border-color: var(--crayola-blue);
233
+ background: var(--bg-3);
234
+ box-shadow: var(--shadow-md);
235
+ outline: none;
236
+ }
237
+ .account-chip .account-dot {
238
+ width: 7px;
239
+ height: 7px;
240
+ flex: 0 0 auto;
241
+ border-radius: 50%;
242
+ background: var(--muted);
243
+ }
244
+ .account-chip[data-state="in"] .account-dot { background: var(--crayola-blue); }
245
+ .account-chip[data-state="out"] .account-dot { background: var(--golden-pollen); }
246
+ .account-chip[data-sync="error"] {
247
+ border-color: var(--scarlet-rush);
248
+ }
249
+ .account-chip[data-sync="error"] .account-dot {
250
+ background: var(--scarlet-rush);
251
+ }
252
+ .account-chip .account-email {
253
+ min-width: 0;
254
+ overflow: hidden;
255
+ text-overflow: ellipsis;
256
+ white-space: nowrap;
257
+ }
258
+ .account-chip[data-state="out"] .account-email { color: var(--muted); }
259
+ .account-chip[data-sync="error"] .account-email { color: var(--scarlet-rush); }
260
+
261
+ /* ── Welcome gate (signed-out landing) ────────────────────── */
262
+ #screen[data-screen="gate"] {
263
+ display: flex;
264
+ align-items: center;
265
+ justify-content: center;
266
+ }
267
+ .gate-screen {
268
+ width: min(100%, 380px);
269
+ margin: 0 auto;
270
+ display: flex;
271
+ flex-direction: column;
272
+ align-items: center;
273
+ text-align: center;
274
+ }
275
+ .gate-heading {
276
+ margin: 0 0 14px;
277
+ justify-content: center;
278
+ }
279
+ .gate-heading .menu-heading p,
280
+ .menu-heading p {
281
+ color: var(--crayola-blue-deep);
282
+ font-weight: 600;
283
+ letter-spacing: .14em;
284
+ }
285
+ .gate-screen .menu-heading h1,
286
+ .menu-heading h1 {
287
+ background: var(--title-gradient);
288
+ -webkit-background-clip: text;
289
+ background-clip: text;
290
+ color: transparent;
291
+ }
292
+ .gate-welcome {
293
+ margin: 0 0 24px;
294
+ max-width: 28ch;
295
+ }
296
+ .gate-welcome p {
297
+ margin: 0;
298
+ color: var(--muted);
299
+ font-size: 14px;
300
+ line-height: 1.6;
301
+ }
302
+ .gate-actions {
303
+ display: flex;
304
+ flex-direction: column;
305
+ align-items: center;
306
+ gap: 10px;
307
+ width: 100%;
308
+ }
309
+ .auth-signin-status {
310
+ min-width: 0;
311
+ max-width: 30ch;
312
+ color: var(--muted);
313
+ font-size: 12px;
314
+ line-height: 1.4;
315
+ text-align: center;
316
+ }
317
+ .auth-signin-status:empty { display: none; }
318
+ .auth-signin-status[data-state="error"] { color: var(--scarlet-rush); }
319
+ .auth-signin-button {
320
+ width: min(100%, 240px);
321
+ height: 38px;
322
+ padding: 0 24px;
323
+ border: 1px solid var(--golden-pollen-deep);
324
+ border-radius: 999px;
325
+ background: var(--accent);
326
+ color: var(--accent-ink);
327
+ cursor: pointer;
328
+ font: 700 13px/1 var(--sans);
329
+ box-shadow: var(--shadow-md);
330
+ transition: background .12s ease, border-color .12s ease, opacity .12s ease, transform .12s ease;
331
+ }
332
+ .auth-signin-button:hover:not(:disabled),
333
+ .auth-signin-button:focus-visible:not(:disabled) {
334
+ background: var(--banana-deep);
335
+ border-color: var(--banana-deep);
336
+ transform: translateY(-1px);
337
+ outline: none;
338
+ }
339
+ .auth-signin-button:disabled { opacity: .6; cursor: default; }
340
+ .auth-signin-cancel {
341
+ width: auto;
342
+ min-width: 128px;
343
+ height: 30px;
344
+ padding: 0 18px;
345
+ border: 1px solid transparent;
346
+ border-radius: var(--radius-s);
347
+ background: transparent;
348
+ color: var(--muted);
349
+ cursor: pointer;
350
+ font: 600 12px/1 var(--sans);
351
+ transition: background .12s ease, color .12s ease;
352
+ }
353
+ .auth-signin-cancel[hidden] { display: none !important; }
354
+ .auth-signin-cancel:hover, .auth-signin-cancel:focus-visible {
355
+ background: var(--bg-3);
356
+ color: var(--ink);
357
+ outline: none;
358
+ }
359
+
360
+ /* ── Screen ───────────────────────────────────────────────── */
361
+ main {
362
+ min-height: 0;
363
+ overflow: hidden;
364
+ padding: clamp(12px, 2vh, 18px);
365
+ background:
366
+ radial-gradient(ellipse 120% 80% at 50% -20%, rgba(55, 114, 255, 0.07) 0%, transparent 55%),
367
+ radial-gradient(ellipse 80% 50% at 100% 100%, rgba(59, 178, 115, 0.05) 0%, transparent 50%),
368
+ var(--bg-0);
369
+ }
370
+
371
+ #screen {
372
+ width: 100%;
373
+ height: 100%;
374
+ display: grid;
375
+ align-content: center;
376
+ }
377
+ #screen[data-screen="game"] {
378
+ align-content: stretch;
379
+ grid-template-rows: auto minmax(0, 1fr);
380
+ min-height: 0;
381
+ }
382
+ #screen[data-screen="game"][data-game="connect4"] {
383
+ padding-top: 2px;
384
+ }
385
+ #screen[data-screen="game"][data-game="connect4"] .playing-heading {
386
+ margin-bottom: 8px;
387
+ }
388
+ /* #screen otherwise centers its two rows (heading, game) as one block.
389
+ For most games the board is a fixed size, so that just centers a
390
+ small object inside a big frame — most of the window's height goes
391
+ unused above and below rather than to the game. Stretching here, and
392
+ letting .game-snake (below) spread hud/board/controls across the
393
+ full row instead of clumping them in the middle, hands that space
394
+ back so the board can grow into it. */
395
+ #screen[data-screen="game"][data-game="snake"] {
396
+ align-content: stretch;
397
+ grid-template-rows: auto minmax(0, 1fr);
398
+ height: 100%;
399
+ min-height: 0;
400
+ }
401
+ /* .game already sets display:grid — this only adds the properties that
402
+ make the row spread across .game-snake's now-tall box rather than
403
+ collapse to the top of it, which needs no display/property fight. */
404
+ .game-snake {
405
+ height: 100%;
406
+ min-height: 0;
407
+ align-content: space-evenly;
408
+ }
409
+
410
+ /* ── Menu ─────────────────────────────────────────────────── */
411
+ .menu-heading { margin: 0 4px 14px; display: flex; align-items: flex-start; gap: 10px; }
412
+ .inline-back {
413
+ height: 32px;
414
+ padding: 0 10px 0 6px;
415
+ border: 1px solid transparent;
416
+ border-radius: var(--radius-s);
417
+ background: transparent;
418
+ color: var(--muted);
419
+ cursor: pointer;
420
+ font-size: 14px;
421
+ font-weight: 600;
422
+ transition: background .12s ease, color .12s ease;
423
+ margin-top: -4px;
424
+ }
425
+ .inline-back:hover,
426
+ .inline-back:focus-visible {
427
+ background: var(--bg-2);
428
+ color: var(--ink);
429
+ outline: none;
430
+ }
431
+ .menu-heading p {
432
+ margin: 0 0 5px;
433
+ font: 600 11px/1 var(--mono);
434
+ text-transform: uppercase;
435
+ }
436
+ .menu-heading h1 {
437
+ margin: 0;
438
+ font-size: clamp(25px, 5.5vw, 32px);
439
+ font-weight: 800;
440
+ letter-spacing: -.04em;
441
+ line-height: 1.05;
442
+ }
443
+ .game-list { display: grid; gap: 8px; }
444
+ .game-card, .variation-card {
445
+ width: 100%;
446
+ min-height: 56px;
447
+ border: 1px solid var(--line);
448
+ border-radius: var(--radius-m);
449
+ background: var(--bg-2);
450
+ color: var(--ink);
451
+ display: grid;
452
+ align-items: center;
453
+ gap: 10px;
454
+ padding: 9px 12px;
455
+ text-align: left;
456
+ cursor: pointer;
457
+ box-shadow: var(--shadow-sm);
458
+ transition: border-color .15s ease, background .15s ease, box-shadow .15s ease, transform .15s ease;
459
+ }
460
+ .game-card { grid-template-columns: 36px 1fr auto; }
461
+ .game-card:hover, .game-card:focus-visible, .game-card.selected,
462
+ .variation-card:hover, .variation-card:focus-visible, .variation-card.selected {
463
+ border-color: rgba(55, 114, 255, 0.45);
464
+ background: var(--bg-1);
465
+ box-shadow: var(--shadow-md);
466
+ transform: translateY(-1px);
467
+ outline: none;
468
+ }
469
+ .game-card strong, .variation-card strong { display: block; font-size: 14px; font-weight: 650; letter-spacing: -.01em; }
470
+ .game-card small, .variation-card small { display: block; margin-top: 2px; color: var(--muted); font-size: 12px; line-height: 1.3; }
471
+ .game-card > b {
472
+ color: var(--muted);
473
+ font: 600 10px/1 var(--mono);
474
+ letter-spacing: .1em;
475
+ text-transform: uppercase;
476
+ }
477
+ .game-card:hover > b, .game-card.selected > b { color: var(--crayola-blue); }
478
+ .game-icon {
479
+ width: 36px;
480
+ aspect-ratio: 1;
481
+ border-radius: var(--radius-s);
482
+ display: grid;
483
+ place-items: center;
484
+ background: var(--bg-3);
485
+ box-shadow: inset 0 0 0 1px rgba(15, 23, 42, 0.04);
486
+ }
487
+ .game-icon { background-repeat: no-repeat; background-position: center; background-size: 22px 22px; }
488
+ .icon-2048 { background-color: rgba(223, 41, 53, .16); background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><rect x="1" y="1" width="8" height="8" rx="1.5" fill="%23df2935" opacity="0.35"/><rect x="11" y="1" width="8" height="8" rx="1.5" fill="%23df2935" opacity="0.6"/><rect x="1" y="11" width="8" height="8" rx="1.5" fill="%23df2935" opacity="0.6"/><rect x="11" y="11" width="8" height="8" rx="1.5" fill="%23df2935"/></svg>'); }
489
+ .icon-tetris { background-color: rgba(55, 114, 255, .16); background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><rect x="7" y="3" width="6" height="6" rx="1" fill="%233772ff"/><rect x="0" y="10" width="6" height="6" rx="1" fill="%233772ff"/><rect x="7" y="10" width="6" height="6" rx="1" fill="%233772ff"/><rect x="14" y="10" width="6" height="6" rx="1" fill="%233772ff"/></svg>'); }
490
+ .icon-minesweeper { background-color: rgba(59, 178, 115, .16); background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><g stroke="%233bb273" stroke-width="1.6" stroke-linecap="round"><line x1="10" y1="1" x2="10" y2="5"/><line x1="10" y1="15" x2="10" y2="19"/><line x1="1" y1="10" x2="5" y2="10"/><line x1="15" y1="10" x2="19" y2="10"/><line x1="3.5" y1="3.5" x2="6.3" y2="6.3"/><line x1="13.7" y1="13.7" x2="16.5" y2="16.5"/><line x1="16.5" y1="3.5" x2="13.7" y2="6.3"/><line x1="6.3" y1="13.7" x2="3.5" y2="16.5"/></g><circle cx="10" cy="10" r="4.5" fill="%233bb273"/><circle cx="8.4" cy="8.4" r="1" fill="%232e2a00"/></svg>'); }
491
+ .icon-snake { background-color: rgba(255, 200, 87, .16); background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M3 15 C3 15 3 11 7 11 C11 11 11 7 15 7 C17.5 7 17.5 4.5 17.5 4.5" fill="none" stroke="%23ffc857" stroke-width="3" stroke-linecap="round"/><circle cx="3" cy="15" r="2.2" fill="%23ffc857"/></svg>'); }
492
+ .icon-sokoban { background-color: rgba(59, 178, 115, .16); background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><rect x="2.5" y="2.5" width="15" height="15" rx="1.5" fill="none" stroke="%233bb273" stroke-width="1.8"/><path d="M2.5 2.5 L17.5 17.5 M17.5 2.5 L2.5 17.5" stroke="%233bb273" stroke-width="1.4"/></svg>'); }
493
+ .icon-connect4 { background-color: rgba(223, 41, 53, .16); background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><rect x="1" y="1" width="18" height="18" rx="3" fill="none" stroke="%23df2935" stroke-width="1.6" opacity="0.5"/><circle cx="6.5" cy="6.5" r="2.6" fill="%23df2935" opacity="0.35"/><circle cx="13.5" cy="6.5" r="2.6" fill="%23df2935"/><circle cx="6.5" cy="13.5" r="2.6" fill="%23df2935"/><circle cx="13.5" cy="13.5" r="2.6" fill="%23df2935" opacity="0.35"/></svg>'); }
494
+ .icon-sudoku { background-color: rgba(55, 114, 255, .16); background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><rect x="1" y="1" width="18" height="18" rx="1.5" fill="none" stroke="%233772ff" stroke-width="1.6"/><line x1="7.33" y1="1" x2="7.33" y2="19" stroke="%233772ff" stroke-width="1"/><line x1="12.67" y1="1" x2="12.67" y2="19" stroke="%233772ff" stroke-width="1"/><line x1="1" y1="7.33" x2="19" y2="7.33" stroke="%233772ff" stroke-width="1"/><line x1="1" y1="12.67" x2="19" y2="12.67" stroke="%233772ff" stroke-width="1"/><circle cx="4.2" cy="4.2" r="1.1" fill="%233772ff"/><circle cx="15.8" cy="10" r="1.1" fill="%233772ff"/><circle cx="10" cy="15.8" r="1.1" fill="%233772ff"/></svg>'); }
495
+ .variation-list { display: grid; gap: 10px; }
496
+ .variation-card { grid-template-columns: 1fr auto; min-height: 84px; }
497
+ .variation-card > b {
498
+ max-width: 120px;
499
+ color: var(--jungle-green);
500
+ font: 600 10px/1.4 var(--mono);
501
+ letter-spacing: .06em;
502
+ text-align: right;
503
+ text-transform: uppercase;
504
+ }
505
+ .variation-card > b em {
506
+ display: block;
507
+ margin-top: 6px;
508
+ color: var(--blue);
509
+ font-style: normal;
510
+ }
511
+ .scores-button {
512
+ min-height: 30px;
513
+ padding: 0 10px;
514
+ border: 1px solid var(--line);
515
+ border-radius: var(--radius-s);
516
+ background: var(--bg-2);
517
+ color: var(--blue);
518
+ cursor: pointer;
519
+ font: 700 12px/1 var(--sans);
520
+ white-space: nowrap;
521
+ }
522
+ .scores-button:hover, .scores-button:focus-visible {
523
+ border-color: var(--crayola-blue);
524
+ background: var(--bg-1);
525
+ outline: none;
526
+ }
527
+ .menu-heading .scores-button { margin-left: auto; }
528
+ #screen[data-screen="scores"] { align-content: stretch; overflow: hidden; }
529
+ .scores-menu { width: min(100%, 620px); height: 100%; margin: 0 auto; display: grid; grid-template-rows: auto minmax(0, 1fr); min-height: 0; }
530
+ .scores-menu .menu-heading { margin: 0 4px 12px; align-items: flex-start; }
531
+ .scores-menu .menu-heading p { color: var(--blue); }
532
+ .scores-menu .menu-heading h1 { font-size: clamp(25px, 5.4vw, 34px); }
533
+ .scores-menu .menu-heading div > small { display: block; max-width: 32ch; margin-top: 7px; color: var(--muted); font-size: 11px; line-height: 1.35; }
534
+ .leaderboard-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); grid-auto-rows: minmax(170px, 1fr); gap: 10px; min-height: 0; overflow-y: auto; padding: 2px; scrollbar-color: var(--line-strong) transparent; }
535
+ .leaderboard-card { display: grid; grid-template-rows: auto minmax(0, 1fr); min-width: 0; overflow: hidden; border: 1px solid var(--line); border-radius: var(--radius-m); background: var(--bg-2); box-shadow: var(--shadow-sm); }
536
+ .leaderboard-card > header { display: grid; grid-template-columns: 32px minmax(0, 1fr) auto; align-items: center; gap: 8px; padding: 10px; border-bottom: 1px solid var(--line); background: var(--bg-1); }
537
+ .leaderboard-card .game-icon { width: 32px; }
538
+ .leaderboard-card header strong { display: block; font-size: 13px; }
539
+ .leaderboard-card header small { display: block; margin-top: 2px; color: var(--muted); font: 600 8px/1 var(--mono); letter-spacing: .08em; text-transform: uppercase; }
540
+ .leaderboard-card header > b { color: var(--blue); font: 800 16px/1 var(--mono); font-variant-numeric: tabular-nums; }
541
+ .run-list { display: grid; align-content: start; gap: 1px; min-height: 0; margin: 0; padding: 0; overflow-y: auto; list-style: none; }
542
+ .run-list li { min-height: 42px; padding: 7px 10px; border-bottom: 1px solid var(--line); }
543
+ .run-list li:last-child { border-bottom: 0; }
544
+ .run-list b { display: block; color: var(--ink); font: 800 13px/1 var(--mono); font-variant-numeric: tabular-nums; }
545
+ .run-list small { display: block; margin-top: 4px; overflow: hidden; color: var(--muted); font: 600 9px/1.2 var(--mono); text-overflow: ellipsis; white-space: nowrap; }
546
+
547
+ /* ── Game frame ───────────────────────────────────────────── */
548
+ .playing-heading {
549
+ display: grid;
550
+ grid-template-columns: 1fr auto 1fr;
551
+ align-items: center;
552
+ margin-bottom: 12px;
553
+ }
554
+ .playing-heading > :first-child { justify-self: start; }
555
+ .playing-heading > :nth-child(2) { text-align: center; }
556
+ .playing-heading > :last-child { justify-self: end; }
557
+ .playing-heading .inline-back { margin-top: 0; }
558
+ .playing-heading small {
559
+ display: block;
560
+ color: var(--blue);
561
+ font: 700 10px/1 var(--mono);
562
+ letter-spacing: .18em;
563
+ text-transform: uppercase;
564
+ }
565
+ .playing-heading .personal-best { margin-top: 5px; color: var(--muted); font-size: 9px; letter-spacing: .08em; }
566
+ .playing-heading .personal-best[hidden] { display: none; }
567
+ .playing-heading .mode-title { margin-top: 4px; }
568
+ .playing-heading strong { display: block; font-size: clamp(20px, 4vw, 26px); font-weight: 700; letter-spacing: -.02em; }
569
+ .playing-heading button {
570
+ min-height: 36px;
571
+ padding: 0 12px;
572
+ border: 1px solid var(--line);
573
+ border-radius: var(--radius-s);
574
+ background: transparent;
575
+ color: var(--blue);
576
+ cursor: pointer;
577
+ font-size: 13px;
578
+ transition: border-color .12s ease, color .12s ease;
579
+ }
580
+ .playing-heading button:hover, .playing-heading button:focus-visible {
581
+ border-color: var(--hover);
582
+ color: var(--ink);
583
+ outline: none;
584
+ }
585
+ .game {
586
+ display: grid;
587
+ grid-template-rows: auto minmax(0, 1fr) auto;
588
+ justify-items: center;
589
+ align-items: center;
590
+ gap: 8px;
591
+ width: 100%;
592
+ height: 100%;
593
+ min-height: 0;
594
+ min-width: 0;
595
+ }
596
+ .game-connect4 {
597
+ gap: 8px;
598
+ min-height: 0;
599
+ height: 100%;
600
+ grid-template-rows: auto minmax(0, 1fr);
601
+ align-content: stretch;
602
+ }
603
+ .game-hud strong.outcome-win { color: var(--gold); }
604
+ .game-hud strong.outcome-loss { color: var(--danger); }
605
+ .game-hud strong.outcome-draw { color: var(--blue); }
606
+ .game-tone-win .board-connect { box-shadow: 0 0 0 2px rgba(255, 200, 87, .45); }
607
+ .game-tone-loss .board-connect { box-shadow: 0 0 0 2px rgba(223, 41, 53, .45); }
608
+ .game-tone-draw .board-connect { box-shadow: 0 0 0 2px rgba(55, 114, 255, .45); }
609
+ .game-hud {
610
+ width: min(100%, 540px);
611
+ display: flex;
612
+ justify-content: center;
613
+ gap: 6px;
614
+ }
615
+ .game-hud span {
616
+ min-width: 72px;
617
+ padding: 7px 10px;
618
+ border: 1px solid var(--line);
619
+ border-radius: var(--radius-s);
620
+ background: var(--bg-1);
621
+ box-shadow: var(--shadow-sm);
622
+ text-align: center;
623
+ }
624
+ .game-hud small { display: block; color: var(--muted); font: 600 9px/1 var(--mono); letter-spacing: .08em; text-transform: uppercase; }
625
+ .game-hud strong { display: block; margin-top: 3px; font: 700 14px/1.1 var(--mono); font-variant-numeric: tabular-nums; }
626
+ .board {
627
+ position: relative;
628
+ touch-action: none;
629
+ }
630
+ .controls { display: flex; justify-content: center; gap: 6px; }
631
+ .control {
632
+ min-width: 34px;
633
+ min-height: 34px;
634
+ padding: 0 10px;
635
+ border: 1px solid var(--line);
636
+ border-radius: var(--radius-s);
637
+ background: var(--bg-1);
638
+ color: var(--ink);
639
+ cursor: pointer;
640
+ font-weight: 600;
641
+ font-size: 12px;
642
+ box-shadow: var(--shadow-sm);
643
+ transition: border-color .12s ease, background .12s ease, box-shadow .12s ease, transform .08s ease;
644
+ }
645
+ .control:hover, .control:focus-visible {
646
+ border-color: var(--crayola-blue);
647
+ background: var(--bg-3);
648
+ box-shadow: var(--shadow-md);
649
+ outline: none;
650
+ }
651
+ /* Pressed by click, or by the player's actual arrow/WASD keys — kept
652
+ in its own class so the two triggers can overlap without conflict. */
653
+ .control:active, .control.key-pressed {
654
+ transform: translateY(1px);
655
+ border-color: var(--crayola-blue);
656
+ background: var(--bg-3);
657
+ box-shadow: inset 0 1px 3px rgba(15, 23, 42, .2);
658
+ }
659
+ .direction-pad { display: grid; grid-template-columns: repeat(3, 38px); }
660
+ .direction-pad [data-action="up"] { grid-column: 2; }
661
+ .direction-pad [data-action="left"] { grid-column: 1; grid-row: 2; }
662
+ .direction-pad [data-action="down"] { grid-column: 2; grid-row: 2; }
663
+ .direction-pad [data-action="right"] { grid-column: 3; grid-row: 2; }
664
+ .direction-pad [data-action="undo"] { grid-column: 1 / 4; grid-row: 3; }
665
+ .game-sokoban .direction-pad [data-action="undo"] { grid-column: 3; grid-row: 1; }
666
+
667
+ /* ── Boards ───────────────────────────────────────────────── */
668
+ .board-2048 {
669
+ width: min(100%, 540px, calc(100vh - 240px));
670
+ margin: 0 auto;
671
+ aspect-ratio: 1;
672
+ padding: 10px;
673
+ border-radius: var(--radius-m);
674
+ background: #aab8cc;
675
+ box-shadow: inset 0 2px 8px rgba(15, 23, 42, 0.14);
676
+ display: grid;
677
+ grid-template-columns: repeat(4, 1fr);
678
+ grid-template-rows: repeat(4, 1fr);
679
+ gap: 8px;
680
+ }
681
+ /* Tiles are raised blocks — border plus a top highlight and a cast
682
+ shadow — so a low-value tile can never be mistaken for the board
683
+ showing through. Empty cells are recessed wells instead. */
684
+ .tile {
685
+ min-width: 0;
686
+ aspect-ratio: 1;
687
+ border-radius: var(--radius-s);
688
+ display: grid;
689
+ place-items: center;
690
+ border: 1px solid rgba(15, 23, 42, .16);
691
+ background: var(--bg-1);
692
+ color: var(--ink);
693
+ box-shadow:
694
+ 0 2px 3px rgba(15, 23, 42, .18),
695
+ inset 0 1px 0 rgba(255, 255, 255, .5);
696
+ font: 700 clamp(18px, 6vw, 32px)/1 var(--mono);
697
+ font-variant-numeric: tabular-nums;
698
+ }
699
+ .tile-empty {
700
+ color: transparent;
701
+ border-color: transparent;
702
+ background: rgba(15, 23, 42, .10);
703
+ box-shadow: inset 0 2px 4px rgba(15, 23, 42, .16);
704
+ }
705
+ /* 2 and 4 stay neutral; the ramp then walks the four brand hues, two
706
+ steps each. Every stop carries a darker border of its own family. */
707
+ .tile-2 { background: #ffffff; color: var(--ink); }
708
+ .tile-4 { background: #dfe6f0; color: var(--ink); border-color: rgba(15, 23, 42, .24); }
709
+ .tile-8 { background:var(--blue); color:var(--ink-on-blue); border-color: var(--blue-deep); }
710
+ .tile-16 { background:var(--blue-deep); color:var(--ink-on-blue); border-color: #1e3f96; }
711
+ .tile-32 { background:var(--banana); color:var(--accent-ink); border-color: var(--banana-deep); }
712
+ .tile-64 { background:var(--banana-deep); color:var(--accent-ink); border-color: #b07f18; }
713
+ .tile-128 { background:var(--pumpkin); color:var(--ink-on-pumpkin); border-color: var(--pumpkin-deep); }
714
+ .tile-256 { background:var(--pumpkin-deep); color:var(--ink-on-pumpkin); border-color: #1f6c43; }
715
+ .tile-512 { background:var(--cinnabar); color: var(--ink-on-vivid); border-color: var(--cinnabar-deep); }
716
+ .tile-128, .tile-256, .tile-512 { font-size: clamp(16px, 5.3vw, 28px); }
717
+ .tile-1024, .tile-2048 { background:var(--cinnabar-deep); color: var(--ink-on-vivid); border-color: #8b1720; font-size: clamp(14px, 4.6vw, 24px); }
718
+ /* One move can change a dozen cells at once, so the whole board is kept
719
+ to a single calm gesture: tiles glide, the arrival gives one soft
720
+ swell, and nothing flashes or overshoots. */
721
+ .tile-new {
722
+ animation: tile-spawn var(--motion-slide) var(--ease-out);
723
+ }
724
+ .tile-merged {
725
+ animation: tile-pop var(--motion-slide) var(--ease-out);
726
+ }
727
+ /* Slide travels from the cell the tile occupied before the move, so a
728
+ four-tile shove reads as four tiles crossing the board. */
729
+ .tile-slid {
730
+ animation: tile-slide var(--motion-slide) var(--ease-out);
731
+ z-index: 1;
732
+ }
733
+ @keyframes tile-spawn {
734
+ from { transform: scale(.62); opacity: 0; }
735
+ to { transform: scale(1); opacity: 1; }
736
+ }
737
+ @keyframes tile-pop {
738
+ 0% { transform: scale(1); }
739
+ 45% { transform: scale(1.07); }
740
+ 100% { transform: scale(1); }
741
+ }
742
+ @keyframes tile-slide {
743
+ from { transform: translate(var(--dx, 0px), var(--dy, 0px)); }
744
+ to { transform: translate(0, 0); }
745
+ }
746
+ /* A merged tile also travelled, so the two animations become one rather
747
+ than fighting over `transform`. */
748
+ .tile-slid.tile-merged { animation: tile-slide-merge var(--motion-slide) var(--ease-out); }
749
+ @keyframes tile-slide-merge {
750
+ 0% { transform: translate(var(--dx, 0px), var(--dy, 0px)) scale(1); }
751
+ 72% { transform: translate(0, 0) scale(1.07); }
752
+ 100% { transform: translate(0, 0) scale(1); }
753
+ }
754
+
755
+ .tetris-layout { display: flex; align-items: flex-start; gap: 10px; }
756
+ .board-tetris {
757
+ position: relative;
758
+ height: min(450px, calc(100vh - 260px));
759
+ aspect-ratio: 1 / 2;
760
+ padding: 4px;
761
+ border: 1px solid var(--line-strong);
762
+ border-radius: var(--radius-s);
763
+ background: var(--bg-0);
764
+ display: grid;
765
+ grid-template-columns: repeat(10, 1fr);
766
+ grid-template-rows: repeat(20, 1fr);
767
+ gap: 2px;
768
+ }
769
+ .tetromino { display: block; background: var(--bg-1); border-radius: 2px; }
770
+ .piece-I { background:var(--blue) !important; } .piece-J { background:var(--blue-deep) !important; }
771
+ .piece-O { background:var(--banana) !important; } .piece-T { background:var(--banana-deep) !important; }
772
+ .piece-S { background:var(--pumpkin) !important; } .piece-L { background:var(--pumpkin-deep) !important; }
773
+ .piece-Z { background:var(--cinnabar) !important; }
774
+ .tetromino.ghost { opacity: .25; }
775
+ /* The falling piece is lifted off the stack so you can always tell which
776
+ blocks are still yours to steer. */
777
+ .tetromino.active {
778
+ box-shadow: inset 0 0 0 1.5px rgba(255, 255, 255, .8);
779
+ transition: transform var(--motion-fast) var(--ease-out);
780
+ }
781
+ .tetromino.active.shifted { animation: piece-shift var(--motion-fast) var(--ease-out); }
782
+ .tetromino.active.spun { animation: piece-spin var(--motion-base) var(--ease-pop); }
783
+ .tetromino.locked { animation: piece-lock var(--motion-base) var(--ease-out); }
784
+ @keyframes piece-shift {
785
+ from { transform: translate(calc(var(--dx, 0) * -100%), calc(var(--dy, 0) * -100%)); }
786
+ to { transform: translate(0, 0); }
787
+ }
788
+ @keyframes piece-spin {
789
+ 0% { transform: scale(.72) rotate(calc(var(--spin, 1) * -22deg)); }
790
+ 100% { transform: scale(1) rotate(0deg); }
791
+ }
792
+ @keyframes piece-lock {
793
+ 0% { transform: scale(1.28); filter: brightness(1.9); }
794
+ 100% { transform: scale(1); filter: none; }
795
+ }
796
+ /* Absolutely positioned so the 200 grid cells keep their auto-placement. */
797
+ .tetris-line-flash {
798
+ position: absolute;
799
+ z-index: 2;
800
+ border-radius: 2px;
801
+ background: #ffffff;
802
+ pointer-events: none;
803
+ animation: line-clear var(--motion-slow) var(--ease-in) forwards;
804
+ }
805
+ @keyframes line-clear {
806
+ 0% { opacity: 0; transform: scaleX(1); }
807
+ 35% { opacity: 1; transform: scaleX(1.04); }
808
+ 100% { opacity: 0; transform: scaleX(.2); }
809
+ }
810
+ .next-pieces { display: grid; gap: 7px; width: 54px; }
811
+ .next-pieces small { color: var(--muted); font: 600 9px/1 var(--mono); letter-spacing: .12em; text-transform: uppercase; }
812
+ .next-pieces b { height: 38px; border-radius: var(--radius-s); display: grid; place-items: center; color: var(--ink-on-vivid); font: 700 13px/1 var(--mono); }
813
+ .tetris-controls { max-width: 330px; flex-wrap: wrap; }
814
+
815
+ .board-mines {
816
+ width: min(100%, 540px, calc(100vh - 220px));
817
+ margin: 0 auto;
818
+ aspect-ratio: 1;
819
+ display: grid;
820
+ grid-template-columns: repeat(9, 1fr);
821
+ border: 1px solid var(--line-strong);
822
+ border-radius: var(--radius-s);
823
+ background: var(--bg-0);
824
+ overflow: hidden;
825
+ }
826
+ .mine-cell {
827
+ min-width: 0;
828
+ min-height: 0;
829
+ padding: 0;
830
+ border: 1px solid var(--bg-0);
831
+ background: var(--bg-1);
832
+ color: var(--ink);
833
+ font: 700 clamp(11px, 4vw, 20px)/1 var(--mono);
834
+ cursor: pointer;
835
+ }
836
+ .mine-cell.cell--1, .mine-cell.cell--2 { background: var(--bg-3); box-shadow: inset 0 1px rgba(15, 23, 42, 0.06); }
837
+ .mine-cell.cell--2 { color: var(--gold); }
838
+ .mine-cell.cell--3 { color: var(--danger); background: rgba(223, 41, 53, .16); }
839
+ .mine-cell.cell-1 { color:var(--blue); } .mine-cell.cell-2 { color:var(--banana); }
840
+ .mine-cell.cell-3 { color:var(--pumpkin); } .mine-cell.cell-4 { color:var(--cinnabar); }
841
+ .mine-cell.selected { outline: 2px solid var(--accent); outline-offset: -2px; }
842
+ /* --step is the cell's ring distance from the click, so a cascade opens
843
+ outward from where you actually pressed instead of all at once. */
844
+ .mine-cell.revealing {
845
+ animation: cell-open var(--motion-base) var(--ease-out) backwards;
846
+ animation-delay: calc(var(--step, 0) * 22ms);
847
+ }
848
+ .mine-cell.flagging { animation: flag-plant var(--motion-base) var(--ease-pop); }
849
+ .mine-cell.detonating { animation: mine-blast var(--motion-slow) var(--ease-out); }
850
+ @keyframes cell-open {
851
+ from { transform: scale(.62); opacity: .15; }
852
+ to { transform: scale(1); opacity: 1; }
853
+ }
854
+ @keyframes flag-plant {
855
+ 0% { transform: scale(.6) rotate(-14deg); }
856
+ 100% { transform: scale(1) rotate(0deg); }
857
+ }
858
+ @keyframes mine-blast {
859
+ 0% { transform: scale(.4); }
860
+ 45% { transform: scale(1.5); box-shadow: 0 0 0 8px rgba(223, 41, 53, .35); }
861
+ 100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(223, 41, 53, 0); }
862
+ }
863
+
864
+ /* Sized in CSS, not from a JS-computed px cell: width:100% of whatever
865
+ box the layout hands it, height derived by aspect-ratio. That makes
866
+ the board genuinely fill the space available — including the extra
867
+ room freed by the stretch layout above — rather than being capped at
868
+ a constant guessed in JS. (A fixed px width here would also fall
869
+ into a classic grid pitfall: a grid item's default min-width is
870
+ `auto`, which floors it at its own explicit width and lets it
871
+ overflow the cell even under `max-width: 100%`.) The checker's
872
+ background-size is in percentages of the same cols/rows, so it self-
873
+ aligns to whatever size the board actually renders at. */
874
+ .board-snake {
875
+ width: min(100%, calc(var(--cell, 14px) * var(--cols, 20)));
876
+ height: auto;
877
+ min-width: 0;
878
+ aspect-ratio: var(--cols, 20) / var(--rows, 13);
879
+ margin: 0 auto;
880
+ border-radius: var(--radius-m);
881
+ overflow: hidden;
882
+ box-shadow: var(--shadow-md);
883
+ background: repeating-conic-gradient(#dcead9 0% 25%, #d1e2cd 0% 50%);
884
+ background-size: calc(200% / var(--cols, 20)) calc(200% / var(--rows, 13));
885
+ }
886
+ /* One square per segment, each outlined so the blocks stay countable as
887
+ they step. The stroke sits inside the block (drawn under the fill and
888
+ doubled, the usual SVG trick) so the outline never bleeds into the
889
+ neighbouring cell and every block stays exactly on the grid. */
890
+ .snake-svg { display: block; width: 100%; height: 100%; }
891
+ .snake-seg {
892
+ fill: var(--snake);
893
+ stroke: rgba(255, 255, 255, .8);
894
+ /* Exactly twice the block's inset: the stroke straddles the block edge,
895
+ so half of it lands precisely on the cell boundary and no further.
896
+ Any wider and the outline bleeds into the neighbouring cell. */
897
+ stroke-width: 0.08;
898
+ paint-order: stroke;
899
+ }
900
+ .snake-eye { fill: #ffffff; }
901
+ .snake-food-body { fill: var(--cinnabar); }
902
+ .snake-food-leaf { fill: var(--jungle-green-deep); }
903
+ /* The pulse rides an inner group, never the one carrying the translate.
904
+ An SVG element scales about the viewBox origin, so scaling the outer
905
+ group multiplies its position and walks the apple off its cell; the
906
+ inner group's own origin already sits at the apple's centre, so the
907
+ same scale stays put. */
908
+ .snake-food-pulse { animation: food-pulse 1s infinite alternate ease-in-out; }
909
+ @keyframes food-pulse {
910
+ from { scale: 0.9; }
911
+ to { scale: 1.1; }
912
+ }
913
+ @media (prefers-reduced-motion: reduce) {
914
+ .snake-food-pulse { animation: none; }
915
+ }
916
+
917
+ .board-sokoban {
918
+ display: flex;
919
+ flex-direction: column;
920
+ align-items: center;
921
+ gap: 4px;
922
+ width: fit-content;
923
+ max-width: 100%;
924
+ margin: 0 auto;
925
+ padding: 10px;
926
+ border: 2px solid #4c3d33;
927
+ border-radius: var(--radius-m);
928
+ background: #6a5545;
929
+ box-shadow:
930
+ 0 12px 24px rgba(44, 32, 24, .22),
931
+ inset 0 1px 0 rgba(255, 255, 255, .18);
932
+ }
933
+ .sokoban-row {
934
+ display: flex;
935
+ gap: 4px;
936
+ }
937
+ .sokoban-cell {
938
+ position: relative;
939
+ display: block;
940
+ width: var(--cell, 48px);
941
+ height: var(--cell, 48px);
942
+ flex: none;
943
+ overflow: hidden;
944
+ border-radius: 7px;
945
+ background:
946
+ linear-gradient(135deg, rgba(255,255,255,.2), transparent 45%),
947
+ repeating-conic-gradient(#e9ddc7 0 25%, #dfd0b7 0 50%) 0 0 / 18px 18px;
948
+ box-shadow:
949
+ inset 0 0 0 1px rgba(92, 68, 47, .14),
950
+ inset 0 -2px 4px rgba(92, 68, 47, .08);
951
+ transition: background-color 150ms ease, transform 150ms ease, box-shadow 150ms ease;
952
+ }
953
+ .sokoban-cell.wall {
954
+ border-radius: 5px;
955
+ background:
956
+ linear-gradient(90deg, transparent 48%, rgba(25, 34, 46, .35) 49% 52%, transparent 53%),
957
+ linear-gradient(rgba(255,255,255,.13) 0 2px, transparent 3px),
958
+ linear-gradient(145deg, #667589, #354255);
959
+ background-size: 32px 100%, 100% 50%, 100% 100%;
960
+ box-shadow:
961
+ inset 0 4px 0 rgba(255,255,255,.12),
962
+ inset 0 -5px 0 rgba(18, 27, 39, .28),
963
+ inset 3px 0 0 rgba(255,255,255,.06),
964
+ inset -3px 0 0 rgba(18, 27, 39, .16);
965
+ }
966
+ .sokoban-cell.goal {
967
+ background:
968
+ radial-gradient(circle, #f5b82e 0 9%, transparent 10% 22%, #c88417 23% 29%, transparent 30%),
969
+ linear-gradient(135deg, rgba(255,255,255,.2), transparent 45%),
970
+ repeating-conic-gradient(#e9ddc7 0 25%, #dfd0b7 0 50%) 0 0 / 18px 18px;
971
+ box-shadow:
972
+ inset 0 0 0 1px rgba(92, 68, 47, .14),
973
+ 0 0 12px rgba(245, 184, 46, .24);
974
+ }
975
+ .sokoban-cell.box {
976
+ background:
977
+ linear-gradient(135deg, rgba(255,255,255,.2), transparent 45%),
978
+ repeating-conic-gradient(#e9ddc7 0 25%, #dfd0b7 0 50%) 0 0 / 18px 18px;
979
+ }
980
+ .sokoban-cell.box::before {
981
+ content: "";
982
+ position: absolute;
983
+ inset: 8%;
984
+ border: 3px solid #80451e;
985
+ border-radius: 7px;
986
+ background: linear-gradient(145deg, #f0b45f, #bd6429);
987
+ box-shadow:
988
+ inset 0 3px 0 rgba(255,255,255,.25),
989
+ inset 0 -4px 0 rgba(105, 51, 20, .2),
990
+ 0 4px 6px rgba(70, 39, 20, .24);
991
+ }
992
+ .sokoban-cell.box::after {
993
+ content: "";
994
+ position: absolute;
995
+ inset: 19%;
996
+ background:
997
+ linear-gradient(45deg, transparent 43%, #8f4b20 44% 55%, transparent 56%),
998
+ linear-gradient(-45deg, transparent 43%, #8f4b20 44% 55%, transparent 56%);
999
+ opacity: .8;
1000
+ }
1001
+ .sokoban-cell.box.goal {
1002
+ background:
1003
+ radial-gradient(circle, #f5b82e 0 9%, transparent 10% 22%, #c88417 23% 29%, transparent 30%),
1004
+ linear-gradient(135deg, rgba(255,255,255,.2), transparent 45%),
1005
+ repeating-conic-gradient(#e9ddc7 0 25%, #dfd0b7 0 50%) 0 0 / 18px 18px;
1006
+ box-shadow: 0 0 0 2px #f5b82e, 0 0 16px rgba(245, 184, 46, .5);
1007
+ }
1008
+ .sokoban-cell.box.goal::before {
1009
+ border-color: #8b5a10;
1010
+ background: linear-gradient(145deg, #ffd66f, #d99524);
1011
+ }
1012
+ .sokoban-cell.player {
1013
+ background:
1014
+ linear-gradient(135deg, rgba(255,255,255,.2), transparent 45%),
1015
+ repeating-conic-gradient(#e9ddc7 0 25%, #dfd0b7 0 50%) 0 0 / 18px 18px;
1016
+ }
1017
+ .sokoban-cell.player::before {
1018
+ content: "";
1019
+ position: absolute;
1020
+ inset: 17%;
1021
+ border: 3px solid #173a68;
1022
+ border-radius: 50%;
1023
+ background:
1024
+ radial-gradient(circle at 34% 42%, #172033 0 4%, transparent 5%),
1025
+ radial-gradient(circle at 66% 42%, #172033 0 4%, transparent 5%),
1026
+ linear-gradient(145deg, #58a6ff, #2f68d1);
1027
+ box-shadow:
1028
+ inset 0 4px 0 rgba(255,255,255,.28),
1029
+ 0 4px 7px rgba(23, 58, 104, .28);
1030
+ }
1031
+ .sokoban-cell.player::after {
1032
+ content: "";
1033
+ position: absolute;
1034
+ left: 38%;
1035
+ top: 55%;
1036
+ width: 24%;
1037
+ height: 12%;
1038
+ border-bottom: 3px solid #172033;
1039
+ border-radius: 50%;
1040
+ }
1041
+ .sokoban-cell.player.goal {
1042
+ background:
1043
+ radial-gradient(circle, #f5b82e 0 9%, transparent 10% 22%, #c88417 23% 29%, transparent 30%),
1044
+ linear-gradient(135deg, rgba(255,255,255,.2), transparent 45%),
1045
+ repeating-conic-gradient(#e9ddc7 0 25%, #dfd0b7 0 50%) 0 0 / 18px 18px;
1046
+ }
1047
+ @media (max-height: 650px) {
1048
+ .sokoban-cell {
1049
+ width: var(--small-cell, var(--cell, 48px));
1050
+ height: var(--small-cell, var(--cell, 48px));
1051
+ }
1052
+ }
1053
+ /* Player and crate slide in from the cell they came from (--dx/--dy is
1054
+ the step), so a push reads as one shove instead of two repaints. */
1055
+ .sokoban-cell.stepped { animation: sokoban-step var(--motion-base) var(--ease-out); z-index: 1; }
1056
+ @keyframes sokoban-step {
1057
+ from { transform: translate(calc(var(--dx, 0) * -110%), calc(var(--dy, 0) * -110%)); }
1058
+ to { transform: translate(0, 0); }
1059
+ }
1060
+ .sokoban-cell.pushed { animation: sokoban-push var(--motion-base) var(--ease-out); z-index: 2; }
1061
+ @keyframes sokoban-push {
1062
+ 0% { transform: translate(calc(var(--dx, 0) * -110%), calc(var(--dy, 0) * -110%)) scale(.92); }
1063
+ 70% { transform: translate(0, 0) scale(1.06); }
1064
+ 100% { transform: translate(0, 0) scale(1); }
1065
+ }
1066
+ .sokoban-cell.snapped { animation: goal-snap var(--motion-slow) var(--ease-pop); z-index: 3; }
1067
+ @keyframes goal-snap {
1068
+ 0% { transform: scale(1); box-shadow: inset 0 0 0 2px var(--banana-deep); }
1069
+ 40% { transform: scale(1.16); box-shadow: 0 0 0 6px rgba(255, 200, 87, .5); }
1070
+ 100% { transform: scale(1); box-shadow: inset 0 0 0 2px var(--banana-deep); }
1071
+ }
1072
+
1073
+ .connect-wrap {
1074
+ width: var(--connect-board-width, min(100%, 540px));
1075
+ max-width: 100%;
1076
+ min-height: 0;
1077
+ height: 100%;
1078
+ margin: 0 auto;
1079
+ display: grid;
1080
+ grid-template-rows: auto minmax(0, 1fr);
1081
+ gap: 8px;
1082
+ align-content: stretch;
1083
+ }
1084
+ .board-connect {
1085
+ width: 100%;
1086
+ max-height: 100%;
1087
+ aspect-ratio: 7 / 6.2;
1088
+ padding: 10px;
1089
+ display: grid;
1090
+ grid-template-columns: repeat(7, 1fr);
1091
+ gap: 6px;
1092
+ border-radius: var(--radius-m);
1093
+ background: var(--blue-deep);
1094
+ align-self: center;
1095
+ justify-self: center;
1096
+ min-height: 0;
1097
+ }
1098
+ .connect-col {
1099
+ display: grid;
1100
+ grid-template-rows: minmax(0, 0.16fr) repeat(6, minmax(0, 1fr));
1101
+ gap: 6px;
1102
+ min-width: 0;
1103
+ min-height: 0;
1104
+ padding: 0;
1105
+ border: 0;
1106
+ border-radius: var(--radius-s);
1107
+ background: transparent;
1108
+ cursor: pointer;
1109
+ color: inherit;
1110
+ font: inherit;
1111
+ }
1112
+ .connect-col .drop-hint {
1113
+ display: block;
1114
+ min-height: 0;
1115
+ align-self: end;
1116
+ color: transparent;
1117
+ font-size: clamp(9px, 2.2vw, 11px);
1118
+ line-height: 1;
1119
+ transition: color .12s ease;
1120
+ }
1121
+ .connect-col:hover .drop-hint,
1122
+ .connect-col.selected .drop-hint,
1123
+ .connect-col:focus-visible .drop-hint {
1124
+ color: var(--accent);
1125
+ }
1126
+ .connect-col.full .drop-hint { color: transparent !important; }
1127
+ .connect-col.full { cursor: not-allowed; }
1128
+ .connect-col.shake { animation: column-shake 420ms ease; }
1129
+ .connect-col.column-full .drop-hint + .disc {
1130
+ box-shadow: inset 0 0 0 2px rgba(223, 41, 53, .65);
1131
+ }
1132
+ @keyframes column-shake {
1133
+ 0%, 100% { transform: translateX(0); }
1134
+ 25% { transform: translateX(-3px); }
1135
+ 75% { transform: translateX(3px); }
1136
+ }
1137
+ .disc { display: block; aspect-ratio: 1; border-radius: 50%; background: var(--line-strong); box-shadow: inset 0 3px 6px rgba(26, 35, 50, .22); }
1138
+ .disc-1 { background: var(--banana); box-shadow: inset 0 3px rgba(255,255,255,.3); }
1139
+ .disc-2 { background: var(--danger); box-shadow: inset 0 3px rgba(255,255,255,.25); }
1140
+ .disc { position: relative; }
1141
+ .disc.falling {
1142
+ animation: drop-in 450ms cubic-bezier(0.5, 0.05, 0.4, 1.2) forwards;
1143
+ }
1144
+ .disc.preview {
1145
+ background: rgba(255, 200, 87, 0.35);
1146
+ box-shadow: none;
1147
+ }
1148
+ @keyframes drop-in {
1149
+ from { transform: translateY(calc(-1 * var(--row) * (100% + 4px))); }
1150
+ to { transform: translateY(0); }
1151
+ }
1152
+ /* Ring on the most recent disc — the board otherwise gives no clue which
1153
+ of the two colours just moved, especially after the bot replies. */
1154
+ .disc.landed::after {
1155
+ content: "";
1156
+ position: absolute;
1157
+ inset: -2px;
1158
+ border-radius: 50%;
1159
+ border: 2px solid var(--ink);
1160
+ opacity: .55;
1161
+ animation: disc-mark 900ms var(--ease-out) forwards;
1162
+ }
1163
+ @keyframes disc-mark {
1164
+ 0%, 60% { opacity: .55; transform: scale(1); }
1165
+ 100% { opacity: 0; transform: scale(1.25); }
1166
+ }
1167
+ .hud-disc { display: inline-block; width: 13px; height: 13px; border-radius: 50%; vertical-align: -2px; }
1168
+
1169
+ .board-sudoku {
1170
+ width: min(100%, 540px, calc(100vh - 240px));
1171
+ aspect-ratio: 1;
1172
+ display: grid;
1173
+ grid-template-columns: repeat(9, 1fr);
1174
+ overflow: hidden;
1175
+ border: 2px solid var(--line-strong);
1176
+ border-radius: var(--radius-s);
1177
+ background: var(--line-strong);
1178
+ }
1179
+ .sudoku-cell {
1180
+ min-width: 0;
1181
+ min-height: 0;
1182
+ padding: 0;
1183
+ border: 0;
1184
+ border-right: 1px solid var(--line);
1185
+ border-bottom: 1px solid var(--line);
1186
+ background: var(--bg-1);
1187
+ color: var(--blue);
1188
+ font: 600 clamp(14px, 5vw, 28px)/1 var(--mono);
1189
+ font-variant-numeric: tabular-nums;
1190
+ cursor: pointer;
1191
+ }
1192
+ .sudoku-cell:nth-child(3n) { border-right: 2px solid var(--line-strong); }
1193
+ .sudoku-cell:nth-child(9n) { border-right: 0; }
1194
+ .sudoku-cell:nth-child(n+19):nth-child(-n+27), .sudoku-cell:nth-child(n+46):nth-child(-n+54) { border-bottom: 2px solid var(--line-strong); }
1195
+ .sudoku-cell.given { color: var(--ink); background: var(--bg-2); cursor: default; }
1196
+ .sudoku-cell.conflict { color: var(--danger); background: rgba(223, 41, 53, .12); }
1197
+ .sudoku-cell.selected { outline: 2px solid var(--accent); outline-offset: -2px; }
1198
+ .sudoku-cell.placed { animation: digit-in var(--motion-base) var(--ease-pop); }
1199
+ .sudoku-cell.erased { animation: digit-out var(--motion-fast) var(--ease-in); }
1200
+ .sudoku-cell.conflict.placed { animation: cell-shake var(--motion-slow) var(--ease-out); }
1201
+ /* --step staggers the sweep across the finished row/column/box. */
1202
+ .sudoku-cell.unit-done {
1203
+ animation: unit-sweep var(--motion-slow) var(--ease-out) backwards;
1204
+ animation-delay: calc(var(--step, 0) * 34ms);
1205
+ }
1206
+ @keyframes digit-in {
1207
+ from { transform: scale(.35); opacity: 0; }
1208
+ to { transform: scale(1); opacity: 1; }
1209
+ }
1210
+ @keyframes digit-out {
1211
+ from { transform: scale(1); opacity: 1; }
1212
+ to { transform: scale(.8); opacity: .35; }
1213
+ }
1214
+ @keyframes cell-shake {
1215
+ 0%, 100% { transform: translateX(0); }
1216
+ 20% { transform: translateX(-3px); }
1217
+ 50% { transform: translateX(3px); }
1218
+ 80% { transform: translateX(-2px); }
1219
+ }
1220
+ @keyframes unit-sweep {
1221
+ 0% { background: var(--jungle-green); color: var(--ink-on-vivid); }
1222
+ 100% { background: var(--bg-1); }
1223
+ }
1224
+ .number-pad { width: min(100%, 330px); display: grid; grid-template-columns: repeat(5, 1fr); }
1225
+ .number-pad .control { min-width: 0; padding: 0; }
1226
+
1227
+ @media (max-height: 700px) {
1228
+ main { padding-block: 8px; }
1229
+ .menu-heading { margin-bottom: 14px; }
1230
+ .playing-heading { margin-bottom: 6px; }
1231
+ #screen[data-screen="game"][data-game="connect4"] { padding-top: 0; }
1232
+ .game { gap: 6px; }
1233
+ .game-connect4 { gap: 4px; }
1234
+ .game-hud span { padding-block: 4px; }
1235
+ .control { min-height: 36px; }
1236
+ }
1237
+
1238
+ /* ── Pause / game-over dialog ─────────────────────────────── */
1239
+ .return-prompt {
1240
+ position: fixed;
1241
+ inset: 0;
1242
+ z-index: 10;
1243
+ display: grid;
1244
+ place-items: center;
1245
+ padding: 28px;
1246
+ background: rgba(15, 23, 42, 0.28);
1247
+ backdrop-filter: blur(6px);
1248
+ }
1249
+ .return-prompt[hidden] { display: none; }
1250
+ .return-card {
1251
+ width: min(100%, 340px);
1252
+ padding: 26px 24px 22px;
1253
+ border: 1px solid var(--line);
1254
+ border-radius: var(--radius-l);
1255
+ background: var(--bg-1);
1256
+ text-align: center;
1257
+ box-shadow: var(--shadow-window);
1258
+ }
1259
+ .return-card h2 {
1260
+ margin: 0 0 20px;
1261
+ font-size: 22px;
1262
+ font-weight: 700;
1263
+ letter-spacing: -.02em;
1264
+ }
1265
+ .return-prompt[data-tone="win"] .return-card h2 { color: var(--gold); }
1266
+ .return-prompt[data-tone="loss"] .return-card h2 { color: var(--danger); }
1267
+ .return-prompt[data-tone="draw"] .return-card h2 { color: var(--blue); }
1268
+ .return-actions { display: grid; gap: 8px; }
1269
+ .return-actions button {
1270
+ min-height: 42px;
1271
+ border: 1px solid var(--line);
1272
+ border-radius: var(--radius-m);
1273
+ background: var(--bg-3);
1274
+ color: var(--ink);
1275
+ cursor: pointer;
1276
+ font-size: 14px;
1277
+ box-shadow: var(--shadow-sm);
1278
+ transition: border-color .12s ease, background .12s ease, box-shadow .12s ease;
1279
+ }
1280
+ .return-actions button:hover, .return-actions button:focus-visible {
1281
+ border-color: var(--hover);
1282
+ outline: none;
1283
+ }
1284
+ .return-actions .primary {
1285
+ background: var(--accent);
1286
+ border-color: var(--accent);
1287
+ color: var(--accent-ink);
1288
+ font-weight: 700;
1289
+ }
1290
+ .return-actions .primary:hover, .return-actions .primary:focus-visible { background: var(--banana-deep); }
1291
+ kbd {
1292
+ display: inline-block;
1293
+ min-width: 20px;
1294
+ padding: 1px 5px;
1295
+ border: 1px solid currentColor;
1296
+ border-radius: 4px;
1297
+ font: 600 11px/1.5 var(--mono);
1298
+ opacity: .85;
1299
+ }
1300
+
1301
+ /* ── Board game-over overlay ─────────────────────────────── */
1302
+ .game-end-overlay {
1303
+ position: absolute;
1304
+ inset: 0;
1305
+ z-index: 5;
1306
+ display: grid;
1307
+ place-items: center;
1308
+ padding: 16px;
1309
+ background: rgba(6, 10, 14, 0.82);
1310
+ backdrop-filter: blur(4px);
1311
+ border-radius: inherit;
1312
+ animation: fade-in 0.25s ease-out;
1313
+ }
1314
+ @keyframes fade-in {
1315
+ from { opacity: 0; }
1316
+ to { opacity: 1; }
1317
+ }
1318
+ .game-end-content {
1319
+ width: 100%;
1320
+ max-width: 280px;
1321
+ text-align: center;
1322
+ }
1323
+ .game-end-title {
1324
+ margin: 0 0 6px;
1325
+ font-size: 24px;
1326
+ font-weight: 800;
1327
+ letter-spacing: -.02em;
1328
+ color: var(--ink);
1329
+ }
1330
+ .game-end-overlay[data-tone="win"] .game-end-title { color: var(--gold); }
1331
+ .game-end-overlay[data-tone="loss"] .game-end-title { color: var(--danger); }
1332
+ .game-end-overlay[data-tone="draw"] .game-end-title { color: var(--blue); }
1333
+
1334
+ .game-end-stats {
1335
+ margin: 0 0 16px;
1336
+ font-size: 13px;
1337
+ font-weight: 500;
1338
+ color: var(--muted);
1339
+ line-height: 1.4;
1340
+ }
1341
+ .game-end-actions {
1342
+ display: grid;
1343
+ gap: 6px;
1344
+ }
1345
+ .game-end-actions button {
1346
+ min-height: 36px;
1347
+ padding: 0 12px;
1348
+ border: 1px solid var(--line-strong);
1349
+ border-radius: var(--radius-s);
1350
+ background: var(--bg-2);
1351
+ color: var(--ink);
1352
+ cursor: pointer;
1353
+ font-size: 13px;
1354
+ font-weight: 600;
1355
+ transition: border-color .12s ease, background .12s ease;
1356
+ }
1357
+ .game-end-actions button:hover, .game-end-actions button:focus-visible {
1358
+ border-color: var(--hover);
1359
+ background: var(--bg-3);
1360
+ outline: none;
1361
+ }
1362
+ .game-end-actions .primary {
1363
+ background: var(--accent);
1364
+ border-color: var(--accent);
1365
+ color: var(--accent-ink);
1366
+ font-weight: 700;
1367
+ }
1368
+ .game-end-actions .primary:hover, .game-end-actions .primary:focus-visible {
1369
+ background: var(--banana-deep);
1370
+ }
1371
+
1372
+ /* ── Minimized handheld pill ──────────────────────────────── */
1373
+ /* Collapsed mode swaps the whole arcade for a palm-sized console
1374
+ silhouette: d-pad, screen, A/B buttons. Drag it anywhere; click to
1375
+ reopen. The window itself is transparent, so only the pill shows. */
1376
+ body[data-mode="pill"] .app,
1377
+ body[data-mode="pill"] #return-prompt,
1378
+ body[data-mode="pill"] #signout-prompt { display: none !important; }
1379
+ body:not([data-mode="pill"]) .pill { display: none !important; }
1380
+
1381
+ .pill {
1382
+ position: fixed;
1383
+ inset: 0;
1384
+ display: flex;
1385
+ align-items: center;
1386
+ gap: 7px;
1387
+ padding: 7px 9px;
1388
+ margin: 0;
1389
+ border: 2px solid transparent;
1390
+ border-radius: 22px;
1391
+ background:
1392
+ linear-gradient(180deg, #1c2028, #0b0d12) padding-box,
1393
+ conic-gradient(from 90deg, #ff5b5b, #ffbf4f, #f7ef61, #5cd888, #55a8ff, #a878ff, #ff70bc, #ff5b5b) border-box;
1394
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, .07);
1395
+ color: #e6eaf2;
1396
+ cursor: grab;
1397
+ -webkit-app-region: drag;
1398
+ }
1399
+ .pill:active { cursor: grabbing; }
1400
+ .pill:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }
1401
+
1402
+ /* D-pad: one horizontal bar crossed by one vertical bar. */
1403
+ .pill-dpad {
1404
+ position: relative;
1405
+ flex: 0 0 auto;
1406
+ width: 18px;
1407
+ height: 18px;
1408
+ }
1409
+ .pill-dpad::before, .pill-dpad::after {
1410
+ content: "";
1411
+ position: absolute;
1412
+ background: #39404e;
1413
+ border-radius: 2px;
1414
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, .08);
1415
+ }
1416
+ .pill-dpad::before { inset: 6px 0; }
1417
+ .pill-dpad::after { inset: 0 6px; }
1418
+
1419
+ /* The little LCD — idle label swaps to "START" on hover. */
1420
+ .pill-screen {
1421
+ position: relative;
1422
+ flex: 1 1 auto;
1423
+ min-width: 0;
1424
+ height: 26px;
1425
+ display: flex;
1426
+ align-items: center;
1427
+ justify-content: center;
1428
+ padding: 0 6px;
1429
+ border: 1px solid #000;
1430
+ border-radius: 5px;
1431
+ background: repeating-linear-gradient(0deg, rgba(255, 255, 255, .06) 0 1px, transparent 1px 3px), #05070a;
1432
+ box-shadow: inset 0 1px 3px rgba(0, 0, 0, .8);
1433
+ overflow: hidden;
1434
+ transition: background .25s ease, box-shadow .25s ease;
1435
+ }
1436
+ .pill:hover .pill-screen {
1437
+ background: repeating-linear-gradient(0deg, rgba(15, 23, 42, .08) 0 1px, transparent 1px 3px), var(--banana);
1438
+ box-shadow: inset 0 1px 2px rgba(15, 23, 42, .12);
1439
+ }
1440
+
1441
+ .pill-label {
1442
+ position: absolute;
1443
+ max-width: calc(100% - 16px);
1444
+ overflow: hidden;
1445
+ text-overflow: ellipsis;
1446
+ white-space: nowrap;
1447
+ font: 600 10px/1 var(--mono);
1448
+ letter-spacing: .12em;
1449
+ text-transform: uppercase;
1450
+ transition: opacity .22s ease;
1451
+ }
1452
+ .pill-label-idle { color: var(--banana); opacity: 1; }
1453
+ .pill-label-hint {
1454
+ color: var(--ink);
1455
+ opacity: 0;
1456
+ letter-spacing: .18em;
1457
+ }
1458
+ .pill:hover .pill-label-idle { opacity: 0; }
1459
+ .pill:hover .pill-label-hint { opacity: 1; }
1460
+
1461
+ /* A/B buttons, offset the way they sit on a real handheld. */
1462
+ .pill-buttons {
1463
+ flex: 0 0 auto;
1464
+ display: flex;
1465
+ align-items: center;
1466
+ gap: 4px;
1467
+ padding-bottom: 4px;
1468
+ }
1469
+ .pill-buttons i {
1470
+ position: relative;
1471
+ width: 9px;
1472
+ height: 9px;
1473
+ border-radius: 50%;
1474
+ box-shadow: inset 0 -1px 1px rgba(0, 0, 0, .4);
1475
+ transition: transform .32s cubic-bezier(.34, 1.45, .64, 1), box-shadow .32s ease;
1476
+ }
1477
+ .pill-buttons i:first-child { background: var(--cinnabar-deep); transform: translateY(5px); }
1478
+ .pill-btn-a {
1479
+ background: var(--pumpkin-deep);
1480
+ transform: translateY(0);
1481
+ }
1482
+ .pill:hover .pill-btn-a {
1483
+ transform: translateY(-2px) scale(1.55);
1484
+ box-shadow: 0 3px 8px rgba(59, 178, 115, .4), inset 0 -1px 1px rgba(0, 0, 0, .35);
1485
+ }
1486
+ .pill-btn-a::after {
1487
+ content: "A";
1488
+ position: absolute;
1489
+ inset: -7px;
1490
+ display: grid;
1491
+ place-items: center;
1492
+ font: 700 7px/1 var(--sans);
1493
+ color: rgba(255, 255, 255, .92);
1494
+ opacity: 0;
1495
+ transform: scale(.4);
1496
+ transition: opacity .2s ease, transform .32s cubic-bezier(.34, 1.45, .64, 1);
1497
+ pointer-events: none;
1498
+ text-shadow: 0 1px 2px rgba(0, 0, 0, .5);
1499
+ }
1500
+ .pill:hover .pill-btn-a::after {
1501
+ opacity: 1;
1502
+ transform: scale(1);
1503
+ }
1504
+
1505
+ @media (prefers-reduced-motion: reduce) {
1506
+ *, *::before, *::after { animation-duration: 0.001ms !important; transition-duration: 0.001ms !important; }
1507
+ /* Cascades and sweeps stagger by index; with motion off the delay only
1508
+ shows as a stall, so collapse it. */
1509
+ *, *::before, *::after { animation-delay: 0s !important; }
1510
+
1511
+ /* Gameplay motion carries "what just changed". With the motion gone the
1512
+ information has to survive somehow, so the same classes paint a static
1513
+ marker instead — app.ts clears them on a timer, not on animationend,
1514
+ so they still disappear on schedule. */
1515
+ .tile-slid, .tile-new, .tile-merged,
1516
+ .tetromino.locked, .tetromino.active.shifted, .tetromino.active.spun,
1517
+ .mine-cell.revealing, .mine-cell.detonating, .mine-cell.flagging,
1518
+ .snake-head.stepped,
1519
+ .sokoban-cell.stepped, .sokoban-cell.pushed, .sokoban-cell.snapped,
1520
+ .sudoku-cell.placed, .sudoku-cell.unit-done {
1521
+ outline: 2px solid var(--accent);
1522
+ outline-offset: -2px;
1523
+ }
1524
+ .disc.landed::after { animation: none; opacity: .55; }
1525
+ /* Purely decorative motion has nothing to preserve. */
1526
+ .snake-trail, .tetris-line-flash, .snake-head.ate::after { display: none; }
1527
+ .snake-food { animation: none; }
1528
+ }
1529
+ </style>
1530
+ </head>
1531
+ <body>
1532
+ <div class="app">
1533
+ <header class="titlebar" data-tauri-drag-region>
1534
+ <button id="account-chip" class="account-chip" type="button" hidden></button>
1535
+ <div class="window-actions">
1536
+ <button id="resize" type="button" aria-label="Expand overlay" title="Expand or compact">↗</button>
1537
+ <button id="hide" type="button" aria-label="Minimize to pill" title="Minimize to a draggable pill">−</button>
1538
+ <button id="shutdown" type="button" aria-label="Shut down for this session" title="Stop opening until the next session">⏻</button>
1539
+ </div>
1540
+ </header>
1541
+
1542
+ <div id="update-banner" class="update-banner" hidden>
1543
+ <div class="update-copy">
1544
+ <p class="update-label">Update available</p>
1545
+ <p class="update-versions"><span id="update-from"></span> → <span id="update-to"></span></p>
1546
+ <p id="update-status" class="update-status"></p>
1547
+ </div>
1548
+ <button id="update-run" type="button" class="update-button">Update</button>
1549
+ </div>
1550
+
1551
+ <main>
1552
+ <div id="screen" aria-live="polite"></div>
1553
+ </main>
1554
+
1555
+ </div>
1556
+
1557
+ <section id="return-prompt" class="return-prompt" role="dialog" aria-modal="true" aria-labelledby="return-title" hidden>
1558
+ <div class="return-card">
1559
+ <h2 id="return-title">Paused</h2>
1560
+ <div class="return-actions">
1561
+ <button id="return-code" class="primary" type="button"><kbd>Enter</kbd> Back to coding</button>
1562
+ <button id="continue-game" type="button">Continue playing</button>
1563
+ <button id="return-menu" type="button"><kbd>M</kbd> Menu</button>
1564
+ </div>
1565
+ </div>
1566
+ </section>
1567
+
1568
+ <section id="signout-prompt" class="return-prompt" role="dialog" aria-modal="true" aria-labelledby="signout-title" hidden>
1569
+ <div class="return-card">
1570
+ <h2 id="signout-title">Sign out of Arcade?</h2>
1571
+ <div class="return-actions">
1572
+ <button id="signout-confirm" class="primary" type="button"><kbd>Enter</kbd> Sign out</button>
1573
+ <button id="signout-cancel" type="button">Cancel</button>
1574
+ </div>
1575
+ </div>
1576
+ </section>
1577
+
1578
+ <button id="pill" class="pill" type="button" aria-label="Reopen the arcade" title="Drag to move · click or press Start (A) to reopen">
1579
+ <span class="pill-dpad" aria-hidden="true"></span>
1580
+ <span class="pill-screen">
1581
+ <span class="pill-label pill-label-idle">Arcade</span>
1582
+ <span class="pill-label pill-label-hint" aria-hidden="true">Start</span>
1583
+ </span>
1584
+ <span class="pill-buttons" aria-hidden="true"><i></i><i class="pill-btn-a"></i></span>
1585
+ </button>
1586
+
1587
+ <script type="module" src="./app.js"></script>
1588
+ </body>
1589
+ </html>