reviewzy 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.
Files changed (46) hide show
  1. package/.env.example +30 -0
  2. package/LICENSE +661 -0
  3. package/README.md +170 -0
  4. package/bin/reviewzy +7 -0
  5. package/package.json +29 -0
  6. package/src/config.ts +76 -0
  7. package/src/daemon/app.ts +159 -0
  8. package/src/daemon/main.ts +103 -0
  9. package/src/daemon/origin.ts +60 -0
  10. package/src/dashboard/auth.ts +122 -0
  11. package/src/dashboard/mount.ts +373 -0
  12. package/src/dashboard/static/components.css +1116 -0
  13. package/src/dashboard/static/dashboard.css +182 -0
  14. package/src/dashboard/static/dashboard.js +169 -0
  15. package/src/dashboard/static/fonts/OFL-JetBrainsMono.txt +93 -0
  16. package/src/dashboard/static/fonts/OFL-Onest.txt +93 -0
  17. package/src/dashboard/static/fonts/jetbrainsmono-latin-ext.woff2 +0 -0
  18. package/src/dashboard/static/fonts/jetbrainsmono-latin.woff2 +0 -0
  19. package/src/dashboard/static/fonts/onest-cyrillic.woff2 +0 -0
  20. package/src/dashboard/static/fonts/onest-latin-ext.woff2 +0 -0
  21. package/src/dashboard/static/fonts/onest-latin.woff2 +0 -0
  22. package/src/dashboard/static/htmx.min.js +2 -0
  23. package/src/dashboard/static/tokens.css +213 -0
  24. package/src/dashboard/views.tsx +1388 -0
  25. package/src/db/human-save.ts +242 -0
  26. package/src/db/migrate.ts +71 -0
  27. package/src/db/migrations.ts +149 -0
  28. package/src/db/queries.ts +425 -0
  29. package/src/db/store.ts +83 -0
  30. package/src/db/style-guide.ts +236 -0
  31. package/src/db/sweep.ts +88 -0
  32. package/src/mcp/await-approved.ts +156 -0
  33. package/src/mcp/fetch-approved.ts +132 -0
  34. package/src/mcp/file-entries.ts +219 -0
  35. package/src/mcp/list-entries.ts +107 -0
  36. package/src/mcp/mark-applied.ts +103 -0
  37. package/src/mcp/route.ts +88 -0
  38. package/src/mcp/server.ts +59 -0
  39. package/src/mcp/style-guide-resource.ts +40 -0
  40. package/src/notify.ts +142 -0
  41. package/src/shim/daemon.ts +318 -0
  42. package/src/shim/lockfile.ts +153 -0
  43. package/src/shim/main.ts +172 -0
  44. package/src/shim/semver.ts +29 -0
  45. package/src/version.ts +4 -0
  46. package/tsconfig.json +26 -0
@@ -0,0 +1,1116 @@
1
+ /* ============================================================
2
+ reviewzy — component styles
3
+ ============================================================ */
4
+
5
+ /* ---- Button ----
6
+ Buttons inherit a fixed height so horizontal toolbars stay aligned regardless
7
+ of label length, icon presence, or element type (<button>, <a>, <input>). */
8
+ .btn, input.btn, button.btn, a.btn {
9
+ display: inline-flex;
10
+ align-items: center;
11
+ justify-content: center;
12
+ gap: var(--space-2);
13
+ height: 32px;
14
+ min-height: 32px;
15
+ padding: 0 12px;
16
+ font: 450 var(--text-base)/1 var(--font-sans);
17
+ vertical-align: middle;
18
+ border: 1px solid transparent;
19
+ border-radius: var(--radius);
20
+ cursor: pointer;
21
+ transition:
22
+ background var(--dur-fast) var(--ease),
23
+ border-color var(--dur-fast) var(--ease),
24
+ color var(--dur-fast) var(--ease),
25
+ transform var(--dur-fast) var(--ease);
26
+ white-space: nowrap;
27
+ text-decoration: none;
28
+ user-select: none;
29
+ }
30
+ input.btn { line-height: 30px; } /* native inputs need explicit line-height */
31
+ .btn:active { transform: translateY(1px); }
32
+ .btn:disabled { opacity: 0.45; cursor: not-allowed; pointer-events: none; }
33
+ .btn svg { width: 14px; height: 14px; flex-shrink: 0; }
34
+
35
+ .btn-sm, input.btn-sm, button.btn-sm, a.btn-sm {
36
+ height: 30px;
37
+ min-height: 30px;
38
+ padding: 0 10px;
39
+ font-size: var(--text-sm);
40
+ }
41
+ input.btn-sm { line-height: 28px; }
42
+
43
+ .btn-lg, input.btn-lg, button.btn-lg, a.btn-lg {
44
+ height: 36px;
45
+ min-height: 36px;
46
+ padding: 0 18px;
47
+ font-size: var(--text-md);
48
+ }
49
+ input.btn-lg { line-height: 34px; }
50
+
51
+ .btn-primary {
52
+ background: var(--accent);
53
+ color: var(--text-on-accent);
54
+ border-color: var(--accent);
55
+ }
56
+ .btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
57
+ .btn-primary:active { background: var(--accent-press); border-color: var(--accent-press); }
58
+
59
+ .btn-secondary {
60
+ background: var(--bg-raised);
61
+ color: var(--text);
62
+ border-color: var(--line);
63
+ }
64
+ .btn-secondary:hover { border-color: var(--line-strong); background: var(--bg-hover); }
65
+
66
+ .btn-ghost {
67
+ background: transparent;
68
+ color: var(--text-muted);
69
+ border-color: transparent;
70
+ }
71
+ .btn-ghost:hover { background: var(--bg-hover); color: var(--text); }
72
+
73
+ .btn-danger {
74
+ background: color-mix(in srgb, var(--danger) 12%, transparent);
75
+ color: var(--danger);
76
+ border-color: color-mix(in srgb, var(--danger) 28%, transparent);
77
+ }
78
+ .btn-danger:hover {
79
+ background: color-mix(in srgb, var(--danger) 20%, transparent);
80
+ border-color: color-mix(in srgb, var(--danger) 44%, transparent);
81
+ }
82
+
83
+ .btn-icon {
84
+ width: 32px;
85
+ min-width: 32px;
86
+ padding: 0;
87
+ background: transparent;
88
+ color: var(--text-dim);
89
+ border-color: var(--line);
90
+ }
91
+ .btn-icon:hover { border-color: var(--line-strong); color: var(--text); background: var(--bg-hover); }
92
+ .btn-icon svg { width: 15px; height: 15px; }
93
+
94
+ /* Loading spinner */
95
+ @keyframes spin { to { transform: rotate(360deg); } }
96
+ .btn-loading { position: relative; color: transparent !important; }
97
+ .btn-loading::after {
98
+ content: '';
99
+ position: absolute; inset: 0; margin: auto;
100
+ width: 12px; height: 12px;
101
+ border: 2px solid currentColor;
102
+ border-top-color: transparent;
103
+ border-radius: 50%;
104
+ animation: spin 0.6s linear infinite;
105
+ color: var(--text-on-accent);
106
+ }
107
+ .btn-secondary.btn-loading::after { color: var(--text-dim); }
108
+
109
+ /* ---- Tabs (underline) ---- */
110
+ .tabs-wrap { border-bottom: 1px solid var(--line); position: relative; }
111
+ .tabs { display: flex; gap: 0; }
112
+ .tab {
113
+ position: relative;
114
+ padding: 9px 14px;
115
+ font: 450 var(--text-base)/1 var(--font-sans);
116
+ color: var(--text-faint);
117
+ background: transparent;
118
+ border: none;
119
+ cursor: pointer;
120
+ transition: color var(--dur-fast) var(--ease);
121
+ user-select: none;
122
+ display: inline-flex;
123
+ align-items: center;
124
+ gap: var(--space-2);
125
+ }
126
+ .tab:hover { color: var(--text-muted); }
127
+ .tab.active { color: var(--accent); }
128
+
129
+ .tab-ink {
130
+ position: absolute;
131
+ bottom: -1px;
132
+ height: 2px;
133
+ background: var(--accent);
134
+ border-radius: 1px;
135
+ pointer-events: none;
136
+ transition:
137
+ left 440ms cubic-bezier(0, 0.85, 0.35, 1),
138
+ width 440ms cubic-bezier(0, 0.85, 0.35, 1),
139
+ opacity 180ms var(--ease);
140
+ will-change: left, width;
141
+ }
142
+
143
+ .tab-badge {
144
+ font: 450 10px/1 var(--font-sans);
145
+ padding: 2px 5px;
146
+ border-radius: var(--radius-pill);
147
+ background: var(--bg-hover);
148
+ color: var(--text-dim);
149
+ }
150
+ .tab.active .tab-badge { background: var(--accent-soft); color: var(--accent); }
151
+
152
+ .tab-panel { display: none; padding-top: var(--space-5); }
153
+ .tab-panel.active { display: block; }
154
+
155
+ /* ---- Tabs (boxed segmented control) ---- */
156
+ .tabs-boxed-wrap {
157
+ background: var(--bg-sunken);
158
+ border: 1px solid var(--line);
159
+ border-radius: calc(var(--radius) + 2px);
160
+ padding: 3px;
161
+ display: inline-flex;
162
+ gap: 2px;
163
+ }
164
+ .tab-box {
165
+ padding: 6px 12px;
166
+ font: 450 var(--text-sm)/1 var(--font-sans);
167
+ color: var(--text-dim);
168
+ background: transparent;
169
+ border: none;
170
+ border-radius: var(--radius);
171
+ cursor: pointer;
172
+ transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
173
+ user-select: none;
174
+ }
175
+ .tab-box:hover { background: var(--bg-hover); color: var(--text-muted); }
176
+ .tab-box.active {
177
+ background: var(--bg-raised);
178
+ color: var(--text);
179
+ box-shadow: 0 1px 2px rgb(0 0 0 / 0.12);
180
+ }
181
+
182
+ /* ---- Inputs ---- */
183
+ .field { display: flex; flex-direction: column; gap: var(--space-2); }
184
+ .field-label {
185
+ font: 450 11px/1 var(--font-sans);
186
+ letter-spacing: 0.07em;
187
+ text-transform: uppercase;
188
+ color: var(--text-dim);
189
+ }
190
+ .field-hint { font-size: var(--text-sm); color: var(--text-faint); }
191
+ .field-error {
192
+ font-size: var(--text-sm);
193
+ color: var(--danger);
194
+ display: flex;
195
+ align-items: center;
196
+ gap: var(--space-1);
197
+ }
198
+
199
+ .input {
200
+ background: var(--bg-sunken);
201
+ border: 1px solid var(--line);
202
+ border-radius: var(--radius);
203
+ padding: 8px 10px;
204
+ font: 400 var(--text-base)/1.4 var(--font-sans);
205
+ color: var(--text);
206
+ width: 100%;
207
+ transition:
208
+ border-color var(--dur-fast) var(--ease),
209
+ box-shadow var(--dur-fast) var(--ease);
210
+ }
211
+ .input::placeholder { color: var(--text-faint); }
212
+ .input:hover:not(:focus):not(.input-invalid) { border-color: var(--line-strong); }
213
+ .input:focus {
214
+ outline: none;
215
+ border-color: var(--accent);
216
+ box-shadow: 0 0 0 3px var(--accent-ring);
217
+ }
218
+ .input-invalid { border-color: var(--danger); }
219
+ .input-invalid:focus {
220
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--danger) 30%, transparent);
221
+ }
222
+
223
+ textarea.input { resize: vertical; min-height: 80px; line-height: 1.55; }
224
+
225
+ select.input {
226
+ appearance: none;
227
+ padding-right: 32px;
228
+ cursor: pointer;
229
+ background-image: url("data:image/svg+xml,%3Csvg width='10' height='6' viewBox='0 0 10 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1L5 5L9 1' stroke='%237F849C' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
230
+ background-repeat: no-repeat;
231
+ background-position: right 10px center;
232
+ }
233
+
234
+ .input-wrap { position: relative; }
235
+ .input-wrap.has-icon .input { padding-left: 32px; }
236
+ .input-icon {
237
+ position: absolute;
238
+ left: 10px;
239
+ top: 50%;
240
+ transform: translateY(-50%);
241
+ color: var(--text-faint);
242
+ pointer-events: none;
243
+ display: flex;
244
+ align-items: center;
245
+ }
246
+ .input-icon svg { width: 14px; height: 14px; }
247
+ .input-suffix {
248
+ position: absolute;
249
+ right: 10px;
250
+ top: 50%;
251
+ transform: translateY(-50%);
252
+ font: 400 var(--text-xs)/1 var(--font-mono);
253
+ color: var(--text-faint);
254
+ pointer-events: none;
255
+ }
256
+
257
+ /* Checkbox / radio (using native inputs) */
258
+ .checkbox-label, .radio-label {
259
+ display: flex;
260
+ align-items: center;
261
+ gap: var(--space-2);
262
+ font-size: var(--text-base);
263
+ color: var(--text-muted);
264
+ cursor: pointer;
265
+ user-select: none;
266
+ }
267
+ .checkbox-label:hover, .radio-label:hover { color: var(--text); }
268
+
269
+ .checkbox-label input[type="checkbox"],
270
+ .radio-label input[type="radio"] {
271
+ width: 15px;
272
+ height: 15px;
273
+ appearance: none;
274
+ background: var(--bg-sunken);
275
+ border: 1px solid var(--line);
276
+ border-radius: calc(var(--radius) * 0.5);
277
+ cursor: pointer;
278
+ flex-shrink: 0;
279
+ position: relative;
280
+ transition: border-color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
281
+ }
282
+ .radio-label input[type="radio"] { border-radius: 50%; }
283
+
284
+ .checkbox-label input[type="checkbox"]:checked,
285
+ .radio-label input[type="radio"]:checked {
286
+ background: var(--accent);
287
+ border-color: var(--accent);
288
+ }
289
+ .checkbox-label input[type="checkbox"]:checked::after {
290
+ content: '';
291
+ position: absolute;
292
+ inset: 3px;
293
+ background: var(--text-on-accent);
294
+ clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
295
+ }
296
+ .radio-label input[type="radio"]:checked::after {
297
+ content: '';
298
+ position: absolute;
299
+ inset: 3px;
300
+ background: var(--text-on-accent);
301
+ border-radius: 50%;
302
+ }
303
+
304
+ /* Toggle (CSS-only, .on toggles state) */
305
+ .toggle-wrap {
306
+ display: flex;
307
+ align-items: center;
308
+ gap: var(--space-3);
309
+ cursor: pointer;
310
+ user-select: none;
311
+ }
312
+ .toggle {
313
+ width: 32px;
314
+ height: 18px;
315
+ background: var(--line-strong);
316
+ border-radius: var(--radius-pill);
317
+ position: relative;
318
+ transition: background var(--dur-fast) var(--ease);
319
+ flex-shrink: 0;
320
+ }
321
+ .toggle::after {
322
+ content: '';
323
+ position: absolute;
324
+ top: 2px;
325
+ left: 2px;
326
+ width: 14px;
327
+ height: 14px;
328
+ background: var(--bg);
329
+ border-radius: 50%;
330
+ transition: transform var(--dur-base) var(--ease);
331
+ }
332
+ .toggle.on { background: var(--accent); }
333
+ .toggle.on::after { transform: translateX(14px); }
334
+ .toggle-label { font-size: var(--text-base); color: var(--text-muted); }
335
+
336
+ /* ---- Card ---- */
337
+ .card {
338
+ background: var(--bg-raised);
339
+ border: 1px solid var(--line);
340
+ border-radius: var(--radius);
341
+ padding: var(--space-4);
342
+ }
343
+ .card-interactive {
344
+ cursor: pointer;
345
+ transition:
346
+ border-color var(--dur-fast) var(--ease),
347
+ transform var(--dur-fast) var(--ease);
348
+ }
349
+ .card-interactive:hover {
350
+ border-color: var(--line-strong);
351
+ transform: translateY(-1px);
352
+ }
353
+ .card-header {
354
+ display: flex;
355
+ align-items: center;
356
+ justify-content: space-between;
357
+ gap: var(--space-3);
358
+ padding-bottom: var(--space-3);
359
+ margin-bottom: var(--space-3);
360
+ border-bottom: 1px solid var(--line);
361
+ }
362
+ .card-title {
363
+ font: 500 var(--text-xl)/1.3 var(--font-sans);
364
+ color: var(--text);
365
+ }
366
+ .card-subtitle {
367
+ font-size: var(--text-sm);
368
+ color: var(--text-dim);
369
+ margin-top: 2px;
370
+ }
371
+
372
+ /* ---- Tag / Pill ---- */
373
+ .tag {
374
+ display: inline-flex;
375
+ align-items: center;
376
+ gap: 5px;
377
+ font: 450 11px/1 var(--font-sans);
378
+ letter-spacing: 0.04em;
379
+ padding: 3px 8px;
380
+ border-radius: var(--radius-pill);
381
+ border: 1px solid transparent;
382
+ white-space: nowrap;
383
+ }
384
+ .tag-dot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; }
385
+
386
+ .tag-default {
387
+ background: var(--bg-hover);
388
+ color: var(--text-dim);
389
+ border-color: var(--line);
390
+ }
391
+ .tag-accent {
392
+ background: var(--accent-soft);
393
+ color: var(--accent);
394
+ border-color: color-mix(in srgb, var(--accent) 28%, transparent);
395
+ }
396
+ .tag-success {
397
+ background: color-mix(in srgb, var(--success) 14%, transparent);
398
+ color: var(--success);
399
+ border-color: color-mix(in srgb, var(--success) 28%, transparent);
400
+ }
401
+ .tag-warning {
402
+ background: color-mix(in srgb, var(--warning) 14%, transparent);
403
+ color: var(--warning);
404
+ border-color: color-mix(in srgb, var(--warning) 28%, transparent);
405
+ }
406
+ [data-theme="light"] .tag-warning { color: #DF8E1D; }
407
+ .tag-danger {
408
+ background: color-mix(in srgb, var(--danger) 14%, transparent);
409
+ color: var(--danger);
410
+ border-color: color-mix(in srgb, var(--danger) 28%, transparent);
411
+ }
412
+ .tag-info {
413
+ background: color-mix(in srgb, var(--info) 14%, transparent);
414
+ color: var(--info);
415
+ border-color: color-mix(in srgb, var(--info) 28%, transparent);
416
+ }
417
+ .tag-orange {
418
+ background: var(--accent-2-soft);
419
+ color: var(--accent-2);
420
+ border-color: color-mix(in srgb, var(--accent-2) 28%, transparent);
421
+ }
422
+
423
+ /* ---- Table ---- */
424
+ .table-wrap {
425
+ border: 1px solid var(--line);
426
+ border-radius: var(--radius);
427
+ overflow: hidden;
428
+ }
429
+ .table-wrap table {
430
+ width: 100%;
431
+ border-collapse: collapse;
432
+ font-size: var(--text-sm);
433
+ }
434
+ .table-wrap thead tr {
435
+ border-bottom: 1px solid var(--line-strong);
436
+ background: var(--bg-sunken);
437
+ }
438
+ .table-wrap thead th {
439
+ font: 450 11px/1 var(--font-sans);
440
+ letter-spacing: 0.07em;
441
+ text-transform: uppercase;
442
+ color: var(--text-dim);
443
+ padding: 10px var(--space-4);
444
+ text-align: left;
445
+ white-space: nowrap;
446
+ }
447
+ .table-wrap thead th.num-col { text-align: right; }
448
+ .table-wrap tbody tr {
449
+ border-bottom: 1px solid var(--line-soft);
450
+ transition: background var(--dur-fast) var(--ease);
451
+ }
452
+ .table-wrap tbody tr:last-child { border-bottom: none; }
453
+ .table-wrap tbody tr:hover { background: var(--bg-hover); }
454
+ .table-wrap tbody td {
455
+ padding: 10px var(--space-4);
456
+ color: var(--text-muted);
457
+ vertical-align: middle;
458
+ }
459
+ .table-wrap tbody td.num-col {
460
+ text-align: right;
461
+ font-variant-numeric: tabular-nums;
462
+ font-feature-settings: "tnum";
463
+ color: var(--text);
464
+ font-family: var(--font-mono);
465
+ font-size: var(--text-xs);
466
+ }
467
+ .table-name { color: var(--text); font-weight: 450; }
468
+
469
+ /* ---- Code / kbd ---- */
470
+ code {
471
+ font: 400 var(--text-sm)/1 var(--font-mono);
472
+ background: var(--bg-sunken);
473
+ border: 1px solid var(--line-soft);
474
+ padding: 2px 6px;
475
+ border-radius: var(--radius);
476
+ color: var(--text-dim);
477
+ }
478
+ kbd {
479
+ font: 450 11px/1 var(--font-mono);
480
+ background: var(--bg-raised);
481
+ border: 1px solid var(--line-strong);
482
+ border-bottom-width: 2px;
483
+ border-radius: var(--radius);
484
+ padding: 2px 6px;
485
+ color: var(--text-dim);
486
+ letter-spacing: 0.02em;
487
+ }
488
+
489
+ /* ---- Stat / KPI ---- */
490
+ .stat-card {
491
+ background: var(--bg-raised);
492
+ border: 1px solid var(--line);
493
+ border-radius: var(--radius);
494
+ padding: var(--space-5);
495
+ }
496
+ .stat-label {
497
+ font: 450 11px/1 var(--font-sans);
498
+ letter-spacing: 0.07em;
499
+ text-transform: uppercase;
500
+ color: var(--text-dim);
501
+ margin-bottom: var(--space-3);
502
+ }
503
+ .stat-value {
504
+ font: 600 var(--text-2xl)/1 var(--font-mono);
505
+ color: var(--text);
506
+ letter-spacing: -0.02em;
507
+ font-variant-numeric: tabular-nums;
508
+ font-feature-settings: "tnum";
509
+ }
510
+ .stat-delta {
511
+ font: 450 var(--text-sm)/1 var(--font-sans);
512
+ margin-top: var(--space-2);
513
+ color: var(--text-dim);
514
+ }
515
+ .stat-delta.up { color: var(--success); }
516
+ .stat-delta.down { color: var(--danger); }
517
+ .stat-highlight .stat-value { color: var(--accent-2); }
518
+
519
+ /* ---- Toast ---- */
520
+ .toast-stack {
521
+ position: fixed;
522
+ bottom: var(--space-6);
523
+ right: var(--space-6);
524
+ display: flex;
525
+ flex-direction: column;
526
+ gap: var(--space-2);
527
+ z-index: 1000;
528
+ pointer-events: none;
529
+ width: 340px;
530
+ }
531
+ .toast {
532
+ background: var(--bg-raised);
533
+ border: 1px solid var(--line);
534
+ border-radius: var(--radius);
535
+ box-shadow: var(--shadow-float);
536
+ padding: var(--space-4);
537
+ display: flex;
538
+ align-items: flex-start;
539
+ gap: var(--space-3);
540
+ pointer-events: all;
541
+ animation: toast-in var(--dur-base) var(--ease-out) both;
542
+ }
543
+ .toast.removing { animation: toast-out var(--dur-fast) var(--ease) both; }
544
+ @keyframes toast-in {
545
+ from { opacity: 0; transform: translateY(8px); }
546
+ to { opacity: 1; transform: translateY(0); }
547
+ }
548
+ @keyframes toast-out {
549
+ to { opacity: 0; transform: translateY(4px); }
550
+ }
551
+ .toast-icon { flex-shrink: 0; margin-top: 1px; }
552
+ .toast-icon svg { width: 16px; height: 16px; }
553
+ .toast-body { flex: 1; min-width: 0; }
554
+ .toast-title { font: 450 var(--text-base)/1.3 var(--font-sans); color: var(--text); }
555
+ .toast-msg { font-size: var(--text-sm); color: var(--text-dim); margin-top: 3px; }
556
+ .toast-close {
557
+ background: none;
558
+ border: none;
559
+ cursor: pointer;
560
+ color: var(--text-faint);
561
+ flex-shrink: 0;
562
+ padding: 2px;
563
+ border-radius: var(--radius);
564
+ transition: color var(--dur-fast) var(--ease);
565
+ display: flex;
566
+ }
567
+ .toast-close:hover { color: var(--text); }
568
+ .toast-close svg { width: 14px; height: 14px; }
569
+ .toast-success { border-left: 3px solid var(--success); }
570
+ .toast-warning { border-left: 3px solid var(--warning); }
571
+ .toast-danger { border-left: 3px solid var(--danger); }
572
+ .toast-info { border-left: 3px solid var(--info); }
573
+
574
+ /* ---- Callout / alert ---- */
575
+ .callout {
576
+ display: flex;
577
+ gap: var(--space-3);
578
+ padding: 12px var(--space-4);
579
+ border-radius: var(--radius);
580
+ border-left: 3px solid;
581
+ background: var(--bg-raised);
582
+ border-top: 1px solid var(--line);
583
+ border-right: 1px solid var(--line);
584
+ border-bottom: 1px solid var(--line);
585
+ }
586
+ .callout-info { border-left-color: var(--info); background: color-mix(in srgb, var(--info) 6%, var(--bg-raised)); }
587
+ .callout-success { border-left-color: var(--success); background: color-mix(in srgb, var(--success) 6%, var(--bg-raised)); }
588
+ .callout-warning { border-left-color: var(--warning); background: color-mix(in srgb, var(--warning) 6%, var(--bg-raised)); }
589
+ .callout-danger { border-left-color: var(--danger); background: color-mix(in srgb, var(--danger) 6%, var(--bg-raised)); }
590
+ .callout-icon { flex-shrink: 0; margin-top: 1px; }
591
+ .callout-title { font: 450 var(--text-base)/1.3 var(--font-sans); color: var(--text); margin-bottom: 2px; }
592
+ .callout-body { font-size: var(--text-sm); color: var(--text-muted); }
593
+
594
+ /* ---- Popover / menu ---- */
595
+ .popover {
596
+ background: var(--bg-raised);
597
+ border: 1px solid var(--line);
598
+ border-radius: var(--radius);
599
+ box-shadow: var(--shadow-float);
600
+ overflow: hidden;
601
+ animation: popover-in var(--dur-base) var(--ease-out) both;
602
+ }
603
+ @keyframes popover-in {
604
+ from { opacity: 0; transform: translateY(-4px) scale(0.98); }
605
+ to { opacity: 1; transform: translateY(0) scale(1); }
606
+ }
607
+ .menu-item {
608
+ display: flex;
609
+ align-items: center;
610
+ gap: var(--space-3);
611
+ padding: 8px 14px;
612
+ font-size: var(--text-base);
613
+ color: var(--text-muted);
614
+ cursor: pointer;
615
+ transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
616
+ white-space: nowrap;
617
+ user-select: none;
618
+ }
619
+ .menu-item:hover { background: var(--bg-hover); color: var(--text); }
620
+ .menu-item.active { color: var(--accent); background: var(--accent-soft); }
621
+ .menu-item.danger { color: var(--danger); }
622
+ .menu-item.danger:hover { background: color-mix(in srgb, var(--danger) 10%, transparent); }
623
+ .menu-divider { height: 1px; background: var(--line); margin: 3px 0; }
624
+
625
+ /* ---- Avatar ---- */
626
+ .avatar {
627
+ width: 32px;
628
+ height: 32px;
629
+ border-radius: var(--radius-pill);
630
+ background: var(--accent-soft);
631
+ border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
632
+ display: flex;
633
+ align-items: center;
634
+ justify-content: center;
635
+ font: 550 var(--text-xs)/1 var(--font-sans);
636
+ color: var(--accent);
637
+ flex-shrink: 0;
638
+ user-select: none;
639
+ }
640
+
641
+ /* ---- Progress ---- */
642
+ .progress {
643
+ height: 3px;
644
+ background: var(--bg-hover);
645
+ border-radius: var(--radius-pill);
646
+ overflow: hidden;
647
+ }
648
+ .progress-bar {
649
+ height: 100%;
650
+ background: var(--accent);
651
+ border-radius: var(--radius-pill);
652
+ transition: width var(--dur-slow) var(--ease);
653
+ }
654
+
655
+ /* ---- Skeleton (no shimmer, by spec) ---- */
656
+ .skeleton {
657
+ background: var(--bg-hover);
658
+ border-radius: var(--radius);
659
+ }
660
+
661
+ /* ---- Data chip (mono variant of .tag) ----
662
+ Machine / numeric values: dates, token counts, costs, durations, hashes. */
663
+ .tag-data {
664
+ font-family: var(--font-mono);
665
+ letter-spacing: 0;
666
+ text-transform: none;
667
+ font-variant-numeric: tabular-nums;
668
+ background: var(--bg-hover);
669
+ color: var(--text-dim);
670
+ border-color: var(--line);
671
+ }
672
+
673
+ /* ---- Key-value / metadata table ----
674
+ Mono readout, no header row; fixed-width uppercase label column. */
675
+ .info-table { width: 100%; border-collapse: collapse; font: 400 var(--text-sm)/1.5 var(--font-mono); }
676
+ .info-table td {
677
+ padding: 8px var(--space-3);
678
+ border-bottom: 1px solid var(--line);
679
+ color: var(--text);
680
+ vertical-align: top;
681
+ }
682
+ .info-table tr:last-child td { border-bottom: none; }
683
+ .info-table td:first-child {
684
+ width: 140px;
685
+ font: 450 11px/1.5 var(--font-sans);
686
+ letter-spacing: 0.07em;
687
+ text-transform: uppercase;
688
+ color: var(--text-faint);
689
+ white-space: nowrap;
690
+ }
691
+
692
+ /* ---- Range slider ---- */
693
+ .slider {
694
+ -webkit-appearance: none;
695
+ appearance: none;
696
+ width: 100%;
697
+ height: 4px;
698
+ background: var(--bg-hover);
699
+ border-radius: var(--radius-pill);
700
+ cursor: pointer;
701
+ }
702
+ .slider::-webkit-slider-thumb {
703
+ -webkit-appearance: none;
704
+ width: 14px; height: 14px;
705
+ background: var(--accent);
706
+ border: none;
707
+ border-radius: 50%;
708
+ cursor: pointer;
709
+ }
710
+ .slider::-moz-range-thumb {
711
+ width: 14px; height: 14px;
712
+ background: var(--accent);
713
+ border: none;
714
+ border-radius: 50%;
715
+ cursor: pointer;
716
+ }
717
+ .slider:focus-visible { outline: 2px solid var(--accent-ring); outline-offset: 4px; }
718
+
719
+ /* ---- Color swatch ---- */
720
+ .swatch {
721
+ width: 48px; height: 48px;
722
+ border: 2px solid transparent;
723
+ border-radius: var(--radius);
724
+ cursor: pointer;
725
+ transition: border-color var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease);
726
+ }
727
+ .swatch:hover { border-color: var(--text-dim); transform: translateY(-2px); }
728
+ .swatch-label { font: 400 10px/1.4 var(--font-mono); color: var(--text-dim); margin-top: 4px; }
729
+
730
+ /* ---- Image zoom on hover ---- */
731
+ .zoom-img { overflow: hidden; }
732
+ .zoom-img img, img.zoom-img { display: block; width: 100%; transition: transform var(--dur-slow) var(--ease); }
733
+ .zoom-img:hover img, img.zoom-img:hover { transform: scale(1.02); }
734
+
735
+ /* ---- Sticky section header ----
736
+ Pins under the nav; gains the raised tone once stuck. JS toggles .is-stuck via
737
+ an IntersectionObserver sentinel — see "Reveal JS" below. */
738
+ .section-sticky {
739
+ position: sticky;
740
+ top: var(--space-12);
741
+ z-index: 5;
742
+ padding: var(--space-2) 0;
743
+ background: var(--bg);
744
+ transition: background var(--dur-fast) var(--ease);
745
+ }
746
+ .section-sticky.is-stuck { background: var(--bg-raised); }
747
+
748
+ /* ---- Scroll reveals ----
749
+ The hiding state lives on a class JS adds at runtime (see "Reveal JS"); markup
750
+ carries only [data-reveal], so no-JS / pre-JS shows the final content. */
751
+ .reveal {
752
+ opacity: 0;
753
+ transform: translateY(16px);
754
+ transition: opacity var(--dur-slow) var(--ease-out), transform var(--dur-slow) var(--ease-out);
755
+ }
756
+ .reveal.in { opacity: 1; transform: none; }
757
+ .reveal-wipe {
758
+ opacity: 0;
759
+ clip-path: inset(0 100% 0 0);
760
+ transition: opacity var(--dur-base) var(--ease-out), clip-path var(--dur-slow) var(--ease-out);
761
+ }
762
+ .reveal-wipe.in { opacity: 1; clip-path: inset(0 0 0 0); }
763
+ @media (prefers-reduced-motion: reduce) {
764
+ .reveal, .reveal-wipe { opacity: 1; transform: none; clip-path: none; transition: none; }
765
+ }
766
+
767
+ /* ---- Custom scrollbar ---- */
768
+ * { scrollbar-width: thin; scrollbar-color: var(--line-strong) transparent; }
769
+ ::-webkit-scrollbar { width: 8px; height: 8px; }
770
+ ::-webkit-scrollbar-track { background: var(--bg-sunken); }
771
+ ::-webkit-scrollbar-thumb { background: var(--line-strong); border-radius: var(--radius-pill); }
772
+ ::-webkit-scrollbar-thumb:hover { background: var(--text-faint); }
773
+
774
+ /* ---- Cursor-reactive dot-grid background (sanctioned ambient; desktop only) ----
775
+ Canvas painted on demand by JS (see "Background & cursor JS"); sits behind all
776
+ content via background propagation. Solid cards keep it to negative space. */
777
+ #bg-dots { position: fixed; inset: 0; z-index: -1; pointer-events: none; }
778
+
779
+ /* ---- Trailing pointer ring (desktop fine-pointer only) ----
780
+ ONE node is the whole cursor. It TIGHTENS over a target rather than growing, which keeps it
781
+ off the small ones, and over text it morphs into the caret bar itself. No dot in the middle
782
+ and no second node for the caret: another mark under the pointer competes with the thing
783
+ being pointed at, and a fade between two nodes throws away the morph.
784
+ Five states: idle, `.hover`, `.press`, `.blocked` over anything refusing the click, and
785
+ `.text` over a glyph run or a text field. `.text` is gated by a hit test against the glyphs,
786
+ never by tag name (see the JS).
787
+ EVERY STATE IS A WIDTH/HEIGHT CHANGE, never `transform: scale()`: a scaled 1px border
788
+ renders sub-pixel, so a ring that tightens by scale fades as it tightens. The ring is fixed
789
+ and childless, so nothing here touches page layout. */
790
+ #cursor {
791
+ position: fixed;
792
+ top: 0; left: 0;
793
+ z-index: 10000;
794
+ pointer-events: none;
795
+ opacity: 0;
796
+ transition: opacity var(--dur-base) var(--ease);
797
+ --caret-h: 20px;
798
+ }
799
+ #cursor.visible { opacity: 1; }
800
+
801
+ #cursor-ring {
802
+ position: fixed;
803
+ top: 0; left: 0;
804
+ width: 26px; height: 26px;
805
+ border: 1px solid var(--accent);
806
+ border-radius: 50%;
807
+ will-change: transform;
808
+ /* AN ACCENT HAIRLINE IS INVISIBLE OVER THE ACCENT FILLS — buttons, active chips, the sidebar
809
+ cursor, bar fills — and those are exactly the things worth pointing at, so a keyline in the
810
+ PAGE BACKGROUND rides just outside it. Whatever is underneath, exactly one of the two reads:
811
+ on the page the keyline is the background and only the hairline shows, on an accent fill the
812
+ hairline is the fill and only the keyline shows. It follows the shape through the caret
813
+ morph, and it is the surface colour rather than a dark one for the light theme's sake, where
814
+ a dark edge reads as a smudge and swallows the 1px line it was meant to protect. */
815
+ box-shadow: 0 0 0 1px var(--bg);
816
+ transition: width var(--dur-base) var(--ease), height var(--dur-base) var(--ease),
817
+ border-radius var(--dur-base) var(--ease), border-color var(--dur-fast) var(--ease),
818
+ background var(--dur-fast) var(--ease);
819
+ }
820
+
821
+ /* 4px per state: the tighten only has to be legible next to the state before it, and a deep one
822
+ reads as the ring flinching away from the target. The fill jumps as well, because size alone
823
+ stops separating press from hover once the steps are this small. */
824
+ #cursor.hover #cursor-ring { width: 22px; height: 22px; background: color-mix(in srgb, var(--accent) 12%, transparent); }
825
+ #cursor.hover.press #cursor-ring { width: 18px; height: 18px; background: color-mix(in srgb, var(--accent) 26%, transparent); }
826
+
827
+ /* refuses the click: holds idle size on purpose. Not doing the tighten is the signal */
828
+ #cursor.blocked #cursor-ring { border-color: var(--danger); }
829
+
830
+ /* the ring IS the caret over text: the circle squeezes into the bar, and the transparent border
831
+ lets the fill through the full 2px. The JS drops the trail here, so it stands under the
832
+ pointer, where the click will put the real caret. */
833
+ #cursor.text #cursor-ring {
834
+ width: 2px; height: var(--caret-h);
835
+ border-radius: 1px;
836
+ border-color: transparent;
837
+ background: var(--accent);
838
+ }
839
+
840
+ /* one-shot on release over a target, removed on animationend. Placed by left/top so the keyframe
841
+ owns the transform outright, and it only ever scales UP, which thickens the hairline. Carries
842
+ the ring's keyline: it fires over targets, and those are the fills it would vanish into */
843
+ .cursor-pulse {
844
+ position: fixed;
845
+ width: 26px; height: 26px;
846
+ margin: -13px 0 0 -13px;
847
+ border: 1px solid var(--accent);
848
+ border-radius: 50%;
849
+ box-shadow: 0 0 0 1px var(--bg);
850
+ will-change: transform;
851
+ animation: cursor-pulse var(--dur-slow) var(--ease-out) forwards;
852
+ }
853
+ @keyframes cursor-pulse {
854
+ from { transform: scale(0.6); opacity: 0.6; }
855
+ to { transform: scale(2.1); opacity: 0; }
856
+ }
857
+
858
+ /* `-webkit-user-drag`: a native drag puts the OS cursor and a ghost image back on screen, and
859
+ neither obeys `cursor: none`. The JS cancels dragstart too, for the engines ignoring this.
860
+ Selection survives — it stops elements being dragged, not text being picked. */
861
+ html.custom-cursor, html.custom-cursor * { cursor: none !important; -webkit-user-drag: none; }
862
+ @media (hover: none), (pointer: coarse) { #cursor { display: none; } }
863
+
864
+ /* ---- Divider ---- */
865
+ hr, .divider {
866
+ height: 1px;
867
+ background: var(--line);
868
+ border: none;
869
+ margin: var(--space-6) 0;
870
+ }
871
+
872
+ /* ---- Breadcrumb ---- */
873
+ .breadcrumb {
874
+ display: flex;
875
+ align-items: center;
876
+ gap: var(--space-2);
877
+ font-size: var(--text-sm);
878
+ color: var(--text-dim);
879
+ }
880
+ .breadcrumb-sep { color: var(--line-strong); }
881
+ .breadcrumb-item {
882
+ cursor: pointer;
883
+ transition: color var(--dur-fast) var(--ease);
884
+ color: var(--text-dim);
885
+ text-decoration: none;
886
+ }
887
+ .breadcrumb-item:hover { color: var(--text); }
888
+ .breadcrumb-item.current { color: var(--text); cursor: default; }
889
+
890
+ /* ---- Sidebar nav ---- */
891
+ .sidebar {
892
+ background: var(--bg-sunken);
893
+ border-right: 1px solid var(--line);
894
+ width: 220px;
895
+ display: flex;
896
+ flex-direction: column;
897
+ padding: var(--space-5) 0 0;
898
+ position: sticky;
899
+ top: 0;
900
+ height: 100vh;
901
+ overflow-y: auto;
902
+ }
903
+ .sidebar-logo {
904
+ padding: 0 var(--space-4) var(--space-5);
905
+ border-bottom: 1px solid var(--line);
906
+ margin-bottom: var(--space-3);
907
+ }
908
+ .logo-name {
909
+ font: 550 var(--text-md)/1 var(--font-sans);
910
+ color: var(--text);
911
+ letter-spacing: -0.02em;
912
+ }
913
+ .logo-version {
914
+ font: 450 10px/1 var(--font-mono);
915
+ color: var(--text-faint);
916
+ margin-top: 3px;
917
+ }
918
+ .nav-group { margin-bottom: var(--space-4); position: relative; }
919
+ .nav-group-label {
920
+ font: 450 11px/1 var(--font-sans);
921
+ letter-spacing: 0.08em;
922
+ text-transform: uppercase;
923
+ color: var(--text-faint);
924
+ padding: 0 var(--space-4) var(--space-2);
925
+ }
926
+ .nav-item {
927
+ display: flex;
928
+ align-items: center;
929
+ gap: var(--space-2);
930
+ padding: 9px var(--space-4);
931
+ font: 400 var(--text-base)/1 var(--font-sans);
932
+ color: var(--text-dim);
933
+ cursor: pointer;
934
+ position: relative;
935
+ transition: color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
936
+ border: none;
937
+ background: none;
938
+ width: 100%;
939
+ text-align: left;
940
+ text-decoration: none;
941
+ user-select: none;
942
+ }
943
+ .nav-item:hover { color: var(--text-muted); background: var(--bg-hover); }
944
+ .nav-item.active { color: var(--accent); background: var(--accent-soft); font-weight: 450; }
945
+ .nav-item svg { width: 14px; height: 14px; flex-shrink: 0; opacity: 0.7; }
946
+ .nav-item.active svg { opacity: 1; }
947
+
948
+ /* Sliding sapphire cursor — positioned by JS, see "Sidebar JS" below */
949
+ .nav-indicator {
950
+ position: absolute;
951
+ left: 0;
952
+ width: 2px;
953
+ background: var(--accent);
954
+ border-radius: 0 1px 1px 0;
955
+ pointer-events: none;
956
+ transition:
957
+ top 500ms cubic-bezier(0, 0.85, 0.35, 1),
958
+ height 500ms cubic-bezier(0, 0.85, 0.35, 1),
959
+ opacity 180ms var(--ease);
960
+ will-change: top, height;
961
+ opacity: 0;
962
+ }
963
+
964
+ /* ---- Top navbar ---- */
965
+ .navbar {
966
+ display: flex;
967
+ align-items: stretch;
968
+ height: 56px;
969
+ padding: 0 var(--space-6);
970
+ background: var(--bg-sunken);
971
+ border-bottom: 1px solid var(--line);
972
+ position: sticky;
973
+ top: 0;
974
+ z-index: 100;
975
+ }
976
+ .navbar-brand {
977
+ display: flex;
978
+ flex-direction: column;
979
+ justify-content: center;
980
+ padding-right: var(--space-5);
981
+ margin-right: var(--space-3);
982
+ border-right: 1px solid var(--line);
983
+ }
984
+ .navbar-nav {
985
+ display: flex;
986
+ align-items: stretch;
987
+ gap: 0;
988
+ position: relative;
989
+ flex: 1;
990
+ }
991
+ .navbar-link {
992
+ position: relative;
993
+ display: inline-flex;
994
+ align-items: center;
995
+ gap: var(--space-2);
996
+ padding: 0 var(--space-4);
997
+ font: 400 var(--text-base)/1 var(--font-sans);
998
+ color: var(--text-muted);
999
+ cursor: pointer;
1000
+ text-decoration: none;
1001
+ background: transparent;
1002
+ border: none;
1003
+ transition:
1004
+ color var(--dur-fast) var(--ease),
1005
+ background var(--dur-fast) var(--ease);
1006
+ user-select: none;
1007
+ white-space: nowrap;
1008
+ }
1009
+ .navbar-link:hover { color: var(--text); background: var(--bg-hover); }
1010
+ .navbar-link.active { color: var(--accent); font-weight: 450; }
1011
+ .navbar-link svg { width: 14px; height: 14px; flex-shrink: 0; opacity: 0.7; }
1012
+ .navbar-link.active svg { opacity: 1; }
1013
+
1014
+ .navbar-badge {
1015
+ font: 450 10px/1 var(--font-sans);
1016
+ padding: 2px 5px;
1017
+ border-radius: var(--radius-pill);
1018
+ background: var(--bg-hover);
1019
+ color: var(--text-dim);
1020
+ }
1021
+ .navbar-link.active .navbar-badge { background: var(--accent-soft); color: var(--accent); }
1022
+
1023
+ /* Sliding sapphire underline — same physics as .tab-ink, positioned by JS */
1024
+ .navbar-ink {
1025
+ position: absolute;
1026
+ bottom: -1px;
1027
+ height: 2px;
1028
+ background: var(--accent);
1029
+ border-radius: 1px;
1030
+ pointer-events: none;
1031
+ transition:
1032
+ left 440ms cubic-bezier(0, 0.85, 0.35, 1),
1033
+ width 440ms cubic-bezier(0, 0.85, 0.35, 1),
1034
+ opacity 180ms var(--ease);
1035
+ will-change: left, width;
1036
+ opacity: 0;
1037
+ }
1038
+
1039
+ .navbar-actions {
1040
+ display: flex;
1041
+ align-items: center;
1042
+ gap: var(--space-2);
1043
+ margin-left: auto;
1044
+ padding-left: var(--space-3);
1045
+ }
1046
+ .navbar-search {
1047
+ position: relative;
1048
+ width: 240px;
1049
+ }
1050
+ .navbar-search .input { padding: 6px 10px 6px 30px; font-size: var(--text-sm); }
1051
+ .navbar-search .input-icon { left: 9px; }
1052
+
1053
+ /* ---- Mobile hamburger / drawer (shared by sidebar and navbar) ---- */
1054
+ .mobile-topbar {
1055
+ display: none;
1056
+ align-items: center;
1057
+ justify-content: space-between;
1058
+ padding: 0 var(--space-4);
1059
+ height: 52px;
1060
+ background: var(--bg-sunken);
1061
+ border-bottom: 1px solid var(--line);
1062
+ position: sticky;
1063
+ top: 0;
1064
+ z-index: 200;
1065
+ }
1066
+ .hamburger {
1067
+ display: flex;
1068
+ align-items: center;
1069
+ justify-content: center;
1070
+ width: 32px;
1071
+ min-width: 32px;
1072
+ height: 32px;
1073
+ padding: 0;
1074
+ background: none;
1075
+ border: 1px solid var(--line);
1076
+ border-radius: var(--radius);
1077
+ cursor: pointer;
1078
+ color: var(--text-dim);
1079
+ transition: border-color var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
1080
+ }
1081
+ .hamburger:hover { border-color: var(--line-strong); color: var(--text); }
1082
+
1083
+ .sidebar-overlay {
1084
+ display: none;
1085
+ position: fixed;
1086
+ inset: 0;
1087
+ background: rgb(0 0 0 / 0.5);
1088
+ z-index: 299;
1089
+ opacity: 0;
1090
+ transition: opacity var(--dur-base) var(--ease);
1091
+ }
1092
+ .sidebar-overlay.open { opacity: 1; }
1093
+
1094
+ @media (max-width: 768px) {
1095
+ .mobile-topbar { display: flex; }
1096
+ .sidebar {
1097
+ position: fixed;
1098
+ top: 0;
1099
+ left: 0;
1100
+ height: 100vh;
1101
+ z-index: 300;
1102
+ transform: translateX(-100%);
1103
+ transition:
1104
+ transform var(--dur-base) var(--ease-out),
1105
+ box-shadow var(--dur-base) var(--ease);
1106
+ width: 240px;
1107
+ }
1108
+ .sidebar.open {
1109
+ transform: translateX(0);
1110
+ box-shadow: var(--shadow-float);
1111
+ }
1112
+ .sidebar-overlay { display: block; pointer-events: none; }
1113
+ .sidebar-overlay.open { pointer-events: all; }
1114
+ .navbar-nav { display: none; }
1115
+ .navbar-search { display: none; }
1116
+ }