stem-lab-toolkit 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.html ADDED
@@ -0,0 +1,359 @@
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, maximum-scale=1.0">
6
+ <title>Calculator</title>
7
+ <style>
8
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
9
+
10
+ body {
11
+ min-height: 100dvh;
12
+ display: flex;
13
+ align-items: center;
14
+ justify-content: center;
15
+ background: #1a1a1a;
16
+ font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', sans-serif;
17
+ }
18
+
19
+ .calc-shell {
20
+ width: 340px;
21
+ background: #1c1c1e;
22
+ border-radius: 24px;
23
+ overflow: hidden;
24
+ box-shadow: 0 32px 80px rgba(0,0,0,.7), 0 0 0 1px rgba(255,255,255,.06);
25
+ user-select: none;
26
+ }
27
+
28
+ /* Header */
29
+ .calc-header {
30
+ display: flex;
31
+ align-items: center;
32
+ justify-content: space-between;
33
+ padding: 14px 20px 8px;
34
+ cursor: pointer;
35
+ }
36
+
37
+ .calc-logo-wrap {
38
+ display: flex;
39
+ align-items: center;
40
+ gap: 8px;
41
+ }
42
+
43
+ .calc-logo-icon {
44
+ width: 22px;
45
+ height: 22px;
46
+ background: linear-gradient(135deg, #ff9f0a, #ff6b00);
47
+ border-radius: 6px;
48
+ display: flex;
49
+ align-items: center;
50
+ justify-content: center;
51
+ font-size: 12px;
52
+ }
53
+
54
+ .calc-logo-text {
55
+ font-size: 0.78rem;
56
+ font-weight: 600;
57
+ color: rgba(255,255,255,.45);
58
+ letter-spacing: .02em;
59
+ }
60
+
61
+ .calc-mode-indicator {
62
+ font-size: 0.7rem;
63
+ color: rgba(255,255,255,.3);
64
+ letter-spacing: .05em;
65
+ }
66
+
67
+ /* Display */
68
+ .calc-display {
69
+ padding: 12px 24px 20px;
70
+ text-align: right;
71
+ min-height: 120px;
72
+ display: flex;
73
+ flex-direction: column;
74
+ justify-content: flex-end;
75
+ gap: 4px;
76
+ }
77
+
78
+ .display-expr {
79
+ font-size: 0.85rem;
80
+ color: rgba(255,255,255,.35);
81
+ min-height: 1.2em;
82
+ overflow: hidden;
83
+ text-overflow: ellipsis;
84
+ white-space: nowrap;
85
+ }
86
+
87
+ .display-main {
88
+ font-size: 3rem;
89
+ font-weight: 300;
90
+ color: #fff;
91
+ line-height: 1;
92
+ overflow: hidden;
93
+ text-overflow: ellipsis;
94
+ white-space: nowrap;
95
+ transition: font-size .1s;
96
+ }
97
+
98
+ /* Scientific row */
99
+ .sci-rows {
100
+ background: #111;
101
+ border-bottom: 1px solid rgba(255,255,255,.06);
102
+ }
103
+
104
+ .sci-row {
105
+ display: grid;
106
+ grid-template-columns: repeat(6, 1fr);
107
+ gap: 0;
108
+ border-bottom: 1px solid rgba(255,255,255,.04);
109
+ }
110
+
111
+ .sci-row:last-child { border-bottom: none; }
112
+
113
+ .btn-sci {
114
+ all: unset;
115
+ height: 46px;
116
+ display: flex;
117
+ align-items: center;
118
+ justify-content: center;
119
+ font-size: 0.78rem;
120
+ color: rgba(255,255,255,.65);
121
+ cursor: pointer;
122
+ transition: background .08s;
123
+ border-right: 1px solid rgba(255,255,255,.04);
124
+ }
125
+
126
+ .btn-sci:last-child { border-right: none; }
127
+ .btn-sci:active { background: rgba(255,255,255,.1); }
128
+ .btn-sci.inv-active { color: #ff9f0a; }
129
+
130
+ /* Main buttons */
131
+ .calc-buttons {
132
+ display: grid;
133
+ grid-template-columns: repeat(4, 1fr);
134
+ gap: 1px;
135
+ background: rgba(255,255,255,.06);
136
+ }
137
+
138
+ .btn {
139
+ all: unset;
140
+ display: flex;
141
+ align-items: center;
142
+ justify-content: center;
143
+ height: 80px;
144
+ font-size: 1.4rem;
145
+ font-weight: 400;
146
+ cursor: pointer;
147
+ transition: filter .08s;
148
+ -webkit-tap-highlight-color: transparent;
149
+ }
150
+
151
+ .btn:active { filter: brightness(1.3); }
152
+ .btn-func { background: #505050; color: #fff; font-size: 1.2rem; }
153
+ .btn-op { background: #ff9f0a; color: #fff; }
154
+ .btn-op.lit { background: #fff; color: #ff9f0a; }
155
+ .btn-num { background: #2c2c2e; color: #fff; }
156
+ .btn-zero { grid-column: span 2; justify-content: flex-start; padding-left: 30px; }
157
+ .btn-eq { background: #ff9f0a; color: #fff; }
158
+
159
+ /* PIN overlay */
160
+ .pin-overlay {
161
+ position: fixed;
162
+ inset: 0;
163
+ background: rgba(0,0,0,.75);
164
+ display: flex;
165
+ align-items: center;
166
+ justify-content: center;
167
+ z-index: 999;
168
+ backdrop-filter: blur(8px);
169
+ }
170
+
171
+ .pin-overlay.hidden { display: none; }
172
+
173
+ .pin-box {
174
+ background: #1c1c1e;
175
+ border-radius: 20px;
176
+ padding: 32px 28px 24px;
177
+ width: 300px;
178
+ box-shadow: 0 32px 80px rgba(0,0,0,.6);
179
+ border: 1px solid rgba(255,255,255,.08);
180
+ text-align: center;
181
+ }
182
+
183
+ .pin-box h2 {
184
+ font-size: 1rem;
185
+ font-weight: 600;
186
+ color: #fff;
187
+ margin-bottom: 4px;
188
+ }
189
+
190
+ .pin-box p {
191
+ font-size: 0.8rem;
192
+ color: rgba(255,255,255,.4);
193
+ margin-bottom: 24px;
194
+ }
195
+
196
+ .pin-dots {
197
+ display: flex;
198
+ justify-content: center;
199
+ gap: 14px;
200
+ margin-bottom: 28px;
201
+ }
202
+
203
+ .pin-dot {
204
+ width: 13px;
205
+ height: 13px;
206
+ border-radius: 50%;
207
+ border: 2px solid rgba(255,255,255,.2);
208
+ background: transparent;
209
+ transition: all .15s;
210
+ }
211
+
212
+ .pin-dot.filled { background: #ff9f0a; border-color: #ff9f0a; }
213
+ .pin-dot.error { background: #ff3b30; border-color: #ff3b30; }
214
+
215
+ .pin-keypad {
216
+ display: grid;
217
+ grid-template-columns: repeat(3, 1fr);
218
+ gap: 10px;
219
+ margin-bottom: 16px;
220
+ }
221
+
222
+ .pin-key {
223
+ padding: 15px;
224
+ border: none;
225
+ border-radius: 12px;
226
+ background: rgba(255,255,255,.08);
227
+ font-size: 1.1rem;
228
+ font-weight: 500;
229
+ color: #fff;
230
+ cursor: pointer;
231
+ transition: background .1s;
232
+ }
233
+
234
+ .pin-key:hover { background: rgba(255,255,255,.14); }
235
+ .pin-key:active { background: rgba(255,255,255,.2); }
236
+
237
+ .pin-key.del {
238
+ font-size: 0.85rem;
239
+ background: transparent;
240
+ color: rgba(255,255,255,.5);
241
+ }
242
+
243
+ .pin-error {
244
+ font-size: 0.8rem;
245
+ color: #ff3b30;
246
+ min-height: 1.2em;
247
+ margin-bottom: 8px;
248
+ }
249
+
250
+ .pin-cancel {
251
+ background: none;
252
+ border: none;
253
+ color: rgba(255,255,255,.4);
254
+ font-size: 0.85rem;
255
+ cursor: pointer;
256
+ padding: 6px;
257
+ }
258
+
259
+ .pin-cancel:hover { color: rgba(255,255,255,.7); }
260
+ </style>
261
+ </head>
262
+ <body>
263
+
264
+ <div class="calc-shell">
265
+
266
+ <!-- Clickable header (3× → PIN) -->
267
+ <div class="calc-header" id="calc-header">
268
+ <div class="calc-logo-wrap">
269
+ <div class="calc-logo-icon">∑</div>
270
+ <span class="calc-logo-text">SciCalc Pro</span>
271
+ </div>
272
+ <span class="calc-mode-indicator" id="mode-indicator">DEG</span>
273
+ </div>
274
+
275
+ <div class="calc-display">
276
+ <div class="display-expr" id="expr"></div>
277
+ <div class="display-main" id="main">0</div>
278
+ </div>
279
+
280
+ <!-- Scientific buttons -->
281
+ <div class="sci-rows">
282
+ <div class="sci-row">
283
+ <button class="btn-sci" id="inv-btn" data-sci="inv">2nd</button>
284
+ <button class="btn-sci" data-sci="sin">sin</button>
285
+ <button class="btn-sci" data-sci="cos">cos</button>
286
+ <button class="btn-sci" data-sci="tan">tan</button>
287
+ <button class="btn-sci" data-sci="pi">π</button>
288
+ <button class="btn-sci" data-sci="e">e</button>
289
+ </div>
290
+ <div class="sci-row">
291
+ <button class="btn-sci" data-sci="sq">x²</button>
292
+ <button class="btn-sci" data-sci="sqrt">√x</button>
293
+ <button class="btn-sci" data-sci="log">log</button>
294
+ <button class="btn-sci" data-sci="ln">ln</button>
295
+ <button class="btn-sci" data-sci="open">(</button>
296
+ <button class="btn-sci" data-sci="close">)</button>
297
+ </div>
298
+ </div>
299
+
300
+ <!-- Standard buttons -->
301
+ <div class="calc-buttons">
302
+ <button class="btn btn-func" data-action="clear">AC</button>
303
+ <button class="btn btn-func" data-action="sign">+/−</button>
304
+ <button class="btn btn-func" data-action="percent">%</button>
305
+ <button class="btn btn-op" data-action="op" data-op="÷">÷</button>
306
+
307
+ <button class="btn btn-num" data-action="digit" data-digit="7">7</button>
308
+ <button class="btn btn-num" data-action="digit" data-digit="8">8</button>
309
+ <button class="btn btn-num" data-action="digit" data-digit="9">9</button>
310
+ <button class="btn btn-op" data-action="op" data-op="×">×</button>
311
+
312
+ <button class="btn btn-num" data-action="digit" data-digit="4">4</button>
313
+ <button class="btn btn-num" data-action="digit" data-digit="5">5</button>
314
+ <button class="btn btn-num" data-action="digit" data-digit="6">6</button>
315
+ <button class="btn btn-op" data-action="op" data-op="−">−</button>
316
+
317
+ <button class="btn btn-num" data-action="digit" data-digit="1">1</button>
318
+ <button class="btn btn-num" data-action="digit" data-digit="2">2</button>
319
+ <button class="btn btn-num" data-action="digit" data-digit="3">3</button>
320
+ <button class="btn btn-op" data-action="op" data-op="+">+</button>
321
+
322
+ <button class="btn btn-num btn-zero" data-action="digit" data-digit="0">0</button>
323
+ <button class="btn btn-num" data-action="dot">.</button>
324
+ <button class="btn btn-eq" data-action="equals">=</button>
325
+ </div>
326
+ </div>
327
+
328
+ <!-- PIN overlay -->
329
+ <div class="pin-overlay hidden" id="pin-overlay">
330
+ <div class="pin-box">
331
+ <h2>Enter PIN</h2>
332
+ <p>Enter your access code</p>
333
+ <div class="pin-dots" id="pin-dots">
334
+ <div class="pin-dot" id="d0"></div>
335
+ <div class="pin-dot" id="d1"></div>
336
+ <div class="pin-dot" id="d2"></div>
337
+ <div class="pin-dot" id="d3"></div>
338
+ </div>
339
+ <div class="pin-error" id="pin-error"></div>
340
+ <div class="pin-keypad">
341
+ <button class="pin-key" data-k="1">1</button>
342
+ <button class="pin-key" data-k="2">2</button>
343
+ <button class="pin-key" data-k="3">3</button>
344
+ <button class="pin-key" data-k="4">4</button>
345
+ <button class="pin-key" data-k="5">5</button>
346
+ <button class="pin-key" data-k="6">6</button>
347
+ <button class="pin-key" data-k="7">7</button>
348
+ <button class="pin-key" data-k="8">8</button>
349
+ <button class="pin-key" data-k="9">9</button>
350
+ <button class="pin-key del" data-k="del">⌫</button>
351
+ <button class="pin-key" data-k="0">0</button>
352
+ <button class="pin-key del" data-k="cancel" style="font-size:.75rem;">cancel</button>
353
+ </div>
354
+ </div>
355
+ </div>
356
+
357
+ <script src="./js/main.js"></script>
358
+ </body>
359
+ </html>
package/js/admin.js ADDED
@@ -0,0 +1,177 @@
1
+ // Guard: must have admin session
2
+ if (!localStorage.getItem('mm_admin_session')) {
3
+ window.location.replace('./index.html');
4
+ }
5
+
6
+ const ADMIN_PIN = '1234';
7
+ let games = [];
8
+
9
+ async function loadGames() {
10
+ try {
11
+ const res = await fetch('./api/games/all');
12
+ if (!res.ok) throw new Error();
13
+ games = await res.json();
14
+ } catch {
15
+ try {
16
+ const res = await fetch('./games.json');
17
+ games = await res.json();
18
+ } catch { games = []; }
19
+ }
20
+ renderStats();
21
+ renderTable();
22
+ }
23
+
24
+ async function saveGames() {
25
+ try {
26
+ const res = await fetch('./api/games', {
27
+ method: 'POST',
28
+ headers: { 'Content-Type': 'application/json' },
29
+ body: JSON.stringify({ adminPin: ADMIN_PIN, games }),
30
+ });
31
+ if (!res.ok) throw new Error();
32
+ return true;
33
+ } catch { return false; }
34
+ }
35
+
36
+ function showFeedback(id, msg, type) {
37
+ const el = document.getElementById(id);
38
+ el.textContent = msg;
39
+ el.className = 'feedback ' + type;
40
+ setTimeout(() => { el.textContent = ''; el.className = 'feedback'; }, 3000);
41
+ }
42
+
43
+ function thumbHtml(game) {
44
+ if (game.thumbUrl && !game.thumbUrl.includes('REPLACE')) {
45
+ return `<img class="thumb-preview" src="${game.thumbUrl}" alt="" onerror="this.outerHTML='<div class=thumb-ph>🎮</div>'">`;
46
+ }
47
+ return `<div class="thumb-ph">🎮</div>`;
48
+ }
49
+
50
+ function renderStats() {
51
+ const counts = { Action:0, Sports:0, Racing:0, Puzzle:0, Multiplayer:0, Casual:0 };
52
+ let visible = 0;
53
+ games.forEach(g => {
54
+ if (g.visible) visible++;
55
+ if (counts[g.category] !== undefined) counts[g.category]++;
56
+ });
57
+ document.getElementById('stat-total').textContent = games.length;
58
+ document.getElementById('stat-visible').textContent = visible;
59
+ document.getElementById('stat-action').textContent = counts.Action;
60
+ document.getElementById('stat-sports').textContent = counts.Sports;
61
+ document.getElementById('stat-racing').textContent = counts.Racing;
62
+ document.getElementById('stat-puzzle').textContent = counts.Puzzle;
63
+ document.getElementById('stat-multi').textContent = counts.Multiplayer;
64
+ document.getElementById('stat-casual').textContent = counts.Casual;
65
+ }
66
+
67
+ function renderTable() {
68
+ const tbody = document.getElementById('table-body');
69
+ if (!games.length) {
70
+ tbody.innerHTML = '<tr><td colspan="8" style="text-align:center;padding:32px;color:var(--muted)">No games yet.</td></tr>';
71
+ return;
72
+ }
73
+
74
+ tbody.innerHTML = games.map((g, i) => `
75
+ <tr data-i="${i}">
76
+ <td>${thumbHtml(g)}</td>
77
+ <td style="font-weight:500;">${g.name}</td>
78
+ <td style="color:var(--muted);font-size:.8rem;">${g.slug}</td>
79
+ <td><span class="cat-badge ${g.category}">${g.category}</span></td>
80
+ <td style="font-size:.8rem;color:var(--muted);">${g.embedType}</td>
81
+ <td>
82
+ <button class="btn btn-sm ${g.gameOfDay ? 'btn-primary' : 'btn-ghost'}" data-action="gotd" data-i="${i}" title="Set as Game of the Day">
83
+ ${g.gameOfDay ? '⭐' : '☆'}
84
+ </button>
85
+ </td>
86
+ <td>
87
+ <label class="toggle">
88
+ <input type="checkbox" data-action="toggle" data-i="${i}" ${g.visible ? 'checked' : ''}>
89
+ <span class="toggle-slider"></span>
90
+ </label>
91
+ </td>
92
+ <td>
93
+ <button class="btn btn-sm btn-danger" data-action="delete" data-i="${i}">Delete</button>
94
+ </td>
95
+ </tr>
96
+ `).join('');
97
+ }
98
+
99
+ document.getElementById('table-body').addEventListener('click', async e => {
100
+ const btn = e.target.closest('[data-action]');
101
+ if (!btn) return;
102
+ const i = parseInt(btn.dataset.i);
103
+ const action = btn.dataset.action;
104
+
105
+ if (action === 'delete') {
106
+ if (!confirm(`Delete "${games[i].name}"?`)) return;
107
+ games.splice(i, 1);
108
+ const ok = await saveGames();
109
+ renderStats();
110
+ renderTable();
111
+ showFeedback('table-feedback', ok ? 'Game deleted.' : 'Deleted locally (API unavailable).', ok ? 'ok' : 'error');
112
+ }
113
+
114
+ if (action === 'gotd') {
115
+ games.forEach(g => g.gameOfDay = false);
116
+ games[i].gameOfDay = true;
117
+ const ok = await saveGames();
118
+ renderTable();
119
+ showFeedback('table-feedback', ok ? `"${games[i].name}" set as Game of the Day.` : 'Saved locally only.', ok ? 'ok' : 'error');
120
+ }
121
+ });
122
+
123
+ document.getElementById('table-body').addEventListener('change', async e => {
124
+ const input = e.target.closest('input[data-action="toggle"]');
125
+ if (!input) return;
126
+ const i = parseInt(input.dataset.i);
127
+ games[i].visible = input.checked;
128
+ const ok = await saveGames();
129
+ renderStats();
130
+ showFeedback('table-feedback', ok ? 'Visibility updated.' : 'Updated locally (API unavailable).', ok ? 'ok' : 'error');
131
+ });
132
+
133
+ // Slug auto-fill
134
+ document.getElementById('f-name').addEventListener('input', () => {
135
+ const name = document.getElementById('f-name').value;
136
+ document.getElementById('f-slug').value = name.toLowerCase().trim().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
137
+ });
138
+
139
+ // Add form
140
+ document.getElementById('add-form').addEventListener('submit', async e => {
141
+ e.preventDefault();
142
+ const embedType = document.getElementById('f-embedtype').value;
143
+ const gameId = document.getElementById('f-gameid').value.trim();
144
+ const embedUrl = document.getElementById('f-embedurl').value.trim();
145
+ const thumbUrl = document.getElementById('f-thumb').value.trim();
146
+
147
+ const game = {
148
+ name: document.getElementById('f-name').value.trim(),
149
+ slug: document.getElementById('f-slug').value.trim(),
150
+ category: document.getElementById('f-category').value,
151
+ embedType,
152
+ gameId: gameId || 'REPLACE_GAME_ID',
153
+ embedUrl: embedUrl || '',
154
+ thumbUrl: thumbUrl || (gameId ? `https://img.gamedistribution.com/${gameId}-512x384.jpeg` : ''),
155
+ visible: true,
156
+ gameOfDay: false,
157
+ };
158
+
159
+ if (!game.name || !game.slug) {
160
+ showFeedback('add-feedback', 'Name and slug are required.', 'error');
161
+ return;
162
+ }
163
+
164
+ if (games.find(g => g.slug === game.slug)) {
165
+ showFeedback('add-feedback', 'Slug already exists.', 'error');
166
+ return;
167
+ }
168
+
169
+ games.push(game);
170
+ const ok = await saveGames();
171
+ renderStats();
172
+ renderTable();
173
+ e.target.reset();
174
+ showFeedback('add-feedback', ok ? 'Game added.' : 'Added locally (API unavailable).', ok ? 'ok' : 'error');
175
+ });
176
+
177
+ loadGames();
package/js/games.js ADDED
@@ -0,0 +1,113 @@
1
+ let allGames = [];
2
+ let activeCategory = 'All';
3
+ let searchQuery = '';
4
+
5
+ async function loadGames() {
6
+ try {
7
+ const res = await fetch('./api/games');
8
+ if (!res.ok) throw new Error();
9
+ allGames = await res.json();
10
+ } catch {
11
+ try {
12
+ const res = await fetch('./games.json');
13
+ const data = await res.json();
14
+ allGames = data.filter(g => g.visible);
15
+ } catch { allGames = []; }
16
+ }
17
+ render();
18
+ }
19
+
20
+ function thumbUrl(game) {
21
+ if (game.thumbUrl && !game.thumbUrl.includes('REPLACE')) return game.thumbUrl;
22
+ return '';
23
+ }
24
+
25
+ function cardThumb(game) {
26
+ const url = thumbUrl(game);
27
+ if (url) return `<img class="game-card-thumb" src="${url}" alt="${game.name}" loading="lazy" onerror="this.parentNode.innerHTML=fallbackThumb()">`;
28
+ return `<div class="game-card-thumb-placeholder">🎮</div>`;
29
+ }
30
+
31
+ function fallbackThumb() {
32
+ return `<div class="game-card-thumb-placeholder">🎮</div>`;
33
+ }
34
+
35
+ function filtered() {
36
+ return allGames.filter(g => {
37
+ const matchCat = activeCategory === 'All' || g.category === activeCategory;
38
+ const matchQ = !searchQuery || g.name.toLowerCase().includes(searchQuery);
39
+ return matchCat && matchQ;
40
+ });
41
+ }
42
+
43
+ function render() {
44
+ renderGotd();
45
+ renderGrid();
46
+ }
47
+
48
+ function renderGotd() {
49
+ const section = document.getElementById('gotd-section');
50
+ const gotd = allGames.find(g => g.gameOfDay && g.visible);
51
+ if (!gotd) { section.innerHTML = ''; return; }
52
+
53
+ const url = thumbUrl(gotd);
54
+ const thumbHtml = url
55
+ ? `<img class="gotd-thumb" src="${url}" alt="${gotd.name}" onerror="this.outerHTML='<div class=gotd-thumb-placeholder>🎮</div>'">`
56
+ : `<div class="gotd-thumb-placeholder">🎮</div>`;
57
+
58
+ section.innerHTML = `
59
+ <div class="section-title">Game of the Day</div>
60
+ <a href="./game.html?slug=${gotd.slug}" class="gotd-card">
61
+ ${thumbHtml}
62
+ <div>
63
+ <div class="gotd-label">⭐ Featured Today</div>
64
+ <div class="gotd-name">${gotd.name}</div>
65
+ <span class="cat-badge ${gotd.category}">${gotd.category}</span>
66
+ <div class="gotd-play-btn">▶ Play Now</div>
67
+ </div>
68
+ </a>
69
+ `;
70
+ }
71
+
72
+ function renderGrid() {
73
+ const grid = document.getElementById('game-grid');
74
+ const label = document.getElementById('grid-label');
75
+ const games = filtered();
76
+
77
+ label.textContent = activeCategory === 'All'
78
+ ? `All Games (${games.length})`
79
+ : `${activeCategory} (${games.length})`;
80
+
81
+ if (!games.length) {
82
+ grid.innerHTML = '<div class="no-results">No games found.</div>';
83
+ return;
84
+ }
85
+
86
+ grid.innerHTML = games.map(g => `
87
+ <a href="./game.html?slug=${g.slug}" class="game-card">
88
+ ${cardThumb(g)}
89
+ <div class="game-card-body">
90
+ <div class="game-card-name">${g.name}</div>
91
+ <span class="cat-badge ${g.category}">${g.category}</span>
92
+ </div>
93
+ </a>
94
+ `).join('');
95
+ }
96
+
97
+ // Category filter
98
+ document.getElementById('cat-filters').addEventListener('click', e => {
99
+ const btn = e.target.closest('.cat-btn');
100
+ if (!btn) return;
101
+ document.querySelectorAll('.cat-btn').forEach(b => b.classList.remove('active'));
102
+ btn.classList.add('active');
103
+ activeCategory = btn.dataset.cat;
104
+ render();
105
+ });
106
+
107
+ // Search
108
+ document.getElementById('search').addEventListener('input', e => {
109
+ searchQuery = e.target.value.trim().toLowerCase();
110
+ render();
111
+ });
112
+
113
+ loadGames();