elder_docs 0.1.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.
@@ -0,0 +1,481 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <link rel="preconnect" href="https://fonts.googleapis.com">
7
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
8
+ <title>ElderDocs - UI Configuration</title>
9
+ <script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
10
+ <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
11
+ <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
12
+ <style>
13
+ * { box-sizing: border-box; margin: 0; padding: 0; }
14
+ body { font-family: system-ui, -apple-system, sans-serif; background: #fff; }
15
+ .container { max-width: 800px; margin: 0 auto; padding: 2rem; }
16
+ .surface { background: #fff; border: 3px solid #000; padding: 2rem; margin-bottom: 1rem; }
17
+ .btn { padding: 0.75rem 1.5rem; border: 3px solid #000; background: #000; color: #fff; cursor: pointer; font-weight: bold; text-transform: uppercase; }
18
+ .btn:hover { background: #333; }
19
+ .btn-secondary { background: #fff; color: #000; }
20
+ .input { width: 100%; padding: 0.5rem; border: 2px solid #000; margin-bottom: 1rem; }
21
+ .error { background: #fee; border: 2px solid #f00; padding: 1rem; margin-bottom: 1rem; color: #c00; }
22
+ .success { background: #efe; border: 2px solid #0a0; padding: 1rem; margin-bottom: 1rem; color: #060; }
23
+ h1 { font-size: 2rem; margin-bottom: 1rem; }
24
+ h2 { font-size: 1.5rem; margin: 1.5rem 0 1rem; }
25
+ label { display: block; font-weight: bold; margin-bottom: 0.5rem; }
26
+ .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
27
+ .color-input { display: flex; gap: 0.5rem; }
28
+ .color-picker { width: 60px; height: 40px; border: 2px solid #000; cursor: pointer; }
29
+ .preview-section { background: #f9f9f9; border: 2px dashed #ccc; padding: 1rem; margin-top: 1rem; border-radius: 4px; }
30
+ </style>
31
+ </head>
32
+ <body>
33
+ <div id="root"></div>
34
+ <script type="text/babel">
35
+ const { useState, useEffect } = React;
36
+
37
+ function UiConfigurator() {
38
+ const [authenticated, setAuthenticated] = useState(false);
39
+ const [password, setPassword] = useState('');
40
+ const [loading, setLoading] = useState(false);
41
+ const [error, setError] = useState('');
42
+ const [saved, setSaved] = useState(false);
43
+
44
+ const [config, setConfig] = useState({
45
+ font_heading: 'Syne',
46
+ font_body: 'IBM Plex Sans',
47
+ colors: {
48
+ primary: '#f8d447',
49
+ secondary: '#000000',
50
+ background: '#ffffff',
51
+ surface: '#ffffff'
52
+ },
53
+ corner_radius: '0px'
54
+ });
55
+
56
+ const availableFonts = ['Syne', 'IBM Plex Sans', 'Inter', 'Space Grotesk', 'Oswald', 'Fira Code', 'Roboto', 'Open Sans'];
57
+
58
+ useEffect(() => {
59
+ checkAuth();
60
+ }, []);
61
+
62
+ const checkAuth = async () => {
63
+ try {
64
+ const response = await fetch('/docs/ui', { credentials: 'include' });
65
+ if (response.ok) {
66
+ const data = await response.json();
67
+ setAuthenticated(true);
68
+ if (data.ui_config) {
69
+ setConfig({
70
+ font_heading: data.ui_config.font_heading || 'Syne',
71
+ font_body: data.ui_config.font_body || 'IBM Plex Sans',
72
+ colors: {
73
+ primary: data.ui_config.colors?.primary || '#f8d447',
74
+ secondary: data.ui_config.colors?.secondary || '#000000',
75
+ background: data.ui_config.colors?.background || '#ffffff',
76
+ surface: data.ui_config.colors?.surface || '#ffffff'
77
+ },
78
+ corner_radius: data.ui_config.corner_radius || '0px'
79
+ });
80
+ }
81
+ }
82
+ } catch (err) {
83
+ setAuthenticated(false);
84
+ }
85
+ };
86
+
87
+ const handleLogin = async (e) => {
88
+ e.preventDefault();
89
+ setLoading(true);
90
+ setError('');
91
+
92
+ try {
93
+ const response = await fetch('/docs/ui/login', {
94
+ method: 'POST',
95
+ headers: { 'Content-Type': 'application/json' },
96
+ body: JSON.stringify({ password }),
97
+ credentials: 'include'
98
+ });
99
+
100
+ const data = await response.json();
101
+ if (data.success) {
102
+ setAuthenticated(true);
103
+ await checkAuth();
104
+ } else {
105
+ setError(data.error || 'Invalid password');
106
+ }
107
+ } catch (err) {
108
+ setError('Login failed. Please try again.');
109
+ } finally {
110
+ setLoading(false);
111
+ }
112
+ };
113
+
114
+ const handleSave = async (e) => {
115
+ e.preventDefault();
116
+ setLoading(true);
117
+ setError('');
118
+ setSaved(false);
119
+
120
+ try {
121
+ const response = await fetch('/docs/ui/config', {
122
+ method: 'POST',
123
+ headers: { 'Content-Type': 'application/json' },
124
+ body: JSON.stringify({
125
+ font_heading: config.font_heading,
126
+ font_body: config.font_body,
127
+ color_primary: config.colors.primary,
128
+ color_secondary: config.colors.secondary,
129
+ color_background: config.colors.background,
130
+ color_surface: config.colors.surface,
131
+ corner_radius: config.corner_radius
132
+ }),
133
+ credentials: 'include'
134
+ });
135
+
136
+ const data = await response.json();
137
+ if (data.success) {
138
+ setSaved(true);
139
+ setTimeout(() => setSaved(false), 3000);
140
+ } else {
141
+ setError(data.error || 'Failed to save configuration');
142
+ }
143
+ } catch (err) {
144
+ setError('Save failed. Please try again.');
145
+ } finally {
146
+ setLoading(false);
147
+ }
148
+ };
149
+
150
+ const handleLogout = async () => {
151
+ try {
152
+ await fetch('/docs/ui/logout', { method: 'POST', credentials: 'include' });
153
+ setAuthenticated(false);
154
+ setPassword('');
155
+ } catch (err) {
156
+ console.error('Logout failed:', err);
157
+ }
158
+ };
159
+
160
+ if (!authenticated) {
161
+ return (
162
+ <div className="container">
163
+ <div className="surface">
164
+ <h1>Admin Login</h1>
165
+ <form onSubmit={handleLogin}>
166
+ <label>Admin Password</label>
167
+ <input
168
+ type="password"
169
+ className="input"
170
+ value={password}
171
+ onChange={(e) => setPassword(e.target.value)}
172
+ placeholder="Enter admin password"
173
+ required
174
+ />
175
+ {error && <div className="error">{error}</div>}
176
+ <button type="submit" className="btn" disabled={loading}>
177
+ {loading ? 'Logging in...' : 'Login'}
178
+ </button>
179
+ </form>
180
+ <p style={{ marginTop: '1rem', fontSize: '0.875rem', color: '#666' }}>
181
+ Default password: <code style={{ background: '#ffeb3b', padding: '2px 4px' }}>admin</code>
182
+ <br />
183
+ Or set via <code style={{ background: '#ffeb3b', padding: '2px 4px' }}>ELDERDOCS_ADMIN_PASSWORD</code> env var
184
+ </p>
185
+ </div>
186
+ </div>
187
+ );
188
+ }
189
+
190
+ return (
191
+ <div className="container">
192
+ <div className="surface">
193
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1.5rem' }}>
194
+ <h1>UI Configuration</h1>
195
+ <button onClick={handleLogout} className="btn btn-secondary">Logout</button>
196
+ </div>
197
+
198
+ {saved && <div className="success">✅ Configuration saved successfully! Refresh the main docs page to see changes.</div>}
199
+ {error && <div className="error">{error}</div>}
200
+
201
+ <form onSubmit={handleSave}>
202
+ <h2>Typography</h2>
203
+ <div className="grid">
204
+ <div>
205
+ <label>Heading Font</label>
206
+ <select
207
+ className="input"
208
+ value={config.font_heading}
209
+ onChange={(e) => setConfig({ ...config, font_heading: e.target.value })}
210
+ >
211
+ {availableFonts.map(font => <option key={font} value={font}>{font}</option>)}
212
+ </select>
213
+ </div>
214
+ <div>
215
+ <label>Body Font</label>
216
+ <select
217
+ className="input"
218
+ value={config.font_body}
219
+ onChange={(e) => setConfig({ ...config, font_body: e.target.value })}
220
+ >
221
+ {availableFonts.map(font => <option key={font} value={font}>{font}</option>)}
222
+ </select>
223
+ </div>
224
+ </div>
225
+
226
+ <h2>Colors</h2>
227
+ <div className="grid">
228
+ <div>
229
+ <label>Primary Color</label>
230
+ <div className="color-input">
231
+ <input
232
+ type="color"
233
+ className="color-picker"
234
+ value={config.colors.primary}
235
+ onChange={(e) => setConfig({ ...config, colors: { ...config.colors, primary: e.target.value } })}
236
+ />
237
+ <input
238
+ type="text"
239
+ className="input"
240
+ value={config.colors.primary}
241
+ onChange={(e) => setConfig({ ...config, colors: { ...config.colors, primary: e.target.value } })}
242
+ />
243
+ </div>
244
+ </div>
245
+ <div>
246
+ <label>Secondary Color</label>
247
+ <div className="color-input">
248
+ <input
249
+ type="color"
250
+ className="color-picker"
251
+ value={config.colors.secondary}
252
+ onChange={(e) => setConfig({ ...config, colors: { ...config.colors, secondary: e.target.value } })}
253
+ />
254
+ <input
255
+ type="text"
256
+ className="input"
257
+ value={config.colors.secondary}
258
+ onChange={(e) => setConfig({ ...config, colors: { ...config.colors, secondary: e.target.value } })}
259
+ />
260
+ </div>
261
+ </div>
262
+ <div>
263
+ <label>Background Color</label>
264
+ <div className="color-input">
265
+ <input
266
+ type="color"
267
+ className="color-picker"
268
+ value={config.colors.background}
269
+ onChange={(e) => setConfig({ ...config, colors: { ...config.colors, background: e.target.value } })}
270
+ />
271
+ <input
272
+ type="text"
273
+ className="input"
274
+ value={config.colors.background}
275
+ onChange={(e) => setConfig({ ...config, colors: { ...config.colors, background: e.target.value } })}
276
+ />
277
+ </div>
278
+ </div>
279
+ <div>
280
+ <label>Surface Color</label>
281
+ <div className="color-input">
282
+ <input
283
+ type="color"
284
+ className="color-picker"
285
+ value={config.colors.surface}
286
+ onChange={(e) => setConfig({ ...config, colors: { ...config.colors, surface: e.target.value } })}
287
+ />
288
+ <input
289
+ type="text"
290
+ className="input"
291
+ value={config.colors.surface}
292
+ onChange={(e) => setConfig({ ...config, colors: { ...config.colors, surface: e.target.value } })}
293
+ />
294
+ </div>
295
+ </div>
296
+ </div>
297
+
298
+ <h2>Styling</h2>
299
+ <div>
300
+ <label>Corner Radius: {config.corner_radius}</label>
301
+ <input
302
+ type="range"
303
+ className="input"
304
+ min="0"
305
+ max="24"
306
+ value={parseInt(config.corner_radius) || 0}
307
+ onChange={(e) => setConfig({ ...config, corner_radius: `${e.target.value}px` })}
308
+ />
309
+ <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '0.875rem', color: '#666' }}>
310
+ <span>Sharp (0px)</span>
311
+ <span>Rounded (24px)</span>
312
+ </div>
313
+ </div>
314
+
315
+ <h2>Live Preview</h2>
316
+ <PreviewPanel config={config} />
317
+
318
+ <div style={{ marginTop: '2rem', display: 'flex', gap: '1rem' }}>
319
+ <button type="submit" className="btn" disabled={loading}>
320
+ {loading ? 'Saving...' : 'Save Configuration'}
321
+ </button>
322
+ </div>
323
+ </form>
324
+ </div>
325
+ </div>
326
+ );
327
+ }
328
+
329
+ function PreviewPanel({ config }) {
330
+ const previewStyle = {
331
+ backgroundColor: config.colors.background,
332
+ border: `3px solid ${config.colors.secondary}`,
333
+ borderRadius: config.corner_radius,
334
+ padding: '1.5rem',
335
+ marginTop: '1rem'
336
+ };
337
+
338
+ const headingStyle = {
339
+ fontFamily: `'${config.font_heading}', sans-serif`,
340
+ color: config.colors.secondary,
341
+ fontSize: '1.5rem',
342
+ fontWeight: 'bold',
343
+ textTransform: 'uppercase',
344
+ marginBottom: '1rem'
345
+ };
346
+
347
+ const bodyStyle = {
348
+ fontFamily: `'${config.font_body}', sans-serif`,
349
+ color: config.colors.secondary,
350
+ fontSize: '0.9rem',
351
+ lineHeight: '1.6'
352
+ };
353
+
354
+ const buttonStyle = {
355
+ fontFamily: `'${config.font_heading}', sans-serif`,
356
+ backgroundColor: config.colors.secondary,
357
+ color: config.colors.background,
358
+ border: `3px solid ${config.colors.secondary}`,
359
+ borderRadius: config.corner_radius,
360
+ padding: '0.6rem 1.2rem',
361
+ fontWeight: 'bold',
362
+ textTransform: 'uppercase',
363
+ cursor: 'pointer',
364
+ fontSize: '0.75rem',
365
+ marginRight: '0.5rem',
366
+ marginBottom: '0.5rem',
367
+ display: 'inline-block'
368
+ };
369
+
370
+ const buttonPrimaryStyle = {
371
+ ...buttonStyle,
372
+ backgroundColor: config.colors.primary,
373
+ color: config.colors.secondary,
374
+ borderColor: config.colors.secondary,
375
+ boxShadow: `4px 4px 0px 0px ${config.colors.primary}`
376
+ };
377
+
378
+ const chipStyle = {
379
+ fontFamily: `'${config.font_heading}', sans-serif`,
380
+ backgroundColor: config.colors.primary,
381
+ color: config.colors.secondary,
382
+ border: `2px solid ${config.colors.secondary}`,
383
+ borderRadius: config.corner_radius,
384
+ padding: '0.3rem 0.8rem',
385
+ fontSize: '0.7rem',
386
+ fontWeight: 'bold',
387
+ textTransform: 'uppercase',
388
+ display: 'inline-block',
389
+ marginRight: '0.5rem',
390
+ marginBottom: '0.5rem'
391
+ };
392
+
393
+ const cardStyle = {
394
+ backgroundColor: config.colors.surface,
395
+ border: `3px solid ${config.colors.secondary}`,
396
+ borderRadius: config.corner_radius,
397
+ padding: '1rem',
398
+ marginBottom: '1rem',
399
+ boxShadow: `4px 4px 0px 0px ${config.colors.secondary}`
400
+ };
401
+
402
+ const inputStyle = {
403
+ width: '100%',
404
+ padding: '0.5rem',
405
+ border: `2px solid ${config.colors.secondary}`,
406
+ borderRadius: config.corner_radius,
407
+ backgroundColor: config.colors.surface,
408
+ color: config.colors.secondary,
409
+ fontFamily: `'${config.font_body}', sans-serif`,
410
+ fontSize: '0.875rem',
411
+ marginBottom: '0.5rem'
412
+ };
413
+
414
+ // Load fonts dynamically
415
+ React.useEffect(() => {
416
+ const loadFont = (fontName) => {
417
+ const link = document.createElement('link');
418
+ link.href = `https://fonts.googleapis.com/css2?family=${fontName.replace(/\s/g, '+')}:wght@400;500;600;700&display=swap`;
419
+ link.rel = 'stylesheet';
420
+ if (!document.querySelector(`link[href*="${fontName}"]`)) {
421
+ document.head.appendChild(link);
422
+ }
423
+ };
424
+ loadFont(config.font_heading);
425
+ loadFont(config.font_body);
426
+ }, [config.font_heading, config.font_body]);
427
+
428
+ return (
429
+ <div style={previewStyle}>
430
+ <div style={headingStyle}>API Documentation Preview</div>
431
+
432
+ <div style={bodyStyle}>
433
+ This is how your documentation will look with the current settings.
434
+ </div>
435
+
436
+ <div style={{ marginTop: '1rem', marginBottom: '1rem' }}>
437
+ <span style={chipStyle}>GET</span>
438
+ <span style={chipStyle}>POST</span>
439
+ <span style={chipStyle}>API</span>
440
+ </div>
441
+
442
+ <div style={cardStyle}>
443
+ <div style={{ ...headingStyle, fontSize: '1.2rem', marginBottom: '0.5rem' }}>
444
+ /api/users/{'{id}'}
445
+ </div>
446
+ <div style={bodyStyle}>
447
+ Retrieve user information by ID. Returns detailed user profile including name, email, and preferences.
448
+ </div>
449
+ <div style={{ marginTop: '0.75rem' }}>
450
+ <button style={buttonPrimaryStyle}>Send Request</button>
451
+ <button style={buttonStyle}>View Details</button>
452
+ </div>
453
+ </div>
454
+
455
+ <div style={{ marginTop: '1rem' }}>
456
+ <input
457
+ type="text"
458
+ placeholder="Enter API key..."
459
+ style={inputStyle}
460
+ readOnly
461
+ />
462
+ <div style={{ ...bodyStyle, fontSize: '0.8rem', color: config.colors.secondary + '80' }}>
463
+ Sample input field with your styling
464
+ </div>
465
+ </div>
466
+
467
+ <div style={{ marginTop: '1rem', padding: '0.75rem', backgroundColor: config.colors.primary + '20', border: `2px solid ${config.colors.primary}`, borderRadius: config.corner_radius }}>
468
+ <div style={{ ...bodyStyle, fontSize: '0.85rem' }}>
469
+ <strong>💡 Tip:</strong> This preview updates in real-time as you change settings.
470
+ Your actual documentation will look like this after saving.
471
+ </div>
472
+ </div>
473
+ </div>
474
+ );
475
+ }
476
+
477
+ ReactDOM.render(<UiConfigurator />, document.getElementById('root'));
478
+ </script>
479
+ </body>
480
+ </html>
481
+
@@ -0,0 +1 @@
1
+ @import"https://fonts.googleapis.com/css2?family=Syne:wght@500;600;700&family=IBM+Plex+Sans:wght@400;500;600&family=Inter:wght@400;600;800&family=Space+Grotesk:wght@400;600;700&family=Oswald:wght@400;600;700&family=Fira+Code:wght@400;600&family=Roboto:wght@400;500;700&family=Open+Sans:wght@400;600;700&display=swap";*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:Inter,system-ui,sans-serif;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}:root{--bd-yellow: #f8d447;--bd-charcoal: #000000;--bd-ink: #121212;--bd-white: #ffffff;--bd-muted: #666666;--bd-panel: #ffffff;--bd-border: #000000;--bd-radius: 0px;--font-heading: "Syne";--font-body: "IBM Plex Sans"}*{border-width:0}body{font-family:var(--font-body),system-ui,-apple-system,BlinkMacSystemFont;font-size:15px;line-height:1.55;color:var(--bd-ink);background:var(--bd-white);min-height:100vh}body:before{display:none}body:after{display:none}body[data-mounted=true] .reveal{opacity:1;transform:translateY(0) scale(1)}.app-shell{position:relative;z-index:1;width:100%;height:100%;display:flex;background:#ffffff}.surface{background:var(--bd-panel);border:3px solid var(--bd-border);box-shadow:8px 8px 0 0 var(--bd-border);border-radius:var(--bd-radius)}.surface--highlight{border-color:var(--bd-border);box-shadow:4px 4px 0 0 var(--bd-yellow)}.pill{font-family:var(--font-heading),system-ui;letter-spacing:.1em;font-size:.7rem;text-transform:uppercase;border:2px solid var(--bd-border);padding:.35rem 1rem;border-radius:var(--bd-radius);font-weight:700;color:var(--bd-charcoal);background:var(--bd-yellow)}.chip{display:inline-flex;align-items:center;gap:.25rem;font-size:.7rem;text-transform:uppercase;letter-spacing:.1em;padding:.3rem .9rem;border-radius:var(--bd-radius);border:2px solid var(--bd-border);font-weight:700;background:var(--bd-panel);color:var(--bd-ink)}.btn-primary{font-family:var(--font-heading),system-ui;text-transform:uppercase;letter-spacing:.1em;font-size:.8rem;background:var(--bd-charcoal);color:var(--bd-white);padding:.85rem 1.6rem;border:3px solid var(--bd-border);border-radius:var(--bd-radius);transition:transform .1s ease,box-shadow .1s ease;font-weight:700;box-shadow:4px 4px 0 0 var(--bd-yellow)}.btn-primary:hover{transform:translate(-2px,-2px);box-shadow:6px 6px 0 0 var(--bd-yellow)}.btn-primary:active{transform:translate(2px,2px);box-shadow:0 0 0 0 var(--bd-yellow)}.btn-secondary{font-family:var(--font-heading),system-ui;text-transform:uppercase;letter-spacing:.1em;font-size:.7rem;background:var(--bd-panel);color:var(--bd-ink);padding:.75rem 1.3rem;border:2px solid var(--bd-border);border-radius:var(--bd-radius);transition:transform .1s ease;font-weight:700}.btn-secondary:hover{background:var(--bd-yellow);color:var(--bd-charcoal)}.input-field{background:var(--bd-panel);border:3px solid var(--bd-border);color:var(--bd-ink);border-radius:var(--bd-radius);padding:.65rem .9rem;transition:box-shadow .1s ease;font-weight:500}.input-field:focus{outline:none;box-shadow:4px 4px 0 0 var(--bd-yellow)}.nav-card{border-radius:var(--bd-radius);border:2px solid transparent;background:transparent;transition:transform .1s ease}.nav-card:hover{background:var(--bd-panel);border:2px solid var(--bd-border);transform:translate(-2px,-2px);box-shadow:4px 4px 0 0 var(--bd-charcoal)}.nav-card--active{background:var(--bd-yellow);border:2px solid var(--bd-border);color:var(--bd-charcoal);box-shadow:4px 4px 0 0 var(--bd-charcoal);transform:translate(-2px,-2px)}.reveal{opacity:0;transform:translateY(15px);transition:opacity .4s cubic-bezier(.16,1,.3,1),transform .4s cubic-bezier(.16,1,.3,1)}.relative{position:relative}.col-span-2{grid-column:span 2 / span 2}.col-span-3{grid-column:span 3 / span 3}.col-span-7{grid-column:span 7 / span 7}.my-5{margin-top:1.25rem;margin-bottom:1.25rem}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-5{margin-bottom:1.25rem}.mb-6{margin-bottom:1.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.block{display:block}.\!inline{display:inline!important}.inline{display:inline}.flex{display:flex}.grid{display:grid}.hidden{display:none}.h-32{height:8rem}.h-full{height:100%}.h-screen{height:100vh}.w-80{width:20rem}.w-\[360px\]{width:360px}.w-full{width:100%}.max-w-none{max-width:none}.flex-1{flex:1 1 0%}.grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.space-y-8>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(2rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(2rem * var(--tw-space-y-reverse))}.divide-y-2>:not([hidden])~:not([hidden]){--tw-divide-y-reverse: 0;border-top-width:calc(2px * calc(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(2px * var(--tw-divide-y-reverse))}.divide-black>:not([hidden])~:not([hidden]){--tw-divide-opacity: 1;border-color:rgb(0 0 0 / var(--tw-divide-opacity, 1))}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.rounded-none{border-radius:0}.border{border-width:1px}.border-2{border-width:2px}.border-b-0{border-bottom-width:0px}.border-l-0{border-left-width:0px}.border-l-4{border-left-width:4px}.border-r-0{border-right-width:0px}.\!border-black{--tw-border-opacity: 1 !important;border-color:rgb(0 0 0 / var(--tw-border-opacity, 1))!important}.border-amber-900{--tw-border-opacity: 1;border-color:rgb(120 53 15 / var(--tw-border-opacity, 1))}.border-black{--tw-border-opacity: 1;border-color:rgb(0 0 0 / var(--tw-border-opacity, 1))}.border-black\/10{border-color:#0000001a}.border-blue-900{--tw-border-opacity: 1;border-color:rgb(30 58 138 / var(--tw-border-opacity, 1))}.border-emerald-900{--tw-border-opacity: 1;border-color:rgb(6 78 59 / var(--tw-border-opacity, 1))}.border-gray-900{--tw-border-opacity: 1;border-color:rgb(17 24 39 / var(--tw-border-opacity, 1))}.border-red-900{--tw-border-opacity: 1;border-color:rgb(127 29 29 / var(--tw-border-opacity, 1))}.border-yellow-400{--tw-border-opacity: 1;border-color:rgb(251 191 36 / var(--tw-border-opacity, 1))}.bg-amber-100{--tw-bg-opacity: 1;background-color:rgb(254 243 199 / var(--tw-bg-opacity, 1))}.bg-blue-100{--tw-bg-opacity: 1;background-color:rgb(219 234 254 / var(--tw-bg-opacity, 1))}.bg-emerald-100{--tw-bg-opacity: 1;background-color:rgb(209 250 229 / var(--tw-bg-opacity, 1))}.bg-gray-100{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity, 1))}.bg-green-100{--tw-bg-opacity: 1;background-color:rgb(220 252 231 / var(--tw-bg-opacity, 1))}.bg-red-100{--tw-bg-opacity: 1;background-color:rgb(254 226 226 / var(--tw-bg-opacity, 1))}.bg-red-50{--tw-bg-opacity: 1;background-color:rgb(254 242 242 / var(--tw-bg-opacity, 1))}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.bg-yellow-100{--tw-bg-opacity: 1;background-color:rgb(254 243 199 / var(--tw-bg-opacity, 1))}.bg-yellow-400{--tw-bg-opacity: 1;background-color:rgb(251 191 36 / var(--tw-bg-opacity, 1))}.p-10{padding:2.5rem}.p-12{padding:3rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.pl-4{padding-left:1rem}.text-left{text-align:left}.text-center{text-align:center}.font-\[\'Syne\'\]{font-family:Syne}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:32px}.text-3xl{font-size:40px}.text-4xl{font-size:48px}.text-\[0\.65rem\]{font-size:.65rem}.text-\[0\.6rem\]{font-size:.6rem}.text-\[0\.82rem\]{font-size:.82rem}.text-\[0\.8rem\]{font-size:.8rem}.text-base{font-size:20px}.text-lg{font-size:24px}.text-sm{font-size:18px}.text-xl{font-size:28px}.text-xs{font-size:16px}.font-black{font-weight:900}.font-bold{font-weight:700}.font-medium{font-weight:500}.uppercase{text-transform:uppercase}.leading-relaxed{line-height:1.625}.tracking-\[0\.05em\]{letter-spacing:.05em}.tracking-\[0\.25em\]{letter-spacing:.25em}.tracking-\[0\.35em\]{letter-spacing:.35em}.tracking-\[0\.3em\]{letter-spacing:.3em}.tracking-tight{letter-spacing:-.025em}.\!text-black{--tw-text-opacity: 1 !important;color:rgb(0 0 0 / var(--tw-text-opacity, 1))!important}.text-amber-900{--tw-text-opacity: 1;color:rgb(120 53 15 / var(--tw-text-opacity, 1))}.text-black{--tw-text-opacity: 1;color:rgb(0 0 0 / var(--tw-text-opacity, 1))}.text-black\/40{color:#0006}.text-black\/50{color:#00000080}.text-black\/60{color:#0009}.text-black\/70{color:#000000b3}.text-black\/80{color:#000c}.text-black\/90{color:#000000e6}.text-blue-900{--tw-text-opacity: 1;color:rgb(30 58 138 / var(--tw-text-opacity, 1))}.text-emerald-900{--tw-text-opacity: 1;color:rgb(6 78 59 / var(--tw-text-opacity, 1))}.text-gray-900{--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity, 1))}.text-red-500{--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity, 1))}.text-red-600{--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity, 1))}.text-red-900{--tw-text-opacity: 1;color:rgb(127 29 29 / var(--tw-text-opacity, 1))}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}@keyframes floatGlow{0%{transform:translateY(0);opacity:.5}50%{transform:translateY(-12px);opacity:1}to{transform:translateY(0);opacity:.5}}@keyframes shimmer{0%{background-position:0% 50%}to{background-position:200% 50%}}.last\:mb-0:last-child{margin-bottom:0}@media (min-width: 768px){.md\:hidden{display:none}}