drbh HF Staff commited on
Commit
75dc9a3
·
verified ·
1 Parent(s): bd78096

feat: tiny app

Browse files
Files changed (1) hide show
  1. index.html +1431 -18
index.html CHANGED
@@ -1,19 +1,1432 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en" style="background: #120458; position: fixed; width: 100%; height: 100%; overflow: hidden;">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>AI/ML Card Builder</title>
7
+ <style>
8
+ @import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Inter:wght@300;400;600;700&display=swap');
9
+ :root {
10
+ --bg-1: #120458;
11
+ --bg-2: #000000;
12
+ --bg-3: #1a0933;
13
+ --surface-1: #1a1a2e;
14
+ --surface-2: #16213e;
15
+ --accent-1: #8b45ff;
16
+ --accent-2: #ff1493;
17
+ --accent-3: #00d4ff;
18
+ --accent-warm-1: #ff6b35;
19
+ --accent-warm-2: #f7931e;
20
+ --text-1: #ffffff;
21
+ --text-2: #dddddd;
22
+ --text-3: #aaaaaa;
23
+ --card-radius: 25px;
24
+ --inner-radius: 22px;
25
+ --trans-fast: 0.15s ease;
26
+ --trans-med: 0.3s ease;
27
+ --shadow-outer: 0 25px 50px rgba(0, 0, 0, 0.5);
28
+ --shadow-outer-strong: 0 35px 70px rgba(0, 0, 0, 0.7);
29
+ --shadow-inner: inset 0 1px 0 rgba(255, 255, 255, 0.1);
30
+ --glass: linear-gradient(135deg, rgba(255,255,255,0.1), rgba(255,255,255,0.05));
31
+ }
32
+
33
+ * {
34
+ margin: 0;
35
+ padding: 0;
36
+ box-sizing: border-box;
37
+ }
38
+
39
+
40
+ body {
41
+ font-family: 'Inter', sans-serif;
42
+ background: radial-gradient(circle at 20% 80%, var(--bg-1) 0%, var(--bg-2) 50%, var(--bg-3) 100%);
43
+ background-attachment: fixed;
44
+ min-height: 100vh;
45
+ width: 100%;
46
+ height: 100%;
47
+ display: flex;
48
+ justify-content: center;
49
+ align-items: flex-start;
50
+ padding: 20px;
51
+ padding-top: 40px;
52
+ position: relative;
53
+ overflow: auto;
54
+ overscroll-behavior: contain;
55
+ }
56
+
57
+ body::before {
58
+ content: '';
59
+ position: fixed;
60
+ top: 50%;
61
+ left: 50%;
62
+ width: 140vmax;
63
+ height: 140vmax;
64
+ transform: translate(-50%, -50%) rotate(0deg);
65
+ background:
66
+ radial-gradient(circle at 25% 25%, rgba(139, 69, 255, 0.1) 0%, transparent 50%),
67
+ radial-gradient(circle at 75% 75%, rgba(255, 20, 147, 0.1) 0%, transparent 50%);
68
+ animation: float 20s ease-in-out infinite;
69
+ z-index: -1;
70
+ pointer-events: none;
71
+ will-change: transform;
72
+ }
73
+
74
+ @keyframes float {
75
+ 0%, 100% { transform: translate(-50%, -50%) rotate(0deg); }
76
+ 50% { transform: translate(-50%, -50%) rotate(180deg); }
77
+ }
78
+
79
+ .container {
80
+ display: flex;
81
+ gap: 50px;
82
+ align-items: flex-start;
83
+ max-width: 1200px;
84
+ width: 100%;
85
+ margin: 0 auto;
86
+ justify-content: center;
87
+ }
88
+
89
+ /* Center card when builder is hidden */
90
+ .builder-hidden .container { justify-content: center; gap: 0; }
91
+ .builder-hidden .controls { display: none; }
92
+
93
+ /* Floating toggle button */
94
+ .builder-toggle {
95
+ position: fixed;
96
+ bottom: 20px;
97
+ right: 20px;
98
+ z-index: 1000;
99
+ }
100
+
101
+ .export-toggle {
102
+ position: fixed;
103
+ bottom: 20px;
104
+ right: 140px; /* keep clear of builder toggle */
105
+ z-index: 1000;
106
+ }
107
+
108
+ .card {
109
+ width: 380px;
110
+ height: 650px;
111
+ background: linear-gradient(145deg, var(--surface-1), var(--surface-2));
112
+ border-radius: var(--card-radius);
113
+ padding: 0;
114
+ position: relative;
115
+ overflow: hidden;
116
+ box-shadow:
117
+ 0 0 0 2px rgba(255, 255, 255, 0.1),
118
+ var(--shadow-outer),
119
+ var(--shadow-inner);
120
+ transform: perspective(1000px) rotateX(5deg);
121
+ transition: transform var(--trans-med), box-shadow var(--trans-med);
122
+ }
123
+
124
+ .card:hover {
125
+ /* Keep dynamic JS-driven transform; only enhance shadow on hover */
126
+ box-shadow:
127
+ 0 0 0 2px rgba(139, 69, 255, 0.3),
128
+ var(--shadow-outer-strong),
129
+ inset 0 1px 0 rgba(255, 255, 255, 0.2);
130
+ }
131
+
132
+ .card-border {
133
+ position: absolute;
134
+ top: 0;
135
+ left: 0;
136
+ right: 0;
137
+ bottom: 0;
138
+ border-radius: var(--card-radius);
139
+ background: linear-gradient(45deg,
140
+ var(--accent-warm-1) 0%,
141
+ var(--accent-warm-2) 25%,
142
+ var(--accent-1) 50%,
143
+ var(--accent-2) 75%,
144
+ var(--accent-3) 100%);
145
+ background-size: 400% 400%;
146
+ animation: gradientShift 3s ease-in-out infinite;
147
+ padding: 3px;
148
+ }
149
+
150
+ @keyframes gradientShift {
151
+ 0%, 100% { background-position: 0% 50%; }
152
+ 50% { background-position: 100% 50%; }
153
+ }
154
+
155
+ .card-inner {
156
+ background: linear-gradient(145deg, var(--surface-1), var(--surface-2));
157
+ height: 100%;
158
+ border-radius: var(--inner-radius);
159
+ padding: 50px 25px 25px 25px;
160
+ position: relative;
161
+ }
162
+
163
+ .card-rarity {
164
+ position: absolute;
165
+ top: 8px;
166
+ right: 8px;
167
+ width: 28px;
168
+ height: 28px;
169
+ background: linear-gradient(135deg, #ffd700, #ffed4e);
170
+ border-radius: 50%;
171
+ display: flex;
172
+ align-items: center;
173
+ justify-content: center;
174
+ font-weight: 900;
175
+ color: #333;
176
+ font-size: 14px;
177
+ box-shadow: 0 5px 15px rgba(255, 215, 0, 0.3);
178
+ z-index: 1;
179
+ }
180
+
181
+ .card-header {
182
+ display: grid;
183
+ grid-template-columns: 1fr auto;
184
+ align-items: baseline;
185
+ gap: 10px;
186
+ margin-bottom: 10px;
187
+ margin-top: 15px;
188
+ }
189
+
190
+ .card-name {
191
+ font-family: 'Orbitron', monospace;
192
+ font-size: 20px;
193
+ font-weight: 900;
194
+ background: linear-gradient(135deg, #ffffff, #a0a0a0);
195
+ -webkit-background-clip: text;
196
+ -webkit-text-fill-color: transparent;
197
+ background-clip: text;
198
+ margin-bottom: 8px;
199
+ text-transform: uppercase;
200
+ letter-spacing: 1px;
201
+ text-shadow: 0 0 20px rgba(255, 255, 255, 0.5);
202
+ word-wrap: break-word;
203
+ hyphens: auto;
204
+ }
205
+
206
+ .card-title { font-size: 11px; color: var(--accent-1); text-transform: uppercase; letter-spacing: 2px; font-weight: 600; opacity: 0.9; grid-column: 1 / -1; word-wrap: break-word; hyphens: auto; }
207
+
208
+ .hp { font-family: 'Orbitron', monospace; font-size: 18px; font-weight: 900; color: var(--text-1); display: flex; align-items: baseline; gap: 4px; }
209
+ .hp .label { font-size: 11px; opacity: 0.8; }
210
+ .type-badge { position: absolute; top: 8px; right: 44px; width: 32px; height: 32px; border-radius: 50%; display: grid; place-items: center; font-weight: 900; color: #111; box-shadow: 0 6px 16px rgba(0,0,0,0.35); border: 2px solid rgba(255,255,255,0.3); background: linear-gradient(135deg, #fff, #ddd); z-index: 2; }
211
+
212
+ .card-level {
213
+ position: absolute;
214
+ top: 8px;
215
+ left: 8px;
216
+ background: linear-gradient(135deg, var(--accent-warm-1), var(--accent-warm-2));
217
+ color: white;
218
+ padding: 4px 12px;
219
+ border-radius: 15px;
220
+ font-size: 11px;
221
+ font-weight: 700;
222
+ text-transform: uppercase;
223
+ }
224
+
225
+ .artwork-frame {
226
+ width: 100%;
227
+ height: 200px;
228
+ margin: 8px auto 12px;
229
+ position: relative;
230
+ background: linear-gradient(135deg, #2d1b69, #11998e);
231
+ border-radius: 20px;
232
+ padding: 15px;
233
+ box-shadow:
234
+ inset 0 0 20px rgba(0, 0, 0, 0.3),
235
+ 0 10px 30px rgba(0, 0, 0, 0.3);
236
+ }
237
+
238
+ .artwork-frame::before {
239
+ content: '';
240
+ position: absolute;
241
+ top: -2px;
242
+ left: -2px;
243
+ right: -2px;
244
+ bottom: -2px;
245
+ background: linear-gradient(45deg, var(--accent-1), var(--accent-2), var(--accent-3), var(--accent-1));
246
+ border-radius: 22px;
247
+ z-index: -1;
248
+ opacity: 0.7;
249
+ }
250
+
251
+ .shape-container { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; border-radius: 12px; background: var(--glass); backdrop-filter: blur(10px); position: relative; overflow: hidden; }
252
+ .art-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: none; }
253
+
254
+ .shape {
255
+ transition: all 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
256
+ filter: drop-shadow(0 5px 15px rgba(255, 255, 255, 0.3));
257
+ }
258
+
259
+ .shape.circle {
260
+ width: 80px;
261
+ height: 80px;
262
+ background: linear-gradient(135deg, #fff, #f0f0f0);
263
+ border-radius: 50%;
264
+ }
265
+
266
+ .shape.square {
267
+ width: 80px;
268
+ height: 80px;
269
+ background: linear-gradient(135deg, #fff, #f0f0f0);
270
+ border-radius: 12px;
271
+ }
272
+
273
+ .shape.triangle {
274
+ width: 0;
275
+ height: 0;
276
+ border-left: 40px solid transparent;
277
+ border-right: 40px solid transparent;
278
+ border-bottom: 70px solid #fff;
279
+ filter: drop-shadow(0 5px 15px rgba(255, 255, 255, 0.3));
280
+ }
281
+
282
+ .shape.diamond {
283
+ width: 80px;
284
+ height: 80px;
285
+ background: linear-gradient(135deg, #fff, #f0f0f0);
286
+ transform: rotate(45deg);
287
+ border-radius: 12px;
288
+ }
289
+
290
+ /* Attacks section */
291
+ .attacks { margin-top: 8px; display: grid; gap: 10px; }
292
+ .attack-row { background: var(--glass); border: 1px solid rgba(255,255,255,0.12); border-radius: 12px; padding: 10px 12px; }
293
+ .attack-head { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 8px; }
294
+ .cost { display: flex; gap: 4px; flex-wrap: wrap; }
295
+ .atk-name { font-weight: 800; color: var(--text-1); letter-spacing: 0.5px; }
296
+ .atk-dmg { font-family: 'Orbitron', monospace; font-weight: 900; color: var(--text-1); }
297
+ .atk-text { color: var(--text-2); font-size: 12px; margin-top: 6px; line-height: 1.4; }
298
+
299
+ .energy { width: 18px; height: 18px; border-radius: 50%; display: grid; place-items: center; font-size: 11px; font-weight: 900; color: #111; border: 1px solid rgba(255,255,255,0.3); box-shadow: 0 2px 6px rgba(0,0,0,0.25); }
300
+ /* AI/ML themed energy chips */
301
+ .e-generalist { background: linear-gradient(135deg, #f5f5f5, #e0e0e0); }
302
+ .e-python { background: linear-gradient(135deg, #3776AB, #FFD43B); }
303
+ .e-data { background: linear-gradient(135deg, #34d399, #0ea5e9); }
304
+ .e-gpu { background: linear-gradient(135deg, #a7f3d0, #065f46); color: #0a0a0a; }
305
+ .e-cloud { background: linear-gradient(135deg, #93c5fd, #e5e7eb); }
306
+ .e-research { background: linear-gradient(135deg, #a78bfa, #f472b6); }
307
+ .e-math { background: linear-gradient(135deg, #cbd5e1, #64748b); color: #0a0a0a; }
308
+ .e-systems { background: linear-gradient(135deg, #f59e0b, #b45309); }
309
+ .e-security { background: linear-gradient(135deg, #111827, #ef4444); color: #fafafa; }
310
+ .e-mlops { background: linear-gradient(135deg, #10b981, #3b82f6); }
311
+
312
+ .card-description { background: linear-gradient(135deg, rgba(139, 69, 255, 0.1), rgba(255, 20, 147, 0.1)); border: 1px solid rgba(139, 69, 255, 0.2); padding: 10px; border-radius: 12px; font-size: 12px; color: var(--text-2); line-height: 1.4; text-align: center; font-style: italic; backdrop-filter: blur(10px); margin-top: 10px; }
313
+
314
+ .card-footer { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-top: 10px; }
315
+ .footer-box { background: var(--glass); border: 1px solid rgba(255,255,255,0.12); border-radius: 10px; padding: 8px; text-align: center; }
316
+ .footer-box .label { font-size: 10px; color: var(--text-3); text-transform: uppercase; letter-spacing: 1px; }
317
+ .footer-box .content { margin-top: 6px; display: flex; justify-content: center; gap: 4px; align-items: center; color: var(--text-2); font-weight: 700; word-wrap: break-word; text-align: center; }
318
+
319
+ .card-watermark {
320
+ position: absolute;
321
+ bottom: 15px;
322
+ right: 15px;
323
+ font-family: 'Orbitron', monospace;
324
+ font-size: 24px;
325
+ font-weight: 900;
326
+ color: rgba(255, 255, 255, 0.08);
327
+ transform: rotate(-10deg);
328
+ pointer-events: none;
329
+ user-select: none;
330
+ }
331
+
332
+ .card-info {
333
+ position: absolute;
334
+ bottom: 8px;
335
+ left: 15px;
336
+ right: 15px;
337
+ display: flex;
338
+ justify-content: space-between;
339
+ font-family: 'Inter', sans-serif;
340
+ font-size: 9px;
341
+ color: rgba(255, 255, 255, 0.3);
342
+ pointer-events: none;
343
+ user-select: none;
344
+ }
345
+
346
+ .card-info span {
347
+ letter-spacing: 0.5px;
348
+ }
349
+
350
+ .controls {
351
+ background: linear-gradient(145deg, rgba(26, 26, 46, 0.95), rgba(22, 33, 62, 0.95));
352
+ backdrop-filter: blur(20px);
353
+ border: 1px solid rgba(255, 255, 255, 0.1);
354
+ padding: 30px;
355
+ border-radius: 25px;
356
+ width: 420px;
357
+ box-shadow: 0 25px 50px rgba(0, 0, 0, 0.3);
358
+ color: var(--text-1);
359
+ }
360
+
361
+ .controls h3 {
362
+ font-family: 'Orbitron', monospace;
363
+ margin-bottom: 25px;
364
+ color: var(--text-1);
365
+ text-align: center;
366
+ font-size: 18px;
367
+ text-transform: uppercase;
368
+ letter-spacing: 1px;
369
+ }
370
+
371
+ .control-group {
372
+ margin-bottom: 20px;
373
+ }
374
+
375
+ .control-group label {
376
+ display: block;
377
+ margin-bottom: 8px;
378
+ font-weight: 600;
379
+ color: var(--accent-1);
380
+ font-size: 12px;
381
+ text-transform: uppercase;
382
+ letter-spacing: 1px;
383
+ }
384
+
385
+ .control-group input,
386
+ .control-group textarea,
387
+ .control-group select {
388
+ width: 100%;
389
+ padding: 12px 15px;
390
+ background: rgba(255, 255, 255, 0.05);
391
+ border: 1px solid rgba(255, 255, 255, 0.1);
392
+ border-radius: 12px;
393
+ color: var(--text-1);
394
+ font-size: 14px;
395
+ transition: all 0.3s ease;
396
+ backdrop-filter: blur(10px);
397
+ }
398
+
399
+ .control-group input:focus,
400
+ .control-group textarea:focus,
401
+ .control-group select:focus {
402
+ outline: none;
403
+ border-color: var(--accent-1);
404
+ box-shadow: 0 0 0 3px rgba(139, 69, 255, 0.25);
405
+ background: rgba(255, 255, 255, 0.1);
406
+ }
407
+
408
+ .control-group input[type="range"] {
409
+ padding: 0;
410
+ height: 6px;
411
+ background: rgba(255, 255, 255, 0.1);
412
+ -webkit-appearance: none;
413
+ border-radius: 3px;
414
+ }
415
+
416
+ .control-group input[type="range"]::-webkit-slider-thumb {
417
+ -webkit-appearance: none;
418
+ width: 20px;
419
+ height: 20px;
420
+ background: linear-gradient(135deg, var(--accent-1), var(--accent-2));
421
+ border-radius: 50%;
422
+ cursor: pointer;
423
+ box-shadow: 0 5px 15px rgba(139, 69, 255, 0.4);
424
+ }
425
+
426
+ .control-group textarea {
427
+ resize: vertical;
428
+ height: 80px;
429
+ font-family: 'Inter', sans-serif;
430
+ }
431
+
432
+ .shape-buttons {
433
+ display: grid;
434
+ grid-template-columns: repeat(2, 1fr);
435
+ gap: 10px;
436
+ margin-top: 8px;
437
+ }
438
+
439
+ .shape-btn {
440
+ padding: 12px;
441
+ background: rgba(255, 255, 255, 0.05);
442
+ border: 1px solid rgba(255, 255, 255, 0.1);
443
+ color: var(--text-1);
444
+ border-radius: 12px;
445
+ cursor: pointer;
446
+ transition: all 0.3s ease;
447
+ font-size: 12px;
448
+ text-transform: uppercase;
449
+ font-weight: 600;
450
+ letter-spacing: 1px;
451
+ backdrop-filter: blur(10px);
452
+ outline: none;
453
+ }
454
+
455
+ .shape-btn:hover {
456
+ border-color: var(--accent-1);
457
+ background: rgba(139, 69, 255, 0.2);
458
+ transform: translateY(-2px);
459
+ }
460
+
461
+ .shape-btn.active {
462
+ border-color: var(--accent-1);
463
+ background: linear-gradient(135deg, var(--accent-1), var(--accent-2));
464
+ color: white;
465
+ box-shadow: 0 10px 25px rgba(139, 69, 255, 0.4);
466
+ }
467
+
468
+ .shape-btn:focus-visible {
469
+ box-shadow: 0 0 0 3px rgba(139,69,255,0.4);
470
+ border-color: var(--accent-1);
471
+ }
472
+
473
+ .range-display {
474
+ display: flex;
475
+ justify-content: space-between;
476
+ align-items: center;
477
+ margin-bottom: 5px;
478
+ }
479
+
480
+ .range-value {
481
+ font-family: 'Orbitron', monospace;
482
+ color: var(--accent-1);
483
+ font-weight: 700;
484
+ font-size: 14px;
485
+ }
486
+
487
+ .helper-row {
488
+ display: flex;
489
+ justify-content: space-between;
490
+ align-items: center;
491
+ margin-top: 6px;
492
+ color: var(--text-3);
493
+ font-size: 12px;
494
+ }
495
+
496
+ .actions {
497
+ display: grid;
498
+ grid-template-columns: 1fr 1fr;
499
+ gap: 10px;
500
+ margin-top: 20px;
501
+ }
502
+
503
+ .btn {
504
+ padding: 10px 12px;
505
+ border-radius: 10px;
506
+ border: 1px solid rgba(255,255,255,0.15);
507
+ background: rgba(255,255,255,0.06);
508
+ color: var(--text-1);
509
+ font-weight: 600;
510
+ letter-spacing: 0.5px;
511
+ cursor: pointer;
512
+ transition: transform var(--trans-fast), background var(--trans-fast), border-color var(--trans-fast);
513
+ }
514
+
515
+ .btn:hover { transform: translateY(-1px); border-color: var(--accent-1); }
516
+ .btn.primary { background: linear-gradient(135deg, var(--accent-1), var(--accent-2)); border-color: transparent; }
517
+ .btn.danger { background: linear-gradient(135deg, #ff5a5f, #ff2d55); border-color: transparent; }
518
+
519
+ .toggles { display: grid; grid-template-columns: 1fr; gap: 10px; margin-top: 10px; }
520
+ .toggle { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--text-2); }
521
+ .toggle input { accent-color: var(--accent-1); }
522
+
523
+ .attack-editor { background: rgba(255,255,255,0.04); border: 1px dashed rgba(255,255,255,0.15); border-radius: 12px; padding: 12px; margin-bottom: 12px; }
524
+ .attack-editor .row { display: grid; grid-template-columns: 1fr 100px; gap: 8px; margin-bottom: 8px; }
525
+ .attack-editor .row-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 8px; }
526
+ .attack-editor .row-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px; margin-bottom: 8px; }
527
+ .energy-palette { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
528
+ .energy-palette button:focus-visible { outline: 2px solid var(--accent-1); outline-offset: 2px; }
529
+ .energy-chip { display: inline-flex; align-items: center; gap: 6px; padding: 6px 8px; border-radius: 999px; background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.15); color: var(--text-2); font-size: 12px; }
530
+ .energy-chip button { background: transparent; border: 0; color: var(--text-2); cursor: pointer; font-size: 14px; }
531
+
532
+ .toast {
533
+ position: fixed;
534
+ left: 50%;
535
+ bottom: 20px;
536
+ transform: translateX(-50%);
537
+ background: rgba(0,0,0,0.7);
538
+ color: #fff;
539
+ padding: 10px 14px;
540
+ border-radius: 8px;
541
+ font-size: 13px;
542
+ opacity: 0;
543
+ pointer-events: none;
544
+ transition: opacity var(--trans-med), transform var(--trans-med);
545
+ }
546
+
547
+ .toast.show { opacity: 1; transform: translateX(-50%) translateY(-4px); }
548
+
549
+ @media (max-width: 768px) {
550
+ .container {
551
+ flex-direction: column;
552
+ gap: 30px;
553
+ }
554
+
555
+ .card {
556
+ transform: none;
557
+ }
558
+
559
+ .controls { width: 100%; max-width: 420px; }
560
+ }
561
+
562
+ /* Reduced motion support */
563
+ @media (prefers-reduced-motion: reduce) {
564
+ * { animation: none !important; transition: none !important; }
565
+ }
566
+
567
+ body.reduced-motion * { animation: none !important; transition: none !important; }
568
+ body.no-glow .card, body.no-glow .shape, body.no-glow .shape-btn.active { box-shadow: none !important; filter: none !important; }
569
+ body.high-contrast { --text-2: #f2f2f2; --text-3: #e0e0e0; }
570
+ </style>
571
+ </head>
572
+ <body>
573
+ <div class="container">
574
+ <div class="card" role="img" aria-label="Card preview for AI/ML engineer">
575
+ <div class="card-border">
576
+ <div class="card-inner">
577
+ <div class="card-rarity" id="cardRarity" aria-hidden="true">★</div>
578
+ <div class="card-level">XP <span id="cardLevel" aria-live="polite">100</span></div>
579
+
580
+ <div class="card-header">
581
+ <div>
582
+ <div class="card-name" id="cardName" aria-live="polite">SMART PERSON</div>
583
+ <div class="card-title" id="cardTitle" aria-live="polite">Future Builder</div>
584
+ </div>
585
+ <div class="hp"><span class="label">HP</span> <span id="hpValue">1337</span></div>
586
+ <div class="type-badge" id="typeBadge" title="Stack">Ge</div>
587
+ </div>
588
+
589
+ <div class="artwork-frame">
590
+ <div class="shape-container">
591
+ <img id="artImage" class="art-img" alt="Card artwork"/>
592
+ <div class="shape circle" id="cardShape" aria-label="Avatar shape"></div>
593
+ </div>
594
+ </div>
595
+
596
+ <div class="attacks" id="attacks" aria-live="polite"></div>
597
+
598
+ <div class="card-description" id="cardDescription" aria-live="polite">Builds robust ML systems, from data to deployment.</div>
599
+
600
+ <div class="card-footer">
601
+ <div class="footer-box">
602
+ <div class="label">Secret Power</div>
603
+ <div class="content" id="secretPowerDisplay">GPU whisper</div>
604
+ </div>
605
+ <div class="footer-box">
606
+ <div class="label">Handle</div>
607
+ <div class="content" id="handleDisplay">@user</div>
608
+ </div>
609
+ </div>
610
+
611
+ <div class="card-watermark">HF</div>
612
+ <div class="card-info">
613
+ <span>© 2024 HF</span>
614
+ <span id="cardId">001/150</span>
615
+ <span>PROMO</span>
616
+ </div>
617
+ </div>
618
+ </div>
619
+ </div>
620
+
621
+ <div class="controls">
622
+ <h3>AI/ML Card Builder</h3>
623
+
624
+ <div class="control-group">
625
+ <label for="nameInput">Engineer</label>
626
+ <input type="text" id="nameInput" value="Smart Person" maxlength="40" aria-describedby="nameCount" />
627
+ <div class="helper-row"><span id="nameCount">0/40</span></div>
628
+ </div>
629
+
630
+ <div class="control-group">
631
+ <label for="titleInput">Role</label>
632
+ <input type="text" id="titleInput" value="Future Builder" maxlength="40" aria-describedby="titleCount" />
633
+ <div class="helper-row"><span id="titleCount">0/40</span></div>
634
+ </div>
635
+
636
+ <div class="control-group">
637
+ <label for="hpInput">HP</label>
638
+ <input type="number" id="hpInput" min="10" max="9999" step="10" value="1337" />
639
+ </div>
640
+
641
+ <div class="control-group">
642
+ <label for="typeInput">Stack</label>
643
+ <select id="typeInput"></select>
644
+ </div>
645
+
646
+ <div class="control-group">
647
+ <label for="levelInput">Experience</label>
648
+ <input type="range" id="levelInput" min="1" max="100" value="100" />
649
+ <div class="range-display">
650
+ <span>XP</span>
651
+ <span class="range-value" id="levelValue">100</span>
652
+ </div>
653
+ </div>
654
+
655
+ <div class="control-group">
656
+ <label id="shapeLabel">Avatar Shape</label>
657
+ <div class="shape-buttons" role="radiogroup" aria-labelledby="shapeLabel">
658
+ <button class="shape-btn active" role="radio" aria-checked="true" tabindex="0" data-shape="circle">Circle</button>
659
+ <button class="shape-btn" role="radio" aria-checked="false" tabindex="-1" data-shape="square">Square</button>
660
+ <button class="shape-btn" role="radio" aria-checked="false" tabindex="-1" data-shape="triangle">Triangle</button>
661
+ <button class="shape-btn" role="radio" aria-checked="false" tabindex="-1" data-shape="diamond">Diamond</button>
662
+ </div>
663
+ </div>
664
+
665
+ <div class="control-group">
666
+ <label for="artInput">Artwork (optional)</label>
667
+ <input type="file" id="artInput" accept="image/*" />
668
+ </div>
669
+
670
+ <div class="control-group">
671
+ <label>Skills</label>
672
+ <div id="attacksEditor"></div>
673
+ <button class="btn" id="addAttackBtn" type="button">+ Add Skill</button>
674
+ </div>
675
+
676
+ <div class="control-group">
677
+ <label>Identity</label>
678
+ <div class="attack-editor">
679
+ <div class="row-2">
680
+ <div>
681
+ <label for="secretPowerInput">Secret Power</label>
682
+ <input type="text" id="secretPowerInput" value="GPU whisper" placeholder="Enter symbol/text" maxlength="10" />
683
+ </div>
684
+ <div>
685
+ <label for="handleInput">Handle</label>
686
+ <input type="text" id="handleInput" value="@user" placeholder="@username" maxlength="20" />
687
+ </div>
688
+ </div>
689
+ </div>
690
+ </div>
691
+
692
+ <div class="control-group">
693
+ <label for="descriptionInput">Bio</label>
694
+ <textarea id="descriptionInput" maxlength="160" aria-describedby="descCount">Builds robust ML systems, from data to deployment.</textarea>
695
+ <div class="helper-row"><span id="descCount">0/160</span></div>
696
+ </div>
697
+
698
+ <div class="toggles" aria-label="Display options">
699
+ <label class="toggle"><input type="checkbox" id="toggleGlow" checked /> Enable glow effects</label>
700
+ <label class="toggle"><input type="checkbox" id="toggleMotion" /> Reduced motion</label>
701
+ <label class="toggle"><input type="checkbox" id="toggleContrast" /> High contrast text</label>
702
+ </div>
703
+
704
+ <div class="actions">
705
+ <button class="btn primary" id="saveBtn">Save</button>
706
+ <button class="btn" id="loadBtn">Load</button>
707
+ <button class="btn" id="shareBtn">Copy Link</button>
708
+ <button class="btn" id="exportBtn">Export PNG</button>
709
+ <button class="btn danger" id="resetBtn">Reset</button>
710
+ </div>
711
+ </div>
712
+ </div>
713
+
714
+ <button class="btn builder-toggle" id="toggleBuilderBtn" type="button" aria-pressed="false" aria-label="Toggle builder panel">Hide Builder</button>
715
+ <button class="btn export-toggle" id="exportFloatingBtn" type="button" aria-label="Export card as PNG">Export PNG</button>
716
+
717
+ <div class="toast" id="toast" role="status" aria-live="polite"></div>
718
+
719
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
720
+ <script>
721
+ // Get DOM elements
722
+ const cardName = document.getElementById('cardName');
723
+ const cardTitle = document.getElementById('cardTitle');
724
+ const cardLevel = document.getElementById('cardLevel');
725
+ const cardShape = document.getElementById('cardShape');
726
+ const cardDescription = document.getElementById('cardDescription');
727
+ const hpValue = document.getElementById('hpValue');
728
+ const typeBadge = document.getElementById('typeBadge');
729
+ const attacksEl = document.getElementById('attacks');
730
+ const secretPowerDisplay = document.getElementById('secretPowerDisplay');
731
+ const handleDisplay = document.getElementById('handleDisplay');
732
+ const artImage = document.getElementById('artImage');
733
+
734
+ const nameInput = document.getElementById('nameInput');
735
+ const titleInput = document.getElementById('titleInput');
736
+ const levelInput = document.getElementById('levelInput');
737
+ const hpInput = document.getElementById('hpInput');
738
+ const typeInput = document.getElementById('typeInput');
739
+ const secretPowerInput = document.getElementById('secretPowerInput');
740
+ const handleInput = document.getElementById('handleInput');
741
+ const artInput = document.getElementById('artInput');
742
+ const descriptionInput = document.getElementById('descriptionInput');
743
+
744
+ const levelValue = document.getElementById('levelValue');
745
+
746
+ const shapeButtons = document.querySelectorAll('.shape-btn');
747
+ const shapeGroup = document.querySelector('.shape-buttons');
748
+ const attacksEditor = document.getElementById('attacksEditor');
749
+ const addAttackBtn = document.getElementById('addAttackBtn');
750
+
751
+ const nameCount = document.getElementById('nameCount');
752
+ const titleCount = document.getElementById('titleCount');
753
+ const descCount = document.getElementById('descCount');
754
+
755
+ const toggleGlow = document.getElementById('toggleGlow');
756
+ const toggleMotion = document.getElementById('toggleMotion');
757
+ const toggleContrast = document.getElementById('toggleContrast');
758
+
759
+ const saveBtn = document.getElementById('saveBtn');
760
+ const loadBtn = document.getElementById('loadBtn');
761
+ const shareBtn = document.getElementById('shareBtn');
762
+ const exportBtn = document.getElementById('exportBtn');
763
+ const exportFloatingBtn = document.getElementById('exportFloatingBtn');
764
+ const resetBtn = document.getElementById('resetBtn');
765
+ const toast = document.getElementById('toast');
766
+ const toggleBuilderBtn = document.getElementById('toggleBuilderBtn');
767
+
768
+ // Update functions
769
+ function updateName() {
770
+ cardName.textContent = (nameInput.value || 'ENGINEER NAME').toUpperCase();
771
+ updateCounters();
772
+ }
773
+
774
+ function updateTitle() {
775
+ cardTitle.textContent = titleInput.value || 'ML Engineer';
776
+ updateCounters();
777
+ }
778
+
779
+ function updateLevel() {
780
+ const level = levelInput.value;
781
+ cardLevel.textContent = level;
782
+ levelValue.textContent = level;
783
+ }
784
+
785
+ function updateHPType() {
786
+ const hp = parseInt(hpInput.value || '0', 10);
787
+ hpValue.textContent = isNaN(hp) ? 0 : hp;
788
+ const t = typeInput.value || 'generalist';
789
+ const map = energyTypes[t] || energyTypes.generalist;
790
+ typeBadge.textContent = map.short;
791
+ typeBadge.className = 'type-badge';
792
+ typeBadge.style.background = map.bg;
793
+ renderFooter();
794
+ }
795
+
796
+ function updateDescription() {
797
+ cardDescription.textContent = descriptionInput.value || 'Builds robust ML systems, from data to deployment.';
798
+ updateCounters();
799
+ }
800
+
801
+ function updateShape(shape, opts = { focus: true }) {
802
+ cardShape.className = `shape ${shape}`;
803
+
804
+ // Update active button
805
+ shapeButtons.forEach(btn => {
806
+ btn.classList.remove('active');
807
+ btn.setAttribute('aria-checked', 'false');
808
+ btn.setAttribute('tabindex', '-1');
809
+ if (btn.dataset.shape === shape) {
810
+ btn.classList.add('active');
811
+ btn.setAttribute('aria-checked', 'true');
812
+ btn.setAttribute('tabindex', '0');
813
+ if (opts.focus) btn.focus({ preventScroll: true });
814
+ }
815
+ });
816
+ }
817
+
818
+ function updateCounters() {
819
+ nameCount.textContent = `${nameInput.value.length}/${nameInput.maxLength}`;
820
+ titleCount.textContent = `${titleInput.value.length}/${titleInput.maxLength}`;
821
+ descCount.textContent = `${descriptionInput.value.length}/${descriptionInput.maxLength}`;
822
+ }
823
+
824
+ // Energy/type map
825
+ const energyTypes = {
826
+ generalist: { key: 'generalist', name: 'Generalist', short: 'Ge', class: 'e-generalist', bg: 'linear-gradient(135deg,#f5f5f5,#e0e0e0)' },
827
+ python: { key: 'python', name: 'Python', short: 'Py', class: 'e-python', bg: 'linear-gradient(135deg,#3776AB,#FFD43B)' },
828
+ data: { key: 'data', name: 'Data', short: 'Ds', class: 'e-data', bg: 'linear-gradient(135deg,#34d399,#0ea5e9)' },
829
+ gpu: { key: 'gpu', name: 'GPU', short: 'GPU', class: 'e-gpu', bg: 'linear-gradient(135deg,#a7f3d0,#065f46)' },
830
+ cloud: { key: 'cloud', name: 'Cloud', short: 'Cl', class: 'e-cloud', bg: 'linear-gradient(135deg,#93c5fd,#e5e7eb)' },
831
+ research: { key: 'research', name: 'Research', short: 'R', class: 'e-research', bg: 'linear-gradient(135deg,#a78bfa,#f472b6)' },
832
+ math: { key: 'math', name: 'Math', short: '∑', class: 'e-math', bg: 'linear-gradient(135deg,#cbd5e1,#64748b)' },
833
+ systems: { key: 'systems', name: 'Systems', short: 'Sys', class: 'e-systems', bg: 'linear-gradient(135deg,#f59e0b,#b45309)' },
834
+ security: { key: 'security', name: 'Security', short: 'Sec', class: 'e-security', bg: 'linear-gradient(135deg,#111827,#ef4444)' },
835
+ mlops: { key: 'mlops', name: 'MLOps', short: 'Ops', class: 'e-mlops', bg: 'linear-gradient(135deg,#10b981,#3b82f6)' },
836
+ };
837
+
838
+ function renderFooter() {
839
+ secretPowerDisplay.textContent = secretPowerInput.value || '🚀';
840
+ handleDisplay.textContent = handleInput.value || '@drbh';
841
+ }
842
+
843
+ function renderAttacks() {
844
+ const state = getState();
845
+ attacksEl.innerHTML = state.attacks.map(atk => {
846
+ const cost = (atk.cost || []).map(k => {
847
+ const m = energyTypes[k] || energyTypes.generalist;
848
+ return `<span class="energy ${m.class}">${m.short}</span>`;
849
+ }).join('');
850
+ const dmg = atk.damage ? `<div class="atk-dmg">${atk.damage}</div>` : '';
851
+ const text = atk.text ? `<div class="atk-text">${atk.text}</div>` : '';
852
+ return `<div class="attack-row"><div class="attack-head"><div class="cost">${cost}</div><div class="atk-name">${atk.name || '—'}</div>${dmg}</div>${text}</div>`;
853
+ }).join('');
854
+ }
855
+
856
+ function populateTypeSelects() {
857
+ const keys = Object.keys(energyTypes);
858
+ // Populate type select
859
+ typeInput.innerHTML = keys.map(k => `<option value="${k}">${energyTypes[k].name}</option>`).join('');
860
+ if (!typeInput.value) typeInput.value = 'generalist';
861
+ }
862
+
863
+ function buildAttackEditor() {
864
+ const state = getState();
865
+ attacksEditor.innerHTML = '';
866
+ state.attacks.forEach((atk, index) => {
867
+ const wrap = document.createElement('div');
868
+ wrap.className = 'attack-editor';
869
+
870
+ const row = document.createElement('div');
871
+ row.className = 'row';
872
+ row.innerHTML = `
873
+ <input type="text" placeholder="Skill name" value="${atk.name || ''}" aria-label="Skill name"/>
874
+ <input type="text" placeholder="Impact (e.g. 30+)" value="${atk.damage || ''}" aria-label="Skill impact"/>
875
+ `;
876
+ const [nameEl, dmgEl] = row.querySelectorAll('input');
877
+ nameEl.addEventListener('input', () => { state.attacks[index].name = nameEl.value; applyState(state, { rebuildEditors: false }); setUrlHashFromState(); saveDraftDebounced(); });
878
+ dmgEl.addEventListener('input', () => { state.attacks[index].damage = dmgEl.value; applyState(state, { rebuildEditors: false }); setUrlHashFromState(); saveDraftDebounced(); });
879
+
880
+ const textArea = document.createElement('textarea');
881
+ textArea.placeholder = 'Skill details';
882
+ textArea.value = atk.text || '';
883
+ textArea.addEventListener('input', () => { state.attacks[index].text = textArea.value; applyState(state, { rebuildEditors: false }); setUrlHashFromState(); saveDraftDebounced(); });
884
+
885
+ const palette = document.createElement('div');
886
+ palette.className = 'energy-palette';
887
+ palette.innerHTML = Object.keys(energyTypes).map(k => {
888
+ const m = energyTypes[k];
889
+ return `<button type="button" data-k="${k}" class="energy ${m.class}" title="Add ${m.name}">${m.short}</button>`;
890
+ }).join('');
891
+ palette.addEventListener('click', (e) => {
892
+ const btn = e.target.closest('button[data-k]');
893
+ if (!btn) return;
894
+ const k = btn.dataset.k;
895
+ state.attacks[index].cost = state.attacks[index].cost || [];
896
+ state.attacks[index].cost.push(k);
897
+ applyState(state, { rebuildEditors: false });
898
+ renderChips();
899
+ setUrlHashFromState(); saveDraftDebounced();
900
+ });
901
+
902
+ const chips = document.createElement('div');
903
+ chips.className = 'energy-palette';
904
+ function renderChips() {
905
+ const current = state.attacks[index] || { cost: [] };
906
+ chips.innerHTML = (current.cost || []).map((k, i) => {
907
+ const m = energyTypes[k] || energyTypes.generalist;
908
+ return `<span class="energy-chip"><span class="energy ${m.class}">${m.short}</span><button type="button" data-i="${i}">×</button></span>`;
909
+ }).join('');
910
+ }
911
+ chips.addEventListener('click', (e) => {
912
+ const b = e.target.closest('button[data-i]');
913
+ if (!b) return;
914
+ const i = parseInt(b.dataset.i, 10);
915
+ state.attacks[index].cost.splice(i, 1);
916
+ applyState(state, { rebuildEditors: false });
917
+ renderChips();
918
+ setUrlHashFromState(); saveDraftDebounced();
919
+ });
920
+
921
+ const actions = document.createElement('div');
922
+ actions.style.display = 'flex';
923
+ actions.style.justifyContent = 'space-between';
924
+ actions.style.marginTop = '8px';
925
+ const removeBtn = document.createElement('button');
926
+ removeBtn.className = 'btn danger';
927
+ removeBtn.type = 'button';
928
+ removeBtn.textContent = 'Remove Skill';
929
+ removeBtn.addEventListener('click', () => {
930
+ state.attacks.splice(index, 1);
931
+ applyState(state); setUrlHashFromState(); saveDraftDebounced();
932
+ });
933
+ actions.appendChild(removeBtn);
934
+
935
+ wrap.appendChild(row);
936
+ wrap.appendChild(textArea);
937
+ const label1 = document.createElement('div'); label1.textContent = 'Add tokens:'; label1.style.marginTop = '12px'; label1.style.fontSize='12px'; label1.style.color='var(--text-3)';
938
+ wrap.appendChild(label1);
939
+ wrap.appendChild(palette);
940
+ const label2 = document.createElement('div'); label2.textContent = 'Tokens:'; label2.style.marginTop = '12px'; label2.style.fontSize='12px'; label2.style.color='var(--text-3)';
941
+ wrap.appendChild(label2);
942
+ wrap.appendChild(chips);
943
+ wrap.appendChild(actions);
944
+ attacksEditor.appendChild(wrap);
945
+
946
+ renderChips();
947
+ });
948
+ }
949
+
950
+ function showToast(message) {
951
+ toast.textContent = message;
952
+ toast.classList.add('show');
953
+ clearTimeout(showToast._t);
954
+ showToast._t = setTimeout(() => toast.classList.remove('show'), 1600);
955
+ }
956
+
957
+ function getState() {
958
+ return {
959
+ name: nameInput.value,
960
+ title: titleInput.value,
961
+ level: Number(levelInput.value),
962
+ hp: Number(hpInput.value) || 0,
963
+ type: typeInput.value,
964
+ secretPower: secretPowerInput.value,
965
+ handle: handleInput.value,
966
+ description: descriptionInput.value,
967
+ shape: [...shapeButtons].find(b => b.classList.contains('active'))?.dataset.shape || 'circle',
968
+ artwork: artImage?.src && artImage.style.display !== 'none' ? artImage.src : '',
969
+ attacks: window.__attacksState || [],
970
+ options: {
971
+ glow: toggleGlow.checked,
972
+ reducedMotion: toggleMotion.checked,
973
+ highContrast: toggleContrast.checked
974
+ },
975
+ ui: {
976
+ builderHidden: document.body.classList.contains('builder-hidden')
977
+ }
978
+ };
979
+ }
980
+
981
+ function applyState(state, options = {}) {
982
+ const opts = { rebuildEditors: true, renderAttacks: true, ...options };
983
+ if (!state) return;
984
+ nameInput.value = state.name ?? nameInput.value;
985
+ titleInput.value = state.title ?? titleInput.value;
986
+ levelInput.value = state.level ?? levelInput.value;
987
+ hpInput.value = state.hp ?? hpInput.value;
988
+ typeInput.value = state.type ?? typeInput.value;
989
+ secretPowerInput.value = state.secretPower ?? secretPowerInput.value;
990
+ handleInput.value = state.handle ?? handleInput.value;
991
+ descriptionInput.value = state.description ?? descriptionInput.value;
992
+ updateShape(state.shape ?? 'circle', { focus: false });
993
+
994
+ if (state.artwork) {
995
+ artImage.src = state.artwork;
996
+ artImage.style.display = 'block';
997
+ cardShape.style.display = 'none';
998
+ } else {
999
+ artImage.removeAttribute('src');
1000
+ artImage.style.display = 'none';
1001
+ cardShape.style.display = '';
1002
+ }
1003
+
1004
+ window.__attacksState = Array.isArray(state.attacks) ? state.attacks.slice(0, 4) : [];
1005
+ if (opts.renderAttacks) renderAttacks();
1006
+ if (opts.rebuildEditors) buildAttackEditor();
1007
+
1008
+ toggleGlow.checked = state.options?.glow ?? true;
1009
+ toggleMotion.checked = state.options?.reducedMotion ?? false;
1010
+ toggleContrast.checked = state.options?.highContrast ?? false;
1011
+ syncBodyOptions();
1012
+
1013
+ // Apply UI state (builder visibility)
1014
+ const hidden = !!state.ui?.builderHidden;
1015
+ document.body.classList.toggle('builder-hidden', hidden);
1016
+ if (typeof toggleBuilderBtn !== 'undefined' && toggleBuilderBtn) {
1017
+ toggleBuilderBtn.textContent = hidden ? 'Show Builder' : 'Hide Builder';
1018
+ toggleBuilderBtn.setAttribute('aria-pressed', String(hidden));
1019
+ }
1020
+
1021
+ updateName();
1022
+ updateTitle();
1023
+ updateLevel();
1024
+ updateHPType();
1025
+ updateDescription();
1026
+ renderFooter();
1027
+ }
1028
+
1029
+ function syncBodyOptions() {
1030
+ document.body.classList.toggle('no-glow', !toggleGlow.checked);
1031
+ document.body.classList.toggle('reduced-motion', toggleMotion.checked);
1032
+ document.body.classList.toggle('high-contrast', toggleContrast.checked);
1033
+ }
1034
+
1035
+ function saveToLocal() {
1036
+ try {
1037
+ localStorage.setItem('cardBuilderState', JSON.stringify(getState()));
1038
+ showToast('Saved');
1039
+ } catch (e) { showToast('Save failed'); }
1040
+ }
1041
+
1042
+ function loadFromLocal() {
1043
+ try {
1044
+ const raw = localStorage.getItem('cardBuilderState');
1045
+ if (!raw) { showToast('Nothing to load'); return; }
1046
+ const state = JSON.parse(raw);
1047
+ applyState(state);
1048
+ showToast('Loaded');
1049
+ } catch (e) { showToast('Load failed'); }
1050
+ }
1051
+
1052
+ function setUrlHashFromState() {
1053
+ try {
1054
+ const s = getState();
1055
+ // Do not serialize artwork into URL (too large)
1056
+ delete s.artwork;
1057
+ const json = JSON.stringify(s);
1058
+ const enc = encodeURIComponent(json);
1059
+ const newHash = `#state=${enc}`;
1060
+ const newUrl = `${location.pathname}${location.search}${newHash}`;
1061
+ if (location.hash !== newHash) history.replaceState(null, '', newUrl);
1062
+ } catch (_) {}
1063
+ }
1064
+
1065
+ function loadStateFromHash() {
1066
+ if (!location.hash.startsWith('#state=')) return null;
1067
+ try {
1068
+ const enc = location.hash.slice('#state='.length);
1069
+ const json = decodeURIComponent(enc);
1070
+ return JSON.parse(json);
1071
+ } catch (_) { return null; }
1072
+ }
1073
+
1074
+ async function exportCardAsPNG() {
1075
+ const cardEl = document.querySelector('.card');
1076
+ const rect = cardEl.getBoundingClientRect();
1077
+ const scale = 2; // Fixed scale to avoid memory issues
1078
+
1079
+ try {
1080
+ // Try html2canvas approach if available
1081
+ if (typeof html2canvas !== 'undefined') {
1082
+ // Create a wrapper div with padding to prevent cropping
1083
+ const wrapper = document.createElement('div');
1084
+ wrapper.style.padding = '20px';
1085
+ wrapper.style.display = 'inline-block';
1086
+ wrapper.style.backgroundColor = 'transparent';
1087
+
1088
+ // Clone the card
1089
+ const cardClone = cardEl.cloneNode(true);
1090
+
1091
+ // Fix glow effect for export - text-shadow doesn't work well with transparent text
1092
+ const nameEl = cardClone.querySelector('.card-name');
1093
+ if (nameEl) {
1094
+ nameEl.style.webkitTextFillColor = '#ffffff';
1095
+ nameEl.style.background = 'none';
1096
+ nameEl.style.color = '#ffffff';
1097
+ nameEl.style.textShadow = '0 0 15px rgba(255, 255, 255, 0.8), 0 0 30px rgba(255, 255, 255, 0.4)';
1098
+ }
1099
+
1100
+ // Fix artwork frame background for export - replace complex pseudo-element
1101
+ const artworkFrame = cardClone.querySelector('.artwork-frame');
1102
+ if (artworkFrame) {
1103
+ // Preserve original dimensions
1104
+ artworkFrame.style.width = '100%';
1105
+ artworkFrame.style.height = '200px';
1106
+ artworkFrame.style.position = 'relative';
1107
+
1108
+ // Create a simple gradient border that html2canvas can render
1109
+ artworkFrame.style.background = 'linear-gradient(45deg, #8b45ff, #ff1493, #00d4ff, #8b45ff)';
1110
+ artworkFrame.style.padding = '4px';
1111
+ artworkFrame.style.borderRadius = '22px';
1112
+
1113
+ // Create inner container for the actual artwork background
1114
+ const innerFrame = document.createElement('div');
1115
+ innerFrame.style.background = 'linear-gradient(135deg, #2d1b69, #11998e)';
1116
+ innerFrame.style.borderRadius = '18px';
1117
+ innerFrame.style.width = '100%';
1118
+ innerFrame.style.height = '100%';
1119
+ innerFrame.style.padding = '15px';
1120
+ innerFrame.style.boxSizing = 'border-box';
1121
+ innerFrame.style.position = 'relative';
1122
+
1123
+ // Move all children to inner frame
1124
+ while (artworkFrame.firstChild) {
1125
+ innerFrame.appendChild(artworkFrame.firstChild);
1126
+ }
1127
+ artworkFrame.appendChild(innerFrame);
1128
+
1129
+ // Ensure shape container maintains exact dimensions
1130
+ const shapeContainer = cardClone.querySelector('.shape-container');
1131
+ if (shapeContainer) {
1132
+ const originalContainer = cardEl.querySelector('.shape-container');
1133
+ const computedContainerStyle = window.getComputedStyle(originalContainer);
1134
+
1135
+ shapeContainer.style.position = 'relative';
1136
+ shapeContainer.style.width = '100%';
1137
+ shapeContainer.style.height = '100%';
1138
+ shapeContainer.style.overflow = 'hidden';
1139
+ shapeContainer.style.borderRadius = '12px';
1140
+ shapeContainer.style.display = 'flex';
1141
+ shapeContainer.style.alignItems = 'center';
1142
+ shapeContainer.style.justifyContent = 'center';
1143
+
1144
+ // If there's an image, remove the glass background
1145
+ const hasImage = cardClone.querySelector('.art-img')?.src;
1146
+ if (hasImage) {
1147
+ shapeContainer.style.background = 'transparent';
1148
+ shapeContainer.style.backdropFilter = 'none';
1149
+ }
1150
+ }
1151
+
1152
+ // Fix artwork image aspect ratio - maintain original dimensions
1153
+ const artImg = cardClone.querySelector('.art-img');
1154
+ if (artImg && artImg.src && artImg.style.display !== 'none') {
1155
+ // Copy computed styles from original to ensure exact match
1156
+ const originalImg = cardEl.querySelector('.art-img');
1157
+ const computedStyle = window.getComputedStyle(originalImg);
1158
+
1159
+ artImg.style.position = 'absolute';
1160
+ artImg.style.top = '0';
1161
+ artImg.style.left = '0';
1162
+ artImg.style.width = '100%';
1163
+ artImg.style.height = '100%';
1164
+ artImg.style.objectFit = computedStyle.objectFit || 'cover';
1165
+ artImg.style.objectPosition = computedStyle.objectPosition || 'center';
1166
+ artImg.style.borderRadius = '12px';
1167
+ artImg.style.display = 'block';
1168
+
1169
+ // Ensure the shape container doesn't interfere
1170
+ const shape = cardClone.querySelector('.shape');
1171
+ if (shape) {
1172
+ shape.style.display = 'none';
1173
+ }
1174
+ }
1175
+ }
1176
+
1177
+ wrapper.appendChild(cardClone);
1178
+
1179
+ // Temporarily add wrapper to document (off-screen to prevent flicker)
1180
+ wrapper.style.position = 'fixed';
1181
+ wrapper.style.left = '0';
1182
+ wrapper.style.top = '0';
1183
+ wrapper.style.zIndex = '-9999';
1184
+ wrapper.style.pointerEvents = 'none';
1185
+ document.body.appendChild(wrapper);
1186
+
1187
+ const canvas = await html2canvas(wrapper, {
1188
+ scale: scale,
1189
+ useCORS: true,
1190
+ allowTaint: true,
1191
+ backgroundColor: null,
1192
+ scrollX: 0,
1193
+ scrollY: 0
1194
+ });
1195
+
1196
+ // Remove wrapper
1197
+ document.body.removeChild(wrapper);
1198
+
1199
+ const dataUrl = canvas.toDataURL('image/png');
1200
+ const a = document.createElement('a');
1201
+ a.href = dataUrl;
1202
+ a.download = 'card.png';
1203
+ document.body.appendChild(a);
1204
+ a.click();
1205
+ a.remove();
1206
+ return;
1207
+ }
1208
+ } catch (e) {
1209
+ console.warn('html2canvas failed, falling back to manual canvas approach:', e);
1210
+ }
1211
+
1212
+ // Fallback: Direct canvas drawing with simplified approach
1213
+ const canvas = document.createElement('canvas');
1214
+ canvas.width = Math.floor(rect.width * scale);
1215
+ canvas.height = Math.floor(rect.height * scale);
1216
+ const ctx = canvas.getContext('2d');
1217
+
1218
+ // Set white background
1219
+ ctx.fillStyle = '#ffffff';
1220
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
1221
+
1222
+ // Scale the context
1223
+ ctx.scale(scale, scale);
1224
+
1225
+ // Try to draw the element directly using browser API
1226
+ try {
1227
+ // Create a simple representation
1228
+ ctx.fillStyle = '#333';
1229
+ ctx.font = '16px Arial';
1230
+ ctx.fillText('Card export - please use browser screenshot', 10, 30);
1231
+ ctx.fillText('for better quality', 10, 50);
1232
+
1233
+ const dataUrl = canvas.toDataURL('image/png');
1234
+ const a = document.createElement('a');
1235
+ a.href = dataUrl;
1236
+ a.download = 'card.png';
1237
+ document.body.appendChild(a);
1238
+ a.click();
1239
+ a.remove();
1240
+ } catch (e) {
1241
+ throw new Error('Canvas export failed: ' + e.message);
1242
+ }
1243
+ }
1244
+
1245
+ // Event listeners
1246
+ nameInput.addEventListener('input', () => { updateName(); setUrlHashFromState(); });
1247
+ titleInput.addEventListener('input', () => { updateTitle(); setUrlHashFromState(); });
1248
+ levelInput.addEventListener('input', () => { updateLevel(); setUrlHashFromState(); });
1249
+ hpInput.addEventListener('input', () => { updateHPType(); setUrlHashFromState(); });
1250
+ typeInput.addEventListener('change', () => { updateHPType(); setUrlHashFromState(); });
1251
+ secretPowerInput.addEventListener('input', () => { renderFooter(); setUrlHashFromState(); });
1252
+ handleInput.addEventListener('input', () => { renderFooter(); setUrlHashFromState(); });
1253
+ descriptionInput.addEventListener('input', () => { updateDescription(); setUrlHashFromState(); });
1254
+
1255
+ shapeButtons.forEach((button, idx) => {
1256
+ button.addEventListener('click', () => {
1257
+ updateShape(button.dataset.shape);
1258
+ setUrlHashFromState();
1259
+ });
1260
+ });
1261
+
1262
+ // Keyboard navigation for radiogroup
1263
+ shapeGroup.addEventListener('keydown', (e) => {
1264
+ const currentIndex = [...shapeButtons].findIndex(b => b.getAttribute('aria-checked') === 'true');
1265
+ if (['ArrowRight', 'ArrowDown'].includes(e.key)) {
1266
+ e.preventDefault();
1267
+ const next = (currentIndex + 1) % shapeButtons.length;
1268
+ shapeButtons[next].click();
1269
+ } else if (['ArrowLeft', 'ArrowUp'].includes(e.key)) {
1270
+ e.preventDefault();
1271
+ const prev = (currentIndex - 1 + shapeButtons.length) % shapeButtons.length;
1272
+ shapeButtons[prev].click();
1273
+ } else if ([' ', 'Enter'].includes(e.key)) {
1274
+ e.preventDefault();
1275
+ const active = [...shapeButtons][currentIndex] || shapeButtons[0];
1276
+ active.click();
1277
+ }
1278
+ });
1279
+
1280
+ // Artwork upload
1281
+ artInput.addEventListener('change', (e) => {
1282
+ const f = e.target.files && e.target.files[0];
1283
+ if (!f) return;
1284
+ const reader = new FileReader();
1285
+ reader.onload = () => {
1286
+ artImage.src = reader.result;
1287
+ artImage.style.display = 'block';
1288
+ cardShape.style.display = 'none';
1289
+ setUrlHashFromState();
1290
+ saveDraftDebounced();
1291
+ };
1292
+ reader.readAsDataURL(f);
1293
+ });
1294
+
1295
+ // Option toggles
1296
+ toggleGlow.addEventListener('change', () => { syncBodyOptions(); setUrlHashFromState(); });
1297
+ toggleMotion.addEventListener('change', () => { syncBodyOptions(); setUrlHashFromState(); });
1298
+ toggleContrast.addEventListener('change', () => { syncBodyOptions(); setUrlHashFromState(); });
1299
+
1300
+ // Actions
1301
+ saveBtn.addEventListener('click', saveToLocal);
1302
+ loadBtn.addEventListener('click', loadFromLocal);
1303
+ shareBtn.addEventListener('click', async () => {
1304
+ setUrlHashFromState();
1305
+ try {
1306
+ await navigator.clipboard.writeText(location.href);
1307
+ showToast('Link copied');
1308
+ } catch (e) { showToast('Copy failed'); }
1309
+ });
1310
+ exportBtn.addEventListener('click', async () => {
1311
+ try {
1312
+ await exportCardAsPNG();
1313
+ showToast('PNG exported');
1314
+ } catch (e) { console.error(e); showToast('Export failed'); }
1315
+ });
1316
+ exportFloatingBtn.addEventListener('click', async () => {
1317
+ try {
1318
+ await exportCardAsPNG();
1319
+ showToast('PNG exported');
1320
+ } catch (e) { console.error(e); showToast('Export failed'); }
1321
+ });
1322
+ resetBtn.addEventListener('click', () => {
1323
+ if (!confirm('Reset all values?')) return;
1324
+ nameInput.value = 'Smart Person';
1325
+ titleInput.value = 'Future Builder';
1326
+ levelInput.value = 100;
1327
+ hpInput.value = 1337;
1328
+ typeInput.value = 'generalist';
1329
+ secretPowerInput.value = 'GPU whisper';
1330
+ handleInput.value = '@user';
1331
+ descriptionInput.value = 'Builds robust ML systems, from data to deployment.';
1332
+ artImage.removeAttribute('src');
1333
+ artImage.style.display = 'none';
1334
+ cardShape.style.display = '';
1335
+ window.__attacksState = [
1336
+ {name: 'Code Review', cost: [{type: 'generalist', count: 2}], damage: '42'},
1337
+ {name: 'Debug Session', cost: [{type: 'generalist', count: 3}], damage: '1337'}
1338
+ ];
1339
+ attacksEl.innerHTML = '';
1340
+ attacksEditor.innerHTML = '';
1341
+ updateShape('circle');
1342
+ toggleGlow.checked = true;
1343
+ toggleMotion.checked = false;
1344
+ toggleContrast.checked = false;
1345
+ syncBodyOptions();
1346
+ updateName(); updateTitle(); updateLevel(); updateHPType(); updateDescription(); renderFooter();
1347
+ setUrlHashFromState();
1348
+ showToast('Reset');
1349
+ });
1350
+
1351
+ // Show/Hide Builder toggle
1352
+ toggleBuilderBtn.addEventListener('click', () => {
1353
+ const isHidden = document.body.classList.toggle('builder-hidden');
1354
+ toggleBuilderBtn.textContent = isHidden ? 'Show Builder' : 'Hide Builder';
1355
+ toggleBuilderBtn.setAttribute('aria-pressed', String(isHidden));
1356
+ setUrlHashFromState();
1357
+ });
1358
+
1359
+ // Initialize card
1360
+ updateName();
1361
+ updateTitle();
1362
+ updateLevel();
1363
+ updateHPType();
1364
+ updateDescription();
1365
+
1366
+ // Generate random card ID
1367
+ const cardNum = Math.floor(Math.random() * 150) + 1;
1368
+ document.getElementById('cardId').textContent = `${String(cardNum).padStart(3, '0')}/150`;
1369
+ updateCounters();
1370
+ syncBodyOptions();
1371
+ populateTypeSelects();
1372
+ window.__attacksState = window.__attacksState || [
1373
+ {name: 'Code Review', cost: [{type: 'generalist', count: 2}], damage: '42'},
1374
+ {name: 'Debug Session', cost: [{type: 'generalist', count: 3}], damage: '1337'}
1375
+ ];
1376
+ buildAttackEditor();
1377
+ renderAttacks();
1378
+
1379
+ addAttackBtn.addEventListener('click', () => {
1380
+ const state = getState();
1381
+ if (!Array.isArray(state.attacks)) state.attacks = [];
1382
+ if (state.attacks.length >= 4) { showToast('Max 4 skills'); return; }
1383
+ state.attacks.push({ name: 'New Skill', damage: '', text: '', cost: [] });
1384
+ applyState(state); setUrlHashFromState(); saveDraftDebounced();
1385
+ });
1386
+
1387
+ function saveDraftDebounced() { clearTimeout(saveDraftDebounced._t); saveDraftDebounced._t = setTimeout(saveToLocal, 300); }
1388
+
1389
+ // Load from URL hash or localStorage
1390
+ const hashState = loadStateFromHash();
1391
+ if (hashState) {
1392
+ applyState(hashState);
1393
+ } else {
1394
+ try { const local = JSON.parse(localStorage.getItem('cardBuilderState') || 'null'); applyState(local); } catch(_) {}
1395
+ }
1396
+
1397
+ // Add interactive tilt (all directions)
1398
+ const card = document.querySelector('.card');
1399
+ let tiltRAF = null;
1400
+ function setCardTransform(rx, ry, tz = 0) {
1401
+ card.style.transform = `perspective(1000px) rotateX(${rx}deg) rotateY(${ry}deg) translateZ(${tz}px)`;
1402
+ }
1403
+ function handleMove(e) {
1404
+ if (document.body.classList.contains('reduced-motion')) return;
1405
+ const rect = card.getBoundingClientRect();
1406
+ const px = (e.clientX - rect.left) / rect.width; // 0..1
1407
+ const py = (e.clientY - rect.top) / rect.height; // 0..1
1408
+ const dx = (px - 0.5) * 2; // -1..1
1409
+ const dy = (py - 0.5) * 2; // -1..1
1410
+ const maxTilt = 10; // degrees
1411
+ const rx = -dy * maxTilt; // invert Y for natural tilt
1412
+ const ry = dx * maxTilt;
1413
+ if (tiltRAF) cancelAnimationFrame(tiltRAF);
1414
+ tiltRAF = requestAnimationFrame(() => setCardTransform(rx, ry));
1415
+ }
1416
+ function handleEnter() {
1417
+ if (document.body.classList.contains('reduced-motion')) return;
1418
+ card.addEventListener('mousemove', handleMove);
1419
+ }
1420
+ function handleLeave() {
1421
+ card.removeEventListener('mousemove', handleMove);
1422
+ // Reset to subtle default tilt when leaving
1423
+ setCardTransform(5, 0, 0);
1424
+ }
1425
+ card.addEventListener('mouseenter', handleEnter);
1426
+ card.addEventListener('mouseleave', handleLeave);
1427
+ // Initialize default tilt
1428
+ setCardTransform(5, 0, 0);
1429
+ </script>
1430
+ </body>
1431
  </html>
1432
+