@tn3w/openage 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/src/ui.js ADDED
@@ -0,0 +1,1048 @@
1
+ export const FACE_SVG = `<svg viewBox="0 0 100 120"
2
+ fill="none" class="oa-face-svg">
3
+ <ellipse cx="50" cy="55" rx="35" ry="45"
4
+ stroke="currentColor" stroke-width="1.8"/>
5
+ <ellipse cx="36" cy="45" rx="5" ry="3.5"
6
+ fill="currentColor" class="oa-eye"/>
7
+ <ellipse cx="64" cy="45" rx="5" ry="3.5"
8
+ fill="currentColor" class="oa-eye"/>
9
+ <line x1="50" y1="52" x2="48" y2="62"
10
+ stroke="currentColor" stroke-width="1.5"
11
+ stroke-linecap="round"/>
12
+ <path d="M40 72 Q50 79 60 72"
13
+ stroke="currentColor" stroke-width="1.5"
14
+ stroke-linecap="round" fill="none"/>
15
+ </svg>`;
16
+
17
+ export const FACE_ICON_SVG = `<svg viewBox="0 0 100 120"
18
+ fill="none" class="oa-face-icon-svg">
19
+ <ellipse cx="50" cy="55" rx="35" ry="45"
20
+ stroke="currentColor" stroke-width="2"/>
21
+ <ellipse cx="36" cy="45" rx="5" ry="3.5"
22
+ fill="currentColor" class="oa-eye"/>
23
+ <ellipse cx="64" cy="45" rx="5" ry="3.5"
24
+ fill="currentColor" class="oa-eye"/>
25
+ <line x1="50" y1="52" x2="48" y2="62"
26
+ stroke="currentColor" stroke-width="1.65"
27
+ stroke-linecap="round"/>
28
+ <path d="M40 72 Q50 79 60 72"
29
+ stroke="currentColor" stroke-width="1.65"
30
+ stroke-linecap="round" fill="none"/>
31
+ </svg>`;
32
+
33
+ export const FACE_GUIDE_SVG = `<svg viewBox="0 0 100 120"
34
+ fill="none" class="oa-face-svg">
35
+ <ellipse cx="50" cy="55" rx="35" ry="45"
36
+ stroke="currentColor" stroke-width="1.8"
37
+ stroke-dasharray="6 4"/>
38
+ <ellipse cx="36" cy="45" rx="5" ry="3.5"
39
+ fill="currentColor" class="oa-eye"/>
40
+ <ellipse cx="64" cy="45" rx="5" ry="3.5"
41
+ fill="currentColor" class="oa-eye"/>
42
+ <line x1="50" y1="52" x2="48" y2="62"
43
+ stroke="currentColor" stroke-width="1.5"
44
+ stroke-linecap="round"/>
45
+ <path d="M40 72 Q50 79 60 72"
46
+ stroke="currentColor" stroke-width="1.5"
47
+ stroke-linecap="round" fill="none"/>
48
+ </svg>`;
49
+
50
+ export const CHECK_SVG = `<svg viewBox="0 0 24 24"
51
+ fill="none" stroke="currentColor" stroke-width="2.5"
52
+ stroke-linecap="round" stroke-linejoin="round">
53
+ <polyline points="20 6 9 17 4 12"/>
54
+ </svg>`;
55
+
56
+ export const CLOSE_SVG = `<svg viewBox="0 0 24 24"
57
+ fill="none" stroke="currentColor" stroke-width="2"
58
+ stroke-linecap="round">
59
+ <line x1="18" y1="6" x2="6" y2="18"/>
60
+ <line x1="6" y1="6" x2="18" y2="18"/>
61
+ </svg>`;
62
+
63
+ export const RETRY_SVG = `<svg viewBox="0 0 16 16"
64
+ xmlns="http://www.w3.org/2000/svg"
65
+ fill="currentColor">
66
+ <path d="m14.955 7.986.116.01a1 1 0 0 1 .85 1.13 8 8 0 0 1-13.374 4.728l-.84.84c-.63.63-1.707.184-1.707-.707V10h3.987c.89 0 1.337 1.077.707 1.707l-.731.731a6 6 0 0 0 8.347-.264 6 6 0 0 0 1.63-3.33 1 1 0 0 1 1.131-.848zM11.514.813a8 8 0 0 1 1.942 1.336l.837-.837c.63-.63 1.707-.184 1.707.707V6h-3.981c-.89 0-1.337-1.077-.707-1.707l.728-.729a6 6 0 0 0-9.98 3.591 1 1 0 1 1-1.98-.281A8 8 0 0 1 11.514.813"/>
67
+ </svg>`;
68
+
69
+ export const SPINNER_SVG = `<svg viewBox="0 0 24 24"
70
+ fill="none" stroke="currentColor" stroke-width="2.5">
71
+ <circle cx="12" cy="12" r="10" opacity="0.2"/>
72
+ <path d="M12 2 A10 10 0 0 1 22 12"
73
+ stroke-linecap="round">
74
+ <animateTransform attributeName="transform"
75
+ type="rotate" from="0 12 12" to="360 12 12"
76
+ dur="0.8s" repeatCount="indefinite"/>
77
+ </path>
78
+ </svg>`;
79
+
80
+ export const SHIELD_SVG = `<svg viewBox="0 0 24 24"
81
+ fill="none" stroke="currentColor" stroke-width="1.5">
82
+ <path d="M12 2l8 4v6c0 5.5-3.8 10-8 12
83
+ C7.8 22 4 17.5 4 12V6l8-4z"/>
84
+ <path d="M9 12l2 2 4-4" stroke-width="2"
85
+ stroke-linecap="round" stroke-linejoin="round"/>
86
+ </svg>`;
87
+
88
+ export function resolveTheme(theme) {
89
+ if (theme === 'light' || theme === 'dark') return theme;
90
+ return resolveAutoTheme();
91
+ }
92
+
93
+ export function watchTheme(host, theme) {
94
+ if (theme === 'light' || theme === 'dark') return;
95
+ if (typeof window === 'undefined') return;
96
+
97
+ const update = () => {
98
+ host.setAttribute('data-theme', resolveAutoTheme());
99
+ };
100
+
101
+ const cleanups = [];
102
+
103
+ for (const query of ['(prefers-color-scheme: dark)', '(prefers-color-scheme: light)']) {
104
+ const cleanup = watchMediaQuery(query, update);
105
+ if (cleanup) cleanups.push(cleanup);
106
+ }
107
+
108
+ if (typeof MutationObserver === 'function') {
109
+ const observer = new MutationObserver(update);
110
+ const options = {
111
+ attributes: true,
112
+ attributeFilter: ['class', 'data-theme', 'style'],
113
+ };
114
+
115
+ if (document.documentElement) {
116
+ observer.observe(document.documentElement, options);
117
+ }
118
+
119
+ if (document.body) {
120
+ observer.observe(document.body, options);
121
+ }
122
+
123
+ cleanups.push(() => observer.disconnect());
124
+ }
125
+
126
+ return () => {
127
+ for (const cleanup of cleanups) {
128
+ cleanup();
129
+ }
130
+ };
131
+ }
132
+
133
+ function resolveAutoTheme() {
134
+ if (typeof window === 'undefined') return 'dark';
135
+
136
+ const documentTheme = resolveDocumentTheme();
137
+ if (documentTheme) return documentTheme;
138
+
139
+ const systemTheme = resolveSystemTheme();
140
+ if (systemTheme) return systemTheme;
141
+
142
+ return 'dark';
143
+ }
144
+
145
+ function resolveSystemTheme() {
146
+ if (typeof window.matchMedia !== 'function') {
147
+ return null;
148
+ }
149
+
150
+ if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
151
+ return 'dark';
152
+ }
153
+
154
+ if (window.matchMedia('(prefers-color-scheme: light)').matches) {
155
+ return 'light';
156
+ }
157
+
158
+ return null;
159
+ }
160
+
161
+ function resolveDocumentTheme() {
162
+ if (typeof document === 'undefined') return null;
163
+
164
+ for (const element of [document.documentElement, document.body]) {
165
+ const theme = readThemeHint(element);
166
+ if (theme) return theme;
167
+ }
168
+
169
+ return null;
170
+ }
171
+
172
+ function readThemeHint(element) {
173
+ if (!element || typeof window.getComputedStyle !== 'function') {
174
+ return null;
175
+ }
176
+
177
+ const explicit = readExplicitTheme(element);
178
+ if (explicit) return explicit;
179
+
180
+ const styles = window.getComputedStyle(element);
181
+ const scheme = parseColorScheme(styles.colorScheme);
182
+ if (scheme) return scheme;
183
+
184
+ return parseBackgroundTheme(styles.backgroundColor);
185
+ }
186
+
187
+ function readExplicitTheme(element) {
188
+ const attrTheme = element.getAttribute('data-theme');
189
+ if (attrTheme === 'light' || attrTheme === 'dark') {
190
+ return attrTheme;
191
+ }
192
+
193
+ const className = element.className;
194
+ if (typeof className !== 'string') return null;
195
+
196
+ if (/\bdark\b|theme-dark|dark-theme/i.test(className)) {
197
+ return 'dark';
198
+ }
199
+
200
+ if (/\blight\b|theme-light|light-theme/i.test(className)) {
201
+ return 'light';
202
+ }
203
+
204
+ return null;
205
+ }
206
+
207
+ function parseColorScheme(value) {
208
+ if (!value || value === 'normal') return null;
209
+
210
+ const normalized = value.toLowerCase();
211
+ const hasDark = normalized.includes('dark');
212
+ const hasLight = normalized.includes('light');
213
+
214
+ if (hasDark && !hasLight) return 'dark';
215
+ if (hasLight && !hasDark) return 'light';
216
+
217
+ return null;
218
+ }
219
+
220
+ function parseBackgroundTheme(value) {
221
+ if (!value || value === 'transparent') return null;
222
+
223
+ const match = value.match(/rgba?\(([^)]+)\)/i);
224
+ if (!match) return null;
225
+
226
+ const parts = match[1]
227
+ .split(',')
228
+ .slice(0, 3)
229
+ .map((part) => Number.parseFloat(part.trim()));
230
+
231
+ if (parts.length !== 3 || parts.some(Number.isNaN)) {
232
+ return null;
233
+ }
234
+
235
+ const [red, green, blue] = parts;
236
+ const brightness = (red * 299 + green * 587 + blue * 114) / 1000;
237
+
238
+ if (brightness <= 140) return 'dark';
239
+ if (brightness >= 180) return 'light';
240
+
241
+ return null;
242
+ }
243
+
244
+ function watchMediaQuery(query, listener) {
245
+ if (typeof window.matchMedia !== 'function') {
246
+ return null;
247
+ }
248
+
249
+ const mediaQuery = window.matchMedia(query);
250
+
251
+ if (typeof mediaQuery.addEventListener === 'function') {
252
+ mediaQuery.addEventListener('change', listener);
253
+ return () => {
254
+ mediaQuery.removeEventListener('change', listener);
255
+ };
256
+ }
257
+
258
+ if (typeof mediaQuery.addListener === 'function') {
259
+ mediaQuery.addListener(listener);
260
+ return () => {
261
+ mediaQuery.removeListener(listener);
262
+ };
263
+ }
264
+
265
+ return null;
266
+ }
267
+
268
+ export const STYLES = `
269
+ :host {
270
+ --oa-bg: #0b0d11;
271
+ --oa-surface: #14161d;
272
+ --oa-border: #1f2230;
273
+ --oa-text: #e8e8ed;
274
+ --oa-text-muted: #7a7d8c;
275
+ --oa-accent: #4ae68a;
276
+ --oa-accent-dim: rgba(74, 230, 138, 0.12);
277
+ --oa-danger: #ef4444;
278
+ --oa-warn: #f59e0b;
279
+ --oa-radius: 16px;
280
+ --oa-font: 'DM Sans', system-ui, -apple-system,
281
+ sans-serif;
282
+ --oa-mono: 'Space Mono', monospace;
283
+
284
+ display: block;
285
+ font-family: var(--oa-font);
286
+ color: var(--oa-text);
287
+ line-height: 1.4;
288
+ }
289
+
290
+ :host([data-theme="light"]) {
291
+ --oa-bg: #f8f9fb;
292
+ --oa-surface: #ffffff;
293
+ --oa-border: #e2e4ea;
294
+ --oa-text: #1a1c24;
295
+ --oa-text-muted: #6b6e7b;
296
+ --oa-accent: #22c55e;
297
+ --oa-accent-dim: rgba(34, 197, 94, 0.1);
298
+ }
299
+
300
+ * { box-sizing: border-box; margin: 0; padding: 0; }
301
+
302
+ /* ── Checkbox ────────────────────────────────── */
303
+
304
+ .oa-checkbox {
305
+ display: flex;
306
+ align-items: center;
307
+ gap: 10px;
308
+ padding: 10px 14px;
309
+ background: var(--oa-surface);
310
+ border: 2px solid var(--oa-border);
311
+ border-radius: var(--oa-radius);
312
+ cursor: pointer;
313
+ user-select: none;
314
+ transition: border-color 0.2s, box-shadow 0.2s;
315
+ }
316
+
317
+ .oa-checkbox:hover {
318
+ border-color: var(--oa-text-muted);
319
+ }
320
+
321
+ .oa-checkbox.oa-verified {
322
+ border-color: var(--oa-accent);
323
+ cursor: default;
324
+ }
325
+
326
+ .oa-checkbox.oa-failed {
327
+ border-color: var(--oa-danger);
328
+ }
329
+
330
+ .oa-checkbox.oa-retry {
331
+ border-color: var(--oa-danger);
332
+ }
333
+
334
+ .oa-checkbox.oa-expired {
335
+ border-color: var(--oa-warn);
336
+ }
337
+
338
+ .oa-checkbox.oa-loading {
339
+ align-items: center;
340
+ }
341
+
342
+ .oa-check-box {
343
+ width: 24px;
344
+ height: 24px;
345
+ border: 2px solid var(--oa-border);
346
+ border-radius: 6px;
347
+ display: flex;
348
+ align-items: center;
349
+ justify-content: center;
350
+ flex-shrink: 0;
351
+ transition: all 0.2s;
352
+ }
353
+
354
+ .oa-loading .oa-check-box {
355
+ border-color: transparent;
356
+ background: transparent;
357
+ }
358
+
359
+ .oa-loading .oa-check-box .oa-spinner {
360
+ width: 100%;
361
+ height: 100%;
362
+ display: flex;
363
+ align-items: center;
364
+ justify-content: center;
365
+ transform: translateY(2px);
366
+ }
367
+
368
+ .oa-verified .oa-check-box {
369
+ background: var(--oa-accent);
370
+ border-color: var(--oa-accent);
371
+ color: var(--oa-bg);
372
+ }
373
+
374
+ .oa-failed .oa-check-box {
375
+ background: var(--oa-danger);
376
+ border-color: var(--oa-danger);
377
+ color: white;
378
+ }
379
+
380
+ .oa-retry .oa-check-box {
381
+ background: var(--oa-danger);
382
+ border-color: var(--oa-danger);
383
+ color: white;
384
+ }
385
+
386
+ .oa-check-box svg {
387
+ width: 14px;
388
+ height: 14px;
389
+ }
390
+
391
+ .oa-check-box .oa-spinner svg {
392
+ width: 18px;
393
+ height: 18px;
394
+ }
395
+
396
+ .oa-label {
397
+ display: flex;
398
+ flex-direction: column;
399
+ gap: 1px;
400
+ flex: 1;
401
+ min-width: 0;
402
+ }
403
+
404
+ .oa-label-text {
405
+ font-size: 13px;
406
+ font-weight: 600;
407
+ }
408
+
409
+ .oa-right-section {
410
+ display: flex;
411
+ flex-direction: column;
412
+ align-items: center;
413
+ gap: 2px;
414
+ flex-shrink: 0;
415
+ margin-left: auto;
416
+ }
417
+
418
+ .oa-face-icon-wrap {
419
+ width: 18px;
420
+ height: 22px;
421
+ color: var(--oa-text-muted);
422
+ display: flex;
423
+ align-items: center;
424
+ justify-content: center;
425
+ flex-shrink: 0;
426
+ }
427
+
428
+ .oa-face-icon-wrap .oa-face-icon-svg {
429
+ width: 100%;
430
+ height: 100%;
431
+ }
432
+
433
+ .oa-face-icon-wrap .oa-eye {
434
+ transform-box: fill-box;
435
+ transform-origin: center;
436
+ animation: oa-idle-blink 5s ease-in-out infinite;
437
+ }
438
+
439
+ .oa-face-icon-wrap .oa-face-icon-svg {
440
+ animation: oa-breathe 4s ease-in-out infinite;
441
+ }
442
+
443
+ .oa-branding-row {
444
+ display: flex;
445
+ align-items: center;
446
+ justify-content: center;
447
+ gap: 4px;
448
+ }
449
+
450
+ .oa-branding-link {
451
+ font-size: 10px;
452
+ font-weight: 700;
453
+ font-family: var(--oa-mono);
454
+ color: var(--oa-text-muted);
455
+ text-decoration: none;
456
+ letter-spacing: -0.02em;
457
+ cursor: pointer;
458
+ }
459
+
460
+ .oa-branding-link:hover {
461
+ color: var(--oa-text);
462
+ }
463
+
464
+ .oa-links-row {
465
+ display: flex;
466
+ gap: 6px;
467
+ justify-content: center;
468
+ }
469
+
470
+ .oa-links-row a {
471
+ font-size: 9px;
472
+ color: var(--oa-text-muted);
473
+ text-decoration: none;
474
+ opacity: 0.7;
475
+ cursor: pointer;
476
+ }
477
+
478
+ .oa-links-row a:hover {
479
+ opacity: 1;
480
+ text-decoration: underline;
481
+ }
482
+
483
+ .oa-compact .oa-label-text {
484
+ font-size: 11px;
485
+ }
486
+
487
+ .oa-compact .oa-face-icon-wrap {
488
+ width: 16px;
489
+ height: 20px;
490
+ }
491
+
492
+ /* ── Error banner (inline in checkbox) ───────── */
493
+
494
+ .oa-error-banner {
495
+ display: flex;
496
+ align-items: center;
497
+ gap: 8px;
498
+ padding: 8px 12px;
499
+ margin-top: 6px;
500
+ background: rgba(239, 68, 68, 0.08);
501
+ border: 1px solid rgba(239, 68, 68, 0.2);
502
+ border-radius: 8px;
503
+ font-size: 12px;
504
+ color: var(--oa-danger);
505
+ animation: oa-fade-in 0.3s ease;
506
+ }
507
+
508
+ .oa-error-banner button {
509
+ margin-left: auto;
510
+ background: none;
511
+ border: 1px solid var(--oa-danger);
512
+ border-radius: 6px;
513
+ color: var(--oa-danger);
514
+ font-size: 11px;
515
+ font-weight: 600;
516
+ padding: 3px 10px;
517
+ cursor: pointer;
518
+ font-family: var(--oa-font);
519
+ flex-shrink: 0;
520
+ }
521
+
522
+ .oa-error-banner button:hover {
523
+ background: rgba(239, 68, 68, 0.1);
524
+ }
525
+
526
+ /* ── Popup / Modal shell ─────────────────────── */
527
+
528
+ .oa-popup {
529
+ position: fixed;
530
+ z-index: 100000;
531
+ background: var(--oa-bg);
532
+ border: 1px solid var(--oa-border);
533
+ border-radius: var(--oa-radius);
534
+ box-shadow: 0 20px 60px rgba(0,0,0,0.4),
535
+ 0 0 0 1px rgba(0,0,0,0.08);
536
+ width: 340px;
537
+ max-height: 90vh;
538
+ overflow: hidden;
539
+ animation: oa-popup-in 0.25s ease;
540
+ display: flex;
541
+ flex-direction: column;
542
+ }
543
+
544
+ .oa-modal-overlay {
545
+ position: fixed;
546
+ inset: 0;
547
+ z-index: 99999;
548
+ background: rgba(0,0,0,0.65);
549
+ display: flex;
550
+ align-items: center;
551
+ justify-content: center;
552
+ animation: oa-fade-in 0.2s ease;
553
+ backdrop-filter: blur(6px);
554
+ }
555
+
556
+ .oa-modal {
557
+ background: var(--oa-bg);
558
+ border: 1px solid var(--oa-border);
559
+ border-radius: var(--oa-radius);
560
+ width: 360px;
561
+ max-width: 95vw;
562
+ max-height: 95vh;
563
+ overflow: hidden;
564
+ animation: oa-slide-in 0.3s ease;
565
+ display: flex;
566
+ flex-direction: column;
567
+ }
568
+
569
+ /* ── Header ──────────────────────────────────── */
570
+
571
+ .oa-header {
572
+ display: flex;
573
+ align-items: center;
574
+ justify-content: space-between;
575
+ padding: 10px 14px;
576
+ border-bottom: 1px solid var(--oa-border);
577
+ flex-shrink: 0;
578
+ }
579
+
580
+ .oa-logo {
581
+ font-family: var(--oa-mono);
582
+ font-size: 0.85rem;
583
+ letter-spacing: -0.03em;
584
+ color: var(--oa-text-muted);
585
+ text-decoration: none;
586
+ cursor: pointer;
587
+ }
588
+
589
+ .oa-logo:hover {
590
+ color: var(--oa-text);
591
+ }
592
+
593
+ .oa-logo strong {
594
+ color: var(--oa-text);
595
+ font-weight: 700;
596
+ }
597
+
598
+ .oa-badge {
599
+ font-size: 0.55rem;
600
+ font-weight: 600;
601
+ text-transform: uppercase;
602
+ letter-spacing: 0.08em;
603
+ color: var(--oa-accent);
604
+ background: var(--oa-accent-dim);
605
+ padding: 2px 6px;
606
+ border-radius: 100px;
607
+ margin-left: 6px;
608
+ }
609
+
610
+ .oa-title {
611
+ display: flex;
612
+ align-items: center;
613
+ gap: 6px;
614
+ }
615
+
616
+ .oa-close-btn {
617
+ width: 28px;
618
+ height: 28px;
619
+ border: none;
620
+ background: none;
621
+ cursor: pointer;
622
+ display: flex;
623
+ align-items: center;
624
+ justify-content: center;
625
+ border-radius: 8px;
626
+ color: var(--oa-text-muted);
627
+ transition: background 0.15s, color 0.15s;
628
+ }
629
+
630
+ .oa-close-btn:hover {
631
+ background: var(--oa-border);
632
+ color: var(--oa-text);
633
+ }
634
+
635
+ .oa-close-btn svg {
636
+ width: 16px;
637
+ height: 16px;
638
+ }
639
+
640
+ /* ── Body (viewport) ─────────────────────────── */
641
+
642
+ .oa-body {
643
+ flex: 1;
644
+ position: relative;
645
+ overflow: hidden;
646
+ }
647
+
648
+ /* ── Hero / start screen ─────────────────────── */
649
+
650
+ .oa-hero {
651
+ display: flex;
652
+ flex-direction: column;
653
+ align-items: center;
654
+ justify-content: center;
655
+ gap: 1rem;
656
+ padding: 2rem 1.2rem 1.2rem;
657
+ background: var(--oa-surface);
658
+ border: 1px solid var(--oa-border);
659
+ border-radius: var(--oa-radius);
660
+ margin: 10px;
661
+ animation: oa-fade-in 0.4s ease;
662
+ }
663
+
664
+ .oa-hero-icon {
665
+ width: 80px;
666
+ height: 96px;
667
+ color: var(--oa-text-muted);
668
+ }
669
+
670
+ .oa-hero-icon .oa-face-svg.oa-idle {
671
+ animation: oa-breathe 4s ease-in-out infinite;
672
+ }
673
+
674
+ .oa-hero-icon .oa-eye {
675
+ transform-box: fill-box;
676
+ transform-origin: center;
677
+ animation: oa-idle-blink 5s ease-in-out infinite;
678
+ }
679
+
680
+ .oa-hero-status {
681
+ color: var(--oa-text-muted);
682
+ font-size: 0.8rem;
683
+ font-weight: 500;
684
+ text-align: center;
685
+ min-height: 1.4em;
686
+ }
687
+
688
+ .oa-hero-privacy {
689
+ font-size: 0.68rem;
690
+ color: var(--oa-text-muted);
691
+ text-align: center;
692
+ line-height: 1.5;
693
+ max-width: 260px;
694
+ opacity: 0.7;
695
+ }
696
+
697
+ .oa-hero-privacy svg {
698
+ width: 10px;
699
+ height: 10px;
700
+ vertical-align: -2px;
701
+ margin-right: 2px;
702
+ }
703
+
704
+ /* ── Start / Retry button ────────────────────── */
705
+
706
+ .oa-actions {
707
+ display: flex;
708
+ justify-content: center;
709
+ padding: 0 14px 14px;
710
+ flex-shrink: 0;
711
+ }
712
+
713
+ .oa-btn {
714
+ font-family: var(--oa-font);
715
+ padding: 0.55rem 1.5rem;
716
+ border: none;
717
+ border-radius: 10px;
718
+ font-size: 0.8rem;
719
+ font-weight: 600;
720
+ cursor: pointer;
721
+ background: var(--oa-accent);
722
+ color: var(--oa-bg);
723
+ transition: transform 0.12s, opacity 0.15s;
724
+ width: 100%;
725
+ max-width: 240px;
726
+ }
727
+
728
+ .oa-btn:hover { opacity: 0.88; }
729
+ .oa-btn:active { transform: scale(0.97); }
730
+
731
+ /* ── Video area ──────────────────────────────── */
732
+
733
+ .oa-video-area {
734
+ position: relative;
735
+ width: 100%;
736
+ aspect-ratio: 3/4;
737
+ max-height: 55vh;
738
+ background: var(--oa-surface);
739
+ overflow: hidden;
740
+ border-radius: var(--oa-radius);
741
+ margin: 10px;
742
+ width: calc(100% - 20px);
743
+ border: 1px solid var(--oa-border);
744
+ animation: oa-fade-in 0.3s ease;
745
+ }
746
+
747
+ .oa-video-area video {
748
+ width: 100%;
749
+ height: 100%;
750
+ object-fit: cover;
751
+ transform: scaleX(-1);
752
+ }
753
+
754
+ /* ── Face guide overlay ──────────────────────── */
755
+
756
+ .oa-face-guide {
757
+ position: absolute;
758
+ top: 50%;
759
+ left: 50%;
760
+ transform: translate(-50%, -50%);
761
+ width: 80%;
762
+ max-width: 360px;
763
+ aspect-ratio: 5/6;
764
+ perspective: 400px;
765
+ color: rgba(255, 255, 255, 0.45);
766
+ pointer-events: none;
767
+ z-index: 2;
768
+ transition: opacity 0.4s ease;
769
+ }
770
+
771
+ .oa-face-guide .oa-face-svg {
772
+ width: 100%;
773
+ height: 100%;
774
+ transform-origin: center center;
775
+ filter: drop-shadow(
776
+ 0 0 16px rgba(15, 23, 42, 0.16)
777
+ );
778
+ }
779
+
780
+ .oa-face-guide .oa-eye {
781
+ transform-box: fill-box;
782
+ transform-origin: center;
783
+ }
784
+
785
+ .oa-face-guide[data-task="turn-left"] .oa-face-svg {
786
+ animation: oa-turn-left 2s ease-in-out infinite;
787
+ }
788
+ .oa-face-guide[data-task="turn-right"] .oa-face-svg {
789
+ animation: oa-turn-right 2s ease-in-out infinite;
790
+ }
791
+ .oa-face-guide[data-task="nod"] .oa-face-svg {
792
+ animation: oa-nod 2s ease-in-out infinite;
793
+ }
794
+ .oa-face-guide[data-task="blink-twice"] .oa-eye {
795
+ animation: oa-blink 2.4s ease-in-out infinite;
796
+ }
797
+ .oa-face-guide[data-task="move-closer"] .oa-face-svg {
798
+ animation: oa-closer 2.5s ease-in-out infinite;
799
+ }
800
+
801
+ /* ── Challenge HUD (bottom gradient overlay) ── */
802
+
803
+ .oa-challenge-hud {
804
+ position: absolute;
805
+ bottom: 0;
806
+ left: 0;
807
+ right: 0;
808
+ padding: 1rem 0.8rem 0.6rem;
809
+ background: linear-gradient(
810
+ to top,
811
+ rgba(11, 13, 17, 0.92) 0%,
812
+ rgba(11, 13, 17, 0) 100%
813
+ );
814
+ z-index: 3;
815
+ pointer-events: none;
816
+ }
817
+
818
+ .oa-challenge-text {
819
+ font-size: 0.85rem;
820
+ font-weight: 600;
821
+ text-align: center;
822
+ color: var(--oa-text);
823
+ text-shadow: 0 1px 6px rgba(0, 0, 0, 0.7);
824
+ margin-bottom: 0.4rem;
825
+ }
826
+
827
+ .oa-challenge-bar {
828
+ height: 3px;
829
+ background: rgba(255, 255, 255, 0.1);
830
+ border-radius: 2px;
831
+ overflow: hidden;
832
+ }
833
+
834
+ .oa-challenge-fill {
835
+ height: 100%;
836
+ background: var(--oa-accent);
837
+ border-radius: 2px;
838
+ transition: width 0.25s ease;
839
+ }
840
+
841
+ /* ── Video status (top gradient overlay) ─────── */
842
+
843
+ .oa-video-status {
844
+ position: absolute;
845
+ top: 0;
846
+ left: 0;
847
+ right: 0;
848
+ padding: 0.8rem 0.8rem 1.2rem;
849
+ background: linear-gradient(
850
+ to bottom,
851
+ rgba(11, 13, 17, 0.85) 0%,
852
+ rgba(11, 13, 17, 0) 100%
853
+ );
854
+ z-index: 3;
855
+ pointer-events: none;
856
+ }
857
+
858
+ .oa-video-status p {
859
+ font-size: 0.75rem;
860
+ font-weight: 500;
861
+ text-align: center;
862
+ color: var(--oa-text-muted);
863
+ text-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
864
+ }
865
+
866
+ /* ── Result area ─────────────────────────────── */
867
+
868
+ .oa-result {
869
+ display: flex;
870
+ flex-direction: column;
871
+ align-items: center;
872
+ gap: 0.5rem;
873
+ padding: 2rem 1.2rem;
874
+ background: var(--oa-surface);
875
+ border: 1px solid var(--oa-border);
876
+ border-radius: var(--oa-radius);
877
+ margin: 10px;
878
+ animation: oa-fade-in 0.4s ease;
879
+ }
880
+
881
+ .oa-result-icon {
882
+ font-size: 2.4rem;
883
+ line-height: 1;
884
+ }
885
+
886
+ .oa-result-text {
887
+ font-size: 0.85rem;
888
+ font-weight: 500;
889
+ text-align: center;
890
+ }
891
+
892
+ .oa-result-pass { color: var(--oa-accent); }
893
+ .oa-result-fail { color: var(--oa-danger); }
894
+ .oa-result-retry { color: var(--oa-warn); }
895
+
896
+ .oa-hidden { display: none !important; }
897
+
898
+ /* ── Animations ──────────────────────────────── */
899
+
900
+ @keyframes oa-popup-in {
901
+ from { opacity: 0; transform: scale(0.95); }
902
+ to { opacity: 1; transform: scale(1); }
903
+ }
904
+
905
+ @keyframes oa-fade-in {
906
+ from { opacity: 0; transform: translateY(6px); }
907
+ to { opacity: 1; transform: translateY(0); }
908
+ }
909
+
910
+ @keyframes oa-slide-in {
911
+ from { opacity: 0; transform: translateY(16px); }
912
+ to { opacity: 1; transform: translateY(0); }
913
+ }
914
+
915
+ @keyframes oa-breathe {
916
+ 0%, 100% { transform: scale(1); }
917
+ 50% { transform: scale(1.04); }
918
+ }
919
+
920
+ @keyframes oa-idle-blink {
921
+ 0%, 42%, 48%, 100% { transform: scaleY(1); }
922
+ 44%, 46% { transform: scaleY(0.05); }
923
+ }
924
+
925
+ @keyframes oa-turn-left {
926
+ 0%, 100% { transform: rotateY(0); }
927
+ 35%, 65% { transform: rotateY(-30deg); }
928
+ }
929
+
930
+ @keyframes oa-turn-right {
931
+ 0%, 100% { transform: rotateY(0); }
932
+ 35%, 65% { transform: rotateY(30deg); }
933
+ }
934
+
935
+ @keyframes oa-nod {
936
+ 0%, 100% { transform: rotateX(0); }
937
+ 30%, 50% { transform: rotateX(25deg); }
938
+ }
939
+
940
+ @keyframes oa-blink {
941
+ 0%, 18%, 32%, 50%, 100% { transform: scaleY(1); }
942
+ 22%, 28% { transform: scaleY(0.05); }
943
+ 40%, 46% { transform: scaleY(0.05); }
944
+ }
945
+
946
+ @keyframes oa-closer {
947
+ 0%, 100% { transform: scale(1); }
948
+ 35%, 55% { transform: scale(1.3); }
949
+ }
950
+ `;
951
+
952
+ export function checkboxTemplate(labelText) {
953
+ return `
954
+ <div class="oa-widget-wrap">
955
+ <div class="oa-checkbox" role="checkbox"
956
+ aria-checked="false" tabindex="0">
957
+ <div class="oa-check-box"></div>
958
+ <div class="oa-label">
959
+ <span class="oa-label-text">${labelText}</span>
960
+ </div>
961
+ <div class="oa-right-section">
962
+ <div class="oa-branding-row">
963
+ <div class="oa-face-icon-wrap">
964
+ ${FACE_ICON_SVG}
965
+ </div>
966
+ <a class="oa-branding-link"
967
+ href="https://github.com/tn3w/OpenAge"
968
+ target="_blank" rel="noopener">OpenAge</a>
969
+ </div>
970
+ <div class="oa-links-row">
971
+ <a href="https://github.com/tn3w/OpenAge"
972
+ target="_blank"
973
+ rel="noopener">Terms</a>
974
+ <a href="https://github.com/tn3w/OpenAge"
975
+ target="_blank"
976
+ rel="noopener">Privacy</a>
977
+ </div>
978
+ </div>
979
+ </div>
980
+ <div class="oa-error-slot"></div>
981
+ </div>
982
+ `;
983
+ }
984
+
985
+ export function heroTemplate(statusText) {
986
+ return `
987
+ <div class="oa-hero">
988
+ <div class="oa-hero-icon">
989
+ ${FACE_SVG.replace('class="oa-face-svg"', 'class="oa-face-svg oa-idle"')}
990
+ </div>
991
+ <p class="oa-hero-status">${statusText}</p>
992
+ <p class="oa-hero-privacy">
993
+ ${SHIELD_SVG}
994
+ Open-source &amp; privacy-focused.
995
+ No photos or camera data leave your device.
996
+ </p>
997
+ </div>
998
+ `;
999
+ }
1000
+
1001
+ export function challengeTemplate() {
1002
+ return `
1003
+ <div class="oa-video-area">
1004
+ <video autoplay playsinline muted></video>
1005
+ <div class="oa-face-guide">
1006
+ ${FACE_GUIDE_SVG}
1007
+ </div>
1008
+ <div class="oa-challenge-hud oa-hidden">
1009
+ <p class="oa-challenge-text"></p>
1010
+ <div class="oa-challenge-bar">
1011
+ <div class="oa-challenge-fill"
1012
+ style="width:0%"></div>
1013
+ </div>
1014
+ </div>
1015
+ <div class="oa-video-status">
1016
+ <p></p>
1017
+ </div>
1018
+ </div>
1019
+ `;
1020
+ }
1021
+
1022
+ export function resultTemplate(outcome, message) {
1023
+ const icons = {
1024
+ fail: '✕',
1025
+ retry: '↻',
1026
+ };
1027
+ const classes = {
1028
+ fail: 'oa-result-fail',
1029
+ retry: 'oa-result-retry',
1030
+ };
1031
+ return `
1032
+ <div class="oa-result ${classes[outcome] || ''}">
1033
+ <div class="oa-result-icon">
1034
+ ${icons[outcome] || '?'}
1035
+ </div>
1036
+ <div class="oa-result-text">${message}</div>
1037
+ </div>
1038
+ `;
1039
+ }
1040
+
1041
+ export function errorBannerTemplate(message) {
1042
+ return `
1043
+ <div class="oa-error-banner">
1044
+ <span>${message}</span>
1045
+ <button class="oa-retry-btn">Retry</button>
1046
+ </div>
1047
+ `;
1048
+ }