@jjlmoya/utils-tabletop 1.3.0 → 1.4.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.
Files changed (53) hide show
  1. package/package.json +1 -1
  2. package/src/category/TabletopCategorySEO.astro +2 -7
  3. package/src/category/i18n/en.ts +4 -4
  4. package/src/category/i18n/ru.ts +2 -2
  5. package/src/category/i18n/zh.ts +8 -8
  6. package/src/category/index.ts +2 -1
  7. package/src/entries.ts +4 -1
  8. package/src/pages/[locale]/[slug].astro +7 -6
  9. package/src/tests/locale_completeness.test.ts +1 -1
  10. package/src/tests/no_em_dash.test.ts +47 -0
  11. package/src/tests/seo_length.test.ts +1 -0
  12. package/src/tests/title_quality.test.ts +1 -1
  13. package/src/tests/tool_validation.test.ts +1 -1
  14. package/src/tool/board-game-timer/modules/ColorHelper.ts +2 -2
  15. package/src/tool/board-game-timer/modules/DuelViewManager.ts +4 -4
  16. package/src/tool/board-game-timer/modules/GameRunner.ts +1 -1
  17. package/src/tool/board-game-timer/modules/MultiplayerViewManager.ts +2 -2
  18. package/src/tool/board-game-timer/modules/SetupPanelManager.ts +1 -1
  19. package/src/tool/board-game-timer/modules/TimerEngine.ts +1 -1
  20. package/src/tool/board-game-timer/types.ts +1 -1
  21. package/src/tool/dice-roller-simulator/client.ts +2 -2
  22. package/src/tool/dice-roller-simulator/i18n/ru.ts +8 -8
  23. package/src/tool/dice-roller-simulator/modules/history.ts +1 -1
  24. package/src/tool/first-player-selector/bibliography.astro +16 -0
  25. package/src/tool/first-player-selector/bibliography.ts +12 -0
  26. package/src/tool/first-player-selector/client.ts +197 -0
  27. package/src/tool/first-player-selector/component.astro +77 -0
  28. package/src/tool/first-player-selector/entry.ts +42 -0
  29. package/src/tool/first-player-selector/first-player-selector.css +303 -0
  30. package/src/tool/first-player-selector/i18n/de.ts +226 -0
  31. package/src/tool/first-player-selector/i18n/en.ts +226 -0
  32. package/src/tool/first-player-selector/i18n/es.ts +226 -0
  33. package/src/tool/first-player-selector/i18n/fr.ts +227 -0
  34. package/src/tool/first-player-selector/i18n/id.ts +226 -0
  35. package/src/tool/first-player-selector/i18n/it.ts +227 -0
  36. package/src/tool/first-player-selector/i18n/ja.ts +227 -0
  37. package/src/tool/first-player-selector/i18n/ko.ts +227 -0
  38. package/src/tool/first-player-selector/i18n/nl.ts +226 -0
  39. package/src/tool/first-player-selector/i18n/pl.ts +227 -0
  40. package/src/tool/first-player-selector/i18n/pt.ts +226 -0
  41. package/src/tool/first-player-selector/i18n/ru.ts +227 -0
  42. package/src/tool/first-player-selector/i18n/sv.ts +226 -0
  43. package/src/tool/first-player-selector/i18n/tr.ts +226 -0
  44. package/src/tool/first-player-selector/i18n/zh.ts +227 -0
  45. package/src/tool/first-player-selector/index.ts +9 -0
  46. package/src/tool/first-player-selector/modules/AnimationEffects.ts +256 -0
  47. package/src/tool/first-player-selector/modules/SelectionEngine.ts +249 -0
  48. package/src/tool/first-player-selector/modules/SoundEffects.ts +87 -0
  49. package/src/tool/first-player-selector/modules/TouchTracker.ts +148 -0
  50. package/src/tool/first-player-selector/seo.astro +16 -0
  51. package/src/tool/first-player-selector/types.ts +21 -0
  52. package/src/tools.ts +2 -0
  53. package/src/types.ts +1 -2
@@ -0,0 +1,197 @@
1
+ import { TouchTracker } from './modules/TouchTracker';
2
+ import { SelectionEngine } from './modules/SelectionEngine';
3
+ import { AnimationEffects } from './modules/AnimationEffects';
4
+ import { SoundEffects } from './modules/SoundEffects';
5
+ import type { ContactPoint, SelectionState, SelectorMode } from './types';
6
+
7
+ const SOUND_ON_SVG = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 5L6 9H2v6h4l5 4V5z"/><path d="M19.07 4.93a10 10 0 010 14.14M15.54 8.46a5 5 0 010 7.07"/></svg>`;
8
+
9
+ const SOUND_OFF_SVG = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 5L6 9H2v6h4l5 4V5z"/><line x1="23" y1="9" x2="17" y2="15"/><line x1="17" y1="9" x2="23" y2="15"/></svg>`;
10
+
11
+ interface Elements {
12
+ canvas: HTMLCanvasElement;
13
+ ctx: CanvasRenderingContext2D;
14
+ btnModeFinger: HTMLButtonElement;
15
+ btnModeWheel: HTMLButtonElement;
16
+ btnSound: HTMLButtonElement;
17
+ btnReset: HTMLButtonElement;
18
+ btnSpin: HTMLButtonElement;
19
+ btnClear: HTMLButtonElement;
20
+ wheelActions: HTMLElement;
21
+ instructionsText: HTMLElement;
22
+ instructionsStatus: HTMLElement;
23
+ }
24
+
25
+ function getElements(): { els: Elements; ui: Record<string, string> } | null {
26
+ const container = document.querySelector('.selector-container[data-tool="first-player-selector"]') as HTMLElement;
27
+ if (!container) return null;
28
+ const canvas = document.getElementById('selector-canvas') as HTMLCanvasElement;
29
+ const ctx = canvas.getContext('2d');
30
+ if (!ctx) return null;
31
+ return {
32
+ els: {
33
+ canvas, ctx,
34
+ btnModeFinger: document.getElementById('btn-mode-finger') as HTMLButtonElement,
35
+ btnModeWheel: document.getElementById('btn-mode-wheel') as HTMLButtonElement,
36
+ btnSound: document.getElementById('btn-sound') as HTMLButtonElement,
37
+ btnReset: document.getElementById('btn-reset') as HTMLButtonElement,
38
+ btnSpin: document.getElementById('btn-spin') as HTMLButtonElement,
39
+ btnClear: document.getElementById('btn-clear') as HTMLButtonElement,
40
+ wheelActions: document.getElementById('wheel-actions') as HTMLElement,
41
+ instructionsText: document.getElementById('instructions-text') as HTMLElement,
42
+ instructionsStatus: document.getElementById('instructions-status') as HTMLElement,
43
+ },
44
+ ui: JSON.parse(container.getAttribute('data-ui') || '{}'),
45
+ };
46
+ }
47
+
48
+ function handleStateChange(state: SelectionState, els: Elements, ui: Record<string, string>): void {
49
+ if (state === 'idle') {
50
+ els.instructionsText.style.display = 'block';
51
+ els.instructionsText.textContent = ui.instructions;
52
+ els.instructionsStatus.textContent = '';
53
+ els.instructionsStatus.className = 'instructions-status';
54
+ } else if (state === 'countdown') {
55
+ els.instructionsText.style.display = 'none';
56
+ els.instructionsStatus.textContent = '';
57
+ els.instructionsStatus.className = 'instructions-status countdown';
58
+ } else if (state === 'selecting') {
59
+ els.instructionsText.style.display = 'none';
60
+ els.instructionsStatus.textContent = '';
61
+ els.instructionsStatus.className = 'instructions-status';
62
+ } else if (state === 'celebration') {
63
+ els.instructionsText.style.display = 'none';
64
+ els.instructionsStatus.textContent = ui.winner;
65
+ els.instructionsStatus.className = 'instructions-status celebration';
66
+ }
67
+ }
68
+
69
+ function handleWinner(winner: ContactPoint, sound: SoundEffects, effects: AnimationEffects): void {
70
+ sound.playWinner();
71
+ effects.spawnCelebration(winner.x, winner.y, winner.color);
72
+ }
73
+
74
+ function updateMode(mode: SelectorMode, tracker: TouchTracker, engine: SelectionEngine, els: Elements): void {
75
+ tracker.setMode(mode);
76
+ engine.setMode(mode);
77
+
78
+ if (mode === 'finger') {
79
+ els.btnModeFinger.classList.add('active');
80
+ els.btnModeWheel.classList.remove('active');
81
+ els.wheelActions.style.display = 'none';
82
+ } else {
83
+ els.btnModeFinger.classList.remove('active');
84
+ els.btnModeWheel.classList.add('active');
85
+ els.wheelActions.style.display = 'flex';
86
+ }
87
+ }
88
+
89
+ function resizeCanvas(canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D): void {
90
+ const rect = canvas.getBoundingClientRect();
91
+ canvas.width = rect.width * window.devicePixelRatio;
92
+ canvas.height = rect.height * window.devicePixelRatio;
93
+ ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
94
+ }
95
+
96
+ function bindSoundToggle(
97
+ btn: HTMLButtonElement, ui: Record<string, string>,
98
+ sound: SoundEffects, isSoundEnabled: { value: boolean }
99
+ ): void {
100
+ btn.addEventListener('click', () => {
101
+ isSoundEnabled.value = !isSoundEnabled.value;
102
+ sound.setMute(!isSoundEnabled.value);
103
+ if (isSoundEnabled.value) {
104
+ btn.classList.add('active');
105
+ btn.classList.remove('muted');
106
+ btn.innerHTML = SOUND_ON_SVG;
107
+ btn.title = ui.soundOn;
108
+ } else {
109
+ btn.classList.remove('active');
110
+ btn.classList.add('muted');
111
+ btn.innerHTML = SOUND_OFF_SVG;
112
+ btn.title = ui.soundOff;
113
+ }
114
+ });
115
+ }
116
+
117
+ function bindEvents(
118
+ els: Elements, ui: Record<string, string>,
119
+ deps: { engine: SelectionEngine; tracker: TouchTracker; sound: SoundEffects }
120
+ ): void {
121
+ els.btnModeFinger.addEventListener('click', () => updateMode('finger', deps.tracker, deps.engine, els));
122
+ els.btnModeWheel.addEventListener('click', () => updateMode('wheel', deps.tracker, deps.engine, els));
123
+
124
+ const isSoundEnabled = { value: true };
125
+ bindSoundToggle(els.btnSound, ui, deps.sound, isSoundEnabled);
126
+
127
+ els.btnReset.addEventListener('click', () => { deps.tracker.clear(); deps.engine.reset(); });
128
+ els.btnClear.addEventListener('click', () => { deps.tracker.clear(); deps.engine.reset(); });
129
+ els.btnSpin.addEventListener('click', () => { deps.engine.startWheelSelection(); });
130
+ }
131
+
132
+ interface LoopDeps {
133
+ engine: SelectionEngine;
134
+ effects: AnimationEffects;
135
+ tracker: TouchTracker;
136
+ }
137
+
138
+ function startLoop(
139
+ canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D,
140
+ deps: LoopDeps
141
+ ): void {
142
+ let lastTime = performance.now();
143
+ function loop(now: number) {
144
+ const dt = now - lastTime;
145
+ lastTime = now;
146
+ deps.engine.update(dt);
147
+
148
+ const width = canvas.width / window.devicePixelRatio;
149
+ const height = canvas.height / window.devicePixelRatio;
150
+ const isLightMode = document.documentElement.classList.contains('theme-light');
151
+
152
+ deps.effects.updateAndDraw({
153
+ ctx, width, height,
154
+ contacts: deps.tracker.getContacts(),
155
+ state: deps.engine.getState(),
156
+ progress: deps.engine.getCountdownProgress(),
157
+ wheelAngle: deps.engine.getWheelAngle(),
158
+ centroid: deps.engine.getCentroid(),
159
+ isLightMode
160
+ });
161
+
162
+ requestAnimationFrame(loop);
163
+ }
164
+ requestAnimationFrame(loop);
165
+ }
166
+
167
+ function init(): void {
168
+ const result = getElements();
169
+ if (!result) return;
170
+ const { els, ui } = result;
171
+
172
+ const sound = new SoundEffects();
173
+ const effects = new AnimationEffects();
174
+
175
+ const engine = new SelectionEngine(
176
+ (state) => handleStateChange(state, els, ui),
177
+ () => sound.playTick(),
178
+ (winner) => handleWinner(winner, sound, effects),
179
+ () => sound.playCountdown()
180
+ );
181
+
182
+ const tracker = new TouchTracker(els.canvas, (contacts) => {
183
+ engine.setContacts(contacts);
184
+ });
185
+
186
+ resizeCanvas(els.canvas, els.ctx);
187
+ window.addEventListener('resize', () => resizeCanvas(els.canvas, els.ctx));
188
+
189
+ bindEvents(els, ui, { engine, tracker, sound });
190
+ startLoop(els.canvas, els.ctx, { engine, effects, tracker });
191
+ }
192
+
193
+ if (document.readyState === 'loading') {
194
+ document.addEventListener('DOMContentLoaded', init);
195
+ } else {
196
+ init();
197
+ }
@@ -0,0 +1,77 @@
1
+ ---
2
+ import './first-player-selector.css';
3
+
4
+ interface Props {
5
+ ui: Record<string, string>;
6
+ }
7
+
8
+ const { ui } = Astro.props;
9
+ ---
10
+
11
+ <div class="selector-container" data-tool="first-player-selector" data-ui={JSON.stringify(ui)}>
12
+ <div class="mode-selector" role="group" aria-label="Selection mode">
13
+ <button class="mode-btn active" id="btn-mode-finger" type="button" title={ui.fingerMode}>
14
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
15
+ <path d="M14.5 11.5a3 3 0 1 0-4-4"/>
16
+ <path d="M12 17V7"/>
17
+ <path d="M8.5 8.5a3 3 0 0 0 1 5.5"/>
18
+ <path d="M10.5 14.5A5 5 0 0 0 17 11"/>
19
+ <path d="M7 19a7 7 0 0 1 10 0"/>
20
+ </svg>
21
+ <span>{ui.fingerMode}</span>
22
+ </button>
23
+ <button class="mode-btn" id="btn-mode-wheel" type="button" title={ui.wheelMode}>
24
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
25
+ <circle cx="12" cy="12" r="8"/>
26
+ <circle cx="12" cy="12" r="2" fill="currentColor"/>
27
+ <path d="M12 2v4M12 18v4M2 12h4M18 12h4"/>
28
+ </svg>
29
+ <span>{ui.wheelMode}</span>
30
+ </button>
31
+ </div>
32
+
33
+
34
+ <button class="icon-btn sound-btn active" id="btn-sound" type="button" title={ui.soundOn}>
35
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
36
+ <path d="M11 5L6 9H2v6h4l5 4V5z"/>
37
+ <path d="M19.07 4.93a10 10 0 010 14.14M15.54 8.46a5 5 0 010 7.07"/>
38
+ </svg>
39
+ </button>
40
+
41
+ <button class="icon-btn reset-btn" id="btn-reset" type="button" title={ui.reset}>
42
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
43
+ <polyline points="23 4 23 10 17 10"/>
44
+ <path d="M20.49 15a9 9 0 11-2.12-9.36L23 10"/>
45
+ </svg>
46
+ </button>
47
+
48
+
49
+ <div class="wheel-actions" id="wheel-actions" style="display: none;">
50
+ <button class="action-btn spin" id="btn-spin" type="button">
51
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor" stroke="none">
52
+ <polygon points="6 3 20 12 6 21 6 3"/>
53
+ </svg>
54
+ {ui.start}
55
+ </button>
56
+ <button class="action-btn clear" id="btn-clear" type="button">
57
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round">
58
+ <line x1="18" y1="6" x2="6" y2="18"/>
59
+ <line x1="6" y1="6" x2="18" y2="18"/>
60
+ </svg>
61
+ {ui.clearPlayers}
62
+ </button>
63
+ </div>
64
+
65
+ <div class="canvas-wrapper">
66
+ <canvas id="selector-canvas" class="selector-canvas"></canvas>
67
+ </div>
68
+
69
+ <div class="instructions-overlay">
70
+ <div class="instructions-text" id="instructions-text">
71
+ {ui.instructions}
72
+ </div>
73
+ <div class="instructions-status" id="instructions-status"></div>
74
+ </div>
75
+ </div>
76
+
77
+ <script src="./client.ts"></script>
@@ -0,0 +1,42 @@
1
+ import type { TabletopToolEntry, ToolLocaleContent } from '../../types';
2
+
3
+ export type FirstPlayerSelectorUI = {
4
+ title: string;
5
+ instructions: string;
6
+ tapToSelect: string;
7
+ winner: string;
8
+ reset: string;
9
+ start: string;
10
+ setupTitle: string;
11
+ clearPlayers: string;
12
+ fingerMode: string;
13
+ wheelMode: string;
14
+ presetsTitle: string;
15
+ soundOn: string;
16
+ soundOff: string;
17
+ playerCountLabel: string;
18
+ };
19
+
20
+ export type FirstPlayerSelectorLocaleContent = ToolLocaleContent<FirstPlayerSelectorUI>;
21
+
22
+ export const firstPlayerSelector: TabletopToolEntry<FirstPlayerSelectorUI> = {
23
+ id: 'first-player-selector',
24
+ icons: { bg: 'mdi:gesture-tap', fg: 'mdi:gesture-double-tap' },
25
+ i18n: {
26
+ de: () => import('./i18n/de').then((m) => m.content),
27
+ en: () => import('./i18n/en').then((m) => m.content),
28
+ es: () => import('./i18n/es').then((m) => m.content),
29
+ fr: () => import('./i18n/fr').then((m) => m.content),
30
+ id: () => import('./i18n/id').then((m) => m.content),
31
+ it: () => import('./i18n/it').then((m) => m.content),
32
+ ja: () => import('./i18n/ja').then((m) => m.content),
33
+ ko: () => import('./i18n/ko').then((m) => m.content),
34
+ nl: () => import('./i18n/nl').then((m) => m.content),
35
+ pl: () => import('./i18n/pl').then((m) => m.content),
36
+ pt: () => import('./i18n/pt').then((m) => m.content),
37
+ ru: () => import('./i18n/ru').then((m) => m.content),
38
+ sv: () => import('./i18n/sv').then((m) => m.content),
39
+ tr: () => import('./i18n/tr').then((m) => m.content),
40
+ zh: () => import('./i18n/zh').then((m) => m.content),
41
+ },
42
+ };
@@ -0,0 +1,303 @@
1
+ .selector-container {
2
+ position: relative;
3
+ width: 100%;
4
+ height: 100vh;
5
+ min-height: 500px;
6
+ overflow: hidden;
7
+ user-select: none;
8
+ -webkit-user-select: none;
9
+ background: var(--bg-surface);
10
+ color: var(--text-base);
11
+ box-shadow: inset 0 0 0 1px var(--border-base);
12
+ }
13
+
14
+ .canvas-wrapper {
15
+ position: absolute;
16
+ top: 0;
17
+ left: 0;
18
+ width: 100%;
19
+ height: 100%;
20
+ z-index: 10;
21
+ touch-action: none;
22
+ }
23
+
24
+ .selector-canvas {
25
+ display: block;
26
+ width: 100%;
27
+ height: 100%;
28
+ }
29
+
30
+ .mode-selector {
31
+ position: absolute;
32
+ top: 20px;
33
+ left: 50%;
34
+ transform: translateX(-50%);
35
+ display: inline-flex;
36
+ background: var(--bg-surface);
37
+ border: 1px solid var(--border-base);
38
+ border-radius: 9999px;
39
+ padding: 3px;
40
+ z-index: 20;
41
+ box-shadow: 0 10px 25px -5px var(--shadow-base), 0 8px 10px -6px var(--shadow-base);
42
+ }
43
+
44
+ .mode-btn {
45
+ display: inline-flex;
46
+ align-items: center;
47
+ gap: 6px;
48
+ padding: 7px 16px;
49
+ font-size: 13px;
50
+ font-weight: 600;
51
+ color: var(--text-dimmed);
52
+ background: transparent;
53
+ border: none;
54
+ border-radius: 9999px;
55
+ cursor: pointer;
56
+ transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
57
+ outline: none;
58
+ white-space: nowrap;
59
+ }
60
+
61
+ .mode-btn:hover {
62
+ color: var(--text-base);
63
+ }
64
+
65
+ .mode-btn.active {
66
+ color: var(--text-base);
67
+ background: var(--bg-muted);
68
+ }
69
+
70
+ .mode-btn svg {
71
+ flex-shrink: 0;
72
+ }
73
+
74
+ .icon-btn {
75
+ position: absolute;
76
+ display: flex;
77
+ align-items: center;
78
+ justify-content: center;
79
+ width: 40px;
80
+ height: 40px;
81
+ background: var(--bg-surface);
82
+ border: 1px solid var(--border-base);
83
+ border-radius: 50%;
84
+ cursor: pointer;
85
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
86
+ outline: none;
87
+ z-index: 20;
88
+ color: var(--text-dimmed);
89
+ }
90
+
91
+ .icon-btn:hover {
92
+ color: var(--text-base);
93
+ background: var(--bg-muted);
94
+ }
95
+
96
+ .icon-btn.active {
97
+ color: var(--text-base);
98
+ background: var(--bg-muted);
99
+ }
100
+
101
+ .icon-btn.muted {
102
+ opacity: 0.5;
103
+ }
104
+
105
+ .sound-btn {
106
+ top: 20px;
107
+ right: 20px;
108
+ }
109
+
110
+ .reset-btn {
111
+ bottom: 20px;
112
+ right: 20px;
113
+ }
114
+
115
+ .wheel-actions {
116
+ position: absolute;
117
+ bottom: 80px;
118
+ left: 50%;
119
+ transform: translateX(-50%);
120
+ display: flex;
121
+ flex-direction: column;
122
+ align-items: center;
123
+ gap: 10px;
124
+ z-index: 20;
125
+ pointer-events: none;
126
+ }
127
+
128
+ .wheel-actions .action-btn {
129
+ pointer-events: auto;
130
+ }
131
+
132
+ .action-btn {
133
+ display: inline-flex;
134
+ align-items: center;
135
+ gap: 8px;
136
+ border: none;
137
+ border-radius: 9999px;
138
+ cursor: pointer;
139
+ font-weight: 600;
140
+ outline: none;
141
+ transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
142
+ }
143
+
144
+ .action-btn.spin {
145
+ padding: 14px 32px;
146
+ font-size: 16px;
147
+ color: var(--text-on-primary);
148
+ background: var(--primary);
149
+ box-shadow: 0 8px 24px var(--shadow-hover);
150
+ }
151
+
152
+ .action-btn.spin:hover {
153
+ transform: translateY(-2px);
154
+ box-shadow: 0 12px 28px var(--shadow-hover);
155
+ }
156
+
157
+ .action-btn.spin:active {
158
+ transform: translateY(0);
159
+ }
160
+
161
+ .action-btn.spin:disabled {
162
+ opacity: 0.4;
163
+ cursor: not-allowed;
164
+ transform: none;
165
+ box-shadow: none;
166
+ }
167
+
168
+ .action-btn.clear {
169
+ padding: 8px 20px;
170
+ font-size: 13px;
171
+ color: var(--text-dimmed);
172
+ background: var(--bg-surface);
173
+ border: 1px solid var(--border-base);
174
+ }
175
+
176
+ .action-btn.clear:hover {
177
+ color: var(--text-base);
178
+ background: var(--bg-muted);
179
+ }
180
+
181
+ .instructions-overlay {
182
+ position: absolute;
183
+ bottom: 40px;
184
+ left: 50%;
185
+ transform: translateX(-50%);
186
+ text-align: center;
187
+ pointer-events: none;
188
+ z-index: 20;
189
+ }
190
+
191
+ .instructions-text {
192
+ font-size: 16px;
193
+ font-weight: 500;
194
+ color: var(--text-dimmed);
195
+ background: var(--bg-surface);
196
+ padding: 8px 20px;
197
+ border-radius: 9999px;
198
+ border: 1px solid var(--border-base);
199
+ letter-spacing: 0.025em;
200
+ animation: pulse-glow 2s infinite ease-in-out;
201
+ }
202
+
203
+ .instructions-status {
204
+ margin-top: 10px;
205
+ font-size: 24px;
206
+ font-weight: 700;
207
+ letter-spacing: 0.05em;
208
+ text-transform: uppercase;
209
+ }
210
+
211
+ .instructions-status.countdown {
212
+ color: var(--text-base);
213
+ animation: scale-up-pulse 0.5s infinite alternate ease-in-out;
214
+ }
215
+
216
+ .instructions-status.celebration {
217
+ color: var(--color-warning);
218
+ text-shadow: 0 0 10px var(--shadow-hover);
219
+ animation: winner-bounce 0.6s infinite alternate ease-in-out;
220
+ }
221
+
222
+ @keyframes pulse-glow {
223
+ 0%, 100% {
224
+ opacity: 0.6;
225
+ }
226
+ 50% {
227
+ opacity: 1.0;
228
+ }
229
+ }
230
+
231
+ @keyframes scale-up-pulse {
232
+ from {
233
+ transform: scale(0.95);
234
+ }
235
+ to {
236
+ transform: scale(1.05);
237
+ }
238
+ }
239
+
240
+ @keyframes winner-bounce {
241
+ from {
242
+ transform: translateY(0);
243
+ }
244
+ to {
245
+ transform: translateY(-8px);
246
+ }
247
+ }
248
+
249
+ @media (max-width: 768px) {
250
+ .mode-selector {
251
+ top: 12px;
252
+ padding: 2px;
253
+ }
254
+
255
+ .mode-btn {
256
+ padding: 6px 12px;
257
+ font-size: 12px;
258
+ gap: 4px;
259
+ }
260
+
261
+ .mode-btn svg {
262
+ width: 16px;
263
+ height: 16px;
264
+ }
265
+
266
+ .icon-btn {
267
+ width: 36px;
268
+ height: 36px;
269
+ }
270
+
271
+ .icon-btn svg {
272
+ width: 18px;
273
+ height: 18px;
274
+ }
275
+
276
+ .sound-btn {
277
+ top: 12px;
278
+ right: 12px;
279
+ }
280
+
281
+ .reset-btn {
282
+ bottom: 12px;
283
+ right: 12px;
284
+ }
285
+
286
+ .wheel-actions {
287
+ bottom: 90px;
288
+ }
289
+
290
+ .action-btn.spin {
291
+ padding: 12px 24px;
292
+ font-size: 14px;
293
+ }
294
+
295
+ .action-btn.clear {
296
+ padding: 6px 16px;
297
+ font-size: 12px;
298
+ }
299
+
300
+ .instructions-overlay {
301
+ bottom: 30px;
302
+ }
303
+ }