syndes 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 (96) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +77 -0
  3. package/adapters/claude-code.mjs +59 -0
  4. package/adapters/codex.mjs +256 -0
  5. package/adapters/index.mjs +92 -0
  6. package/analytics/index.mjs +189 -0
  7. package/analytics/metrics/context.mjs +95 -0
  8. package/analytics/metrics/cost.mjs +83 -0
  9. package/analytics/metrics/friction.mjs +86 -0
  10. package/analytics/metrics/prompts.mjs +93 -0
  11. package/analytics/metrics/rework.mjs +113 -0
  12. package/analytics/metrics/time.mjs +104 -0
  13. package/analytics/metrics/tokens.mjs +88 -0
  14. package/analytics/metrics/tools.mjs +118 -0
  15. package/analytics/metrics/volume.mjs +98 -0
  16. package/analytics/ranges.mjs +98 -0
  17. package/analytics/rollup.mjs +151 -0
  18. package/analytics/score.mjs +194 -0
  19. package/bin/cli.mjs +596 -0
  20. package/bin/postinstall.mjs +44 -0
  21. package/collect/classify.mjs +226 -0
  22. package/collect/git.mjs +78 -0
  23. package/collect/projects.mjs +82 -0
  24. package/collect/redact.mjs +85 -0
  25. package/collect/sessions.mjs +119 -0
  26. package/collect/tail.mjs +126 -0
  27. package/collect/tools.mjs +121 -0
  28. package/collect/transcript.mjs +128 -0
  29. package/dashboard/api/index.mjs +296 -0
  30. package/dashboard/auth.mjs +235 -0
  31. package/dashboard/router.mjs +55 -0
  32. package/dashboard/security.mjs +95 -0
  33. package/dashboard/server.mjs +156 -0
  34. package/dashboard/static.mjs +47 -0
  35. package/dashboard/web/SynDes.icns +0 -0
  36. package/dashboard/web/api.js +80 -0
  37. package/dashboard/web/app.css +532 -0
  38. package/dashboard/web/app.js +261 -0
  39. package/dashboard/web/charts.js +273 -0
  40. package/dashboard/web/index.html +23 -0
  41. package/dashboard/web/logo.png +0 -0
  42. package/dashboard/web/ui.js +434 -0
  43. package/dashboard/web/views/habits.js +166 -0
  44. package/dashboard/web/views/ledger.js +164 -0
  45. package/dashboard/web/views/overview.js +214 -0
  46. package/dashboard/web/views/sessions.js +133 -0
  47. package/dashboard/web/views/settings.js +180 -0
  48. package/ledger/append.mjs +126 -0
  49. package/ledger/chain.mjs +53 -0
  50. package/ledger/keys.mjs +72 -0
  51. package/ledger/read.mjs +77 -0
  52. package/ledger/retention.mjs +104 -0
  53. package/ledger/schema.mjs +96 -0
  54. package/ledger/segments.mjs +109 -0
  55. package/ledger/verify.mjs +174 -0
  56. package/notify/index.mjs +67 -0
  57. package/notify/linux.mjs +41 -0
  58. package/notify/mac.mjs +44 -0
  59. package/notify/terminal.mjs +15 -0
  60. package/notify/windows.mjs +61 -0
  61. package/package.json +66 -0
  62. package/practices/budget.mjs +97 -0
  63. package/practices/catalog.mjs +64 -0
  64. package/practices/deliver.mjs +101 -0
  65. package/practices/engine.mjs +107 -0
  66. package/practices/rules/batch-tool-calls.mjs +15 -0
  67. package/practices/rules/context-hygiene.mjs +17 -0
  68. package/practices/rules/delegate-wide-search.mjs +15 -0
  69. package/practices/rules/index.mjs +28 -0
  70. package/practices/rules/permission-friction.mjs +16 -0
  71. package/practices/rules/project-memory.mjs +27 -0
  72. package/practices/rules/prompt-specificity.mjs +15 -0
  73. package/practices/rules/read-before-edit.mjs +16 -0
  74. package/practices/rules/retry-storm.mjs +22 -0
  75. package/practices/rules/session-sprawl.mjs +15 -0
  76. package/practices/rules/verify-after-change.mjs +16 -0
  77. package/runtime/config.mjs +116 -0
  78. package/runtime/hook.mjs +154 -0
  79. package/runtime/jsonl.mjs +104 -0
  80. package/runtime/lock.mjs +98 -0
  81. package/runtime/log.mjs +37 -0
  82. package/runtime/paths.mjs +116 -0
  83. package/runtime/platform.mjs +74 -0
  84. package/runtime/spool.mjs +92 -0
  85. package/runtime/worker.mjs +275 -0
  86. package/src/briefing.mjs +94 -0
  87. package/src/doctor.mjs +153 -0
  88. package/src/export.mjs +68 -0
  89. package/src/install.mjs +95 -0
  90. package/src/open.mjs +23 -0
  91. package/src/report.mjs +120 -0
  92. package/src/settings.mjs +173 -0
  93. package/src/status.mjs +61 -0
  94. package/src/systemauth.mjs +179 -0
  95. package/src/term.mjs +272 -0
  96. package/src/uninstall.mjs +43 -0
@@ -0,0 +1,532 @@
1
+ /**
2
+ * syndes — theme.
3
+ *
4
+ * Palette sampled directly from the reference: lime #B9FF66, orange #FF9B22,
5
+ * card #1F1F1F on a pure black shell, sitting on a #C5E1A5 page.
6
+ *
7
+ * Two rules the whole design follows:
8
+ * • Lime means healthy, orange means "look at this", white means neutral
9
+ * volume. Nothing is coloured for decoration — if a thing is coloured, the
10
+ * colour is carrying meaning, and the legend says which.
11
+ * • Every shape is a capsule. Bars, chips, buttons, avatars and meters all
12
+ * use the same fully-rounded end, which is what makes the surface read as
13
+ * one object rather than a pile of widgets.
14
+ */
15
+
16
+ :root {
17
+ /* Sampled from the reference image */
18
+ --mint: #C5E1A5;
19
+ --black: #000000;
20
+ --surface: #1F1F1F;
21
+ --surface-2: #171717;
22
+ --surface-3: #262626;
23
+ --line: #2E2E2E;
24
+
25
+ --ink: #FFFFFF;
26
+ --ink-2: #A6A6A6;
27
+ --ink-3: #6E6E6E;
28
+
29
+ --lime: #B9FF66;
30
+ --orange: #FF9B22;
31
+ --red: #E5484D;
32
+
33
+ /* Semantic aliases. Views name meaning, never hue. */
34
+ --good: var(--lime);
35
+ --warn: var(--orange);
36
+ --neutral: #FFFFFF;
37
+ --track: #313131;
38
+
39
+ --r-shell: 40px;
40
+ --r-card: 28px;
41
+ --r-pill: 999px;
42
+ --r-chip: 14px;
43
+
44
+ --s1: 4px; --s2: 8px; --s3: 12px; --s4: 16px;
45
+ --s5: 22px; --s6: 28px; --s7: 40px; --s8: 56px;
46
+
47
+ --rail: 56px;
48
+
49
+ --font: -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
50
+ --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
51
+ }
52
+
53
+ *, *::before, *::after { box-sizing: border-box; }
54
+
55
+ /* The page itself never scrolls. Every view is sized to the viewport and the
56
+ scrolling happens inside whichever section actually has more than fits —
57
+ which is what keeps the shell's rounded corners on screen at all times. */
58
+ html, body { height: 100%; overflow: hidden; }
59
+
60
+ body {
61
+ margin: 0;
62
+ padding: 18px;
63
+ background: var(--mint);
64
+ color: var(--ink);
65
+ font: 13px/1.5 var(--font);
66
+ -webkit-font-smoothing: antialiased;
67
+ }
68
+
69
+ /* Thin, quiet scrollbars. A default 15px grey trough beside a black card reads
70
+ as a seam in the design rather than as a control. */
71
+ * { scrollbar-width: thin; scrollbar-color: var(--surface-3) transparent; }
72
+ ::-webkit-scrollbar { width: 8px; height: 8px; }
73
+ ::-webkit-scrollbar-track { background: transparent; }
74
+ ::-webkit-scrollbar-thumb { background: var(--surface-3); border-radius: var(--r-pill); }
75
+ ::-webkit-scrollbar-thumb:hover { background: #3d3d3d; }
76
+ ::-webkit-scrollbar-corner { background: transparent; }
77
+
78
+ h1, h2, h3, p, figure { margin: 0; }
79
+ button, input, select { font: inherit; color: inherit; }
80
+ a { color: inherit; text-decoration: none; }
81
+ :focus-visible { outline: 2px solid var(--lime); outline-offset: 3px; border-radius: var(--r-chip); }
82
+
83
+ .num, .tnum { font-variant-numeric: tabular-nums; }
84
+
85
+ /* ── Shell ────────────────────────────────────────────────────────────── */
86
+
87
+ .shell {
88
+ background: var(--black);
89
+ border-radius: var(--r-shell);
90
+ height: calc(100vh - 36px);
91
+ padding: var(--s5) var(--s6) var(--s6);
92
+ display: flex;
93
+ flex-direction: column;
94
+ gap: var(--s4);
95
+ overflow: hidden;
96
+ }
97
+
98
+ /* ── Top bar ──────────────────────────────────────────────────────────── */
99
+
100
+ .topbar { display: flex; align-items: center; gap: var(--s3); }
101
+
102
+ .logo {
103
+ width: 46px; height: 46px;
104
+ border-radius: var(--r-pill);
105
+ background: var(--ink);
106
+ display: grid; place-items: center;
107
+ flex: none;
108
+ overflow: hidden;
109
+ }
110
+ /* The mark is black ink on transparency, so it sits directly on the white chip.
111
+ 72% keeps an even optical margin inside the circle — the glyph is wider than
112
+ it is tall, so a full-bleed fit would crowd the sides. */
113
+ .logo img { width: 72%; height: 72%; object-fit: contain; display: block; }
114
+
115
+ .nav { display: flex; gap: var(--s2); }
116
+
117
+ .navpill {
118
+ display: inline-flex; align-items: center; gap: var(--s2);
119
+ height: 46px; padding: 0 var(--s5);
120
+ border: 0; border-radius: var(--r-pill);
121
+ background: var(--surface);
122
+ color: var(--ink-2);
123
+ font-weight: 550;
124
+ cursor: pointer;
125
+ white-space: nowrap;
126
+ }
127
+ .navpill:hover { background: var(--surface-3); color: var(--ink); }
128
+ .navpill[aria-current="page"] { background: var(--ink); color: var(--black); font-weight: 650; }
129
+ .navpill__dot {
130
+ min-width: 18px; height: 18px; padding: 0 5px;
131
+ border-radius: var(--r-pill);
132
+ background: var(--orange); color: var(--black);
133
+ font-size: 11px; font-weight: 700;
134
+ display: grid; place-items: center;
135
+ }
136
+ .navpill[aria-current="page"] .navpill__dot { background: var(--black); color: var(--lime); }
137
+
138
+ .topbar__right { margin-left: auto; display: flex; align-items: center; gap: var(--s3); }
139
+
140
+ .status {
141
+ display: flex; align-items: center; gap: var(--s2);
142
+ height: 46px; padding: 0 var(--s4) 0 var(--s3);
143
+ border-radius: var(--r-pill);
144
+ background: var(--surface);
145
+ font-size: 12px; color: var(--ink-2);
146
+ }
147
+ .status__led { width: 8px; height: 8px; border-radius: var(--r-pill); background: var(--lime); flex: none; }
148
+ .status__led--bad { background: var(--orange); }
149
+
150
+ /* ── Page head ────────────────────────────────────────────────────────── */
151
+
152
+ .pagehead { display: flex; align-items: center; gap: var(--s4); flex-wrap: wrap; }
153
+
154
+ .title {
155
+ font-size: 38px;
156
+ font-weight: 800;
157
+ letter-spacing: -0.025em;
158
+ text-transform: uppercase;
159
+ line-height: 1;
160
+ }
161
+ .title__sub { font-size: 13px; font-weight: 400; color: var(--ink-3); text-transform: none; letter-spacing: 0; margin-top: 6px; }
162
+
163
+ .filters { margin-left: auto; display: flex; gap: var(--s2); flex-wrap: wrap; }
164
+
165
+ .filter {
166
+ display: inline-flex; align-items: center; gap: var(--s2);
167
+ height: 44px; padding: 0 var(--s4);
168
+ border: 0; border-radius: var(--r-pill);
169
+ background: var(--surface);
170
+ color: var(--ink-2);
171
+ cursor: pointer;
172
+ }
173
+ .filter__key { color: var(--ink-3); }
174
+ .filter__value { color: var(--ink); font-weight: 600; }
175
+
176
+ /* ── Dropdown ─────────────────────────────────────────────────────────── */
177
+
178
+ .dropdown {
179
+ display: inline-flex; align-items: center; gap: var(--s2);
180
+ height: 44px; padding: 0 var(--s4);
181
+ border: 0; border-radius: var(--r-pill);
182
+ background: var(--surface); color: var(--ink);
183
+ cursor: pointer; font: inherit;
184
+ }
185
+ .dropdown:hover { background: var(--surface-3); }
186
+ .dropdown--on-card { background: var(--surface-3); height: 34px; padding: 0 var(--s3) 0 var(--s4); font-size: 12px; }
187
+ .dropdown--on-card:hover { background: #333; }
188
+ .dropdown__label { color: var(--ink-3); }
189
+ .dropdown__value { font-weight: 600; }
190
+ .dropdown svg { color: var(--ink-3); transition: transform 0.15s ease; flex: none; }
191
+ .dropdown[aria-expanded="true"] svg { transform: rotate(180deg); }
192
+ .dropdown[aria-expanded="true"] { background: var(--surface-3); }
193
+
194
+ .dropdown__menu {
195
+ position: fixed;
196
+ z-index: 60;
197
+ padding: 6px;
198
+ border-radius: 18px;
199
+ background: var(--surface-3);
200
+ box-shadow: 0 18px 40px rgb(0 0 0 / 0.55);
201
+ display: grid;
202
+ gap: 2px;
203
+ max-height: min(70vh, 420px);
204
+ overflow-y: auto;
205
+ }
206
+ .dropdown__item {
207
+ display: flex; align-items: center; gap: var(--s2);
208
+ width: 100%; text-align: left;
209
+ padding: 9px var(--s3);
210
+ border: 0; border-radius: 12px;
211
+ background: transparent; color: var(--ink-2);
212
+ cursor: pointer; font: inherit; white-space: nowrap;
213
+ }
214
+ .dropdown__item:hover, .dropdown__item:focus-visible { background: rgb(255 255 255 / 0.06); color: var(--ink); outline: none; }
215
+ /* The selected row carries its own raised surface. Leaving it to :focus-visible
216
+ made the highlight appear or not depending on where focus happened to be, so
217
+ the same menu looked different from one screenshot to the next. */
218
+ .dropdown__item[aria-selected="true"] {
219
+ background: rgb(255 255 255 / 0.08);
220
+ color: var(--ink);
221
+ font-weight: 600;
222
+ }
223
+ .dropdown__item[aria-selected="true"]:hover { background: rgb(255 255 255 / 0.12); }
224
+ .dropdown__item[aria-selected="true"] .dropdown__item-hint { color: var(--ink-2); }
225
+ .dropdown__check { width: 15px; display: grid; place-items: center; color: var(--lime); flex: none; }
226
+ .dropdown__item-name { display: block; }
227
+ .dropdown__item-hint { display: block; font-size: 11px; color: var(--ink-3); font-weight: 400; margin-top: 1px; }
228
+
229
+ /* ── Body: rail + content ─────────────────────────────────────────────── */
230
+
231
+ /* min-height:0 is what lets a flex child actually shrink and scroll instead of
232
+ growing the page. Without it the whole viewport lock silently does nothing. */
233
+ .body { display: grid; grid-template-columns: var(--rail) minmax(0, 1fr); gap: var(--s4); flex: 1; min-height: 0; }
234
+
235
+ .rail { display: flex; flex-direction: column; gap: var(--s3); }
236
+ .railbtn {
237
+ width: var(--rail); height: var(--rail);
238
+ border: 0; border-radius: var(--r-pill);
239
+ background: var(--surface);
240
+ color: var(--ink-2);
241
+ display: grid; place-items: center;
242
+ cursor: pointer;
243
+ flex: none;
244
+ }
245
+ .railbtn svg { width: 20px; height: 20px; display: block; }
246
+ .railbtn[aria-current="page"] { background: var(--ink); color: var(--black); }
247
+ .railbtn:hover { background: var(--surface-3); color: var(--ink); }
248
+ .railbtn--accent { background: var(--lime); color: var(--black); }
249
+ .railbtn--accent:hover { background: var(--lime); filter: brightness(1.08); }
250
+ .rail__spacer { flex: 1; }
251
+
252
+ .content { display: grid; gap: var(--s4); align-content: start; min-width: 0; min-height: 0; overflow-y: auto; padding-right: 4px; }
253
+ /* A view that fits exactly: fixed top row, remaining space to the rest. */
254
+ .content--fit { align-content: stretch; overflow: hidden; }
255
+ .fill { min-height: 0; overflow: hidden; }
256
+ .fill > .card { height: 100%; }
257
+
258
+ /* ── Card ─────────────────────────────────────────────────────────────── */
259
+
260
+ .card {
261
+ background: var(--surface);
262
+ border-radius: var(--r-card);
263
+ padding: var(--s5);
264
+ display: flex;
265
+ flex-direction: column;
266
+ gap: var(--s3);
267
+ min-width: 0;
268
+ min-height: 0;
269
+ }
270
+ /* The scrolling region inside a card: the head stays put, the body moves. */
271
+ .card__scroll { overflow-y: auto; min-height: 0; flex: 1; margin-right: -6px; padding-right: 6px; }
272
+ .card--flush { padding: var(--s6) 0; }
273
+ .card--tight { gap: var(--s3); }
274
+
275
+ .card__head { display: flex; align-items: center; gap: var(--s3); }
276
+ .card__title {
277
+ font-size: 14px; font-weight: 700;
278
+ letter-spacing: 0.04em; text-transform: uppercase;
279
+ }
280
+ .card__note { font-size: 12px; color: var(--ink-3); }
281
+ /* Actions sit far right; the note stays next to the title it qualifies. Both
282
+ claiming margin-left:auto is what stranded the note in the middle. */
283
+ .card__actions { margin-left: auto; display: flex; align-items: center; gap: var(--s2); }
284
+ .card__more {
285
+ margin-left: auto; background: none; border: 0; color: var(--ink-3);
286
+ cursor: pointer; font-size: 18px; line-height: 1; letter-spacing: 2px;
287
+ }
288
+
289
+ /* ── Grids ────────────────────────────────────────────────────────────── */
290
+
291
+ .grid { display: grid; gap: var(--s5); }
292
+ /* Two-column rows pair cards of genuinely unequal length, so they hang from the
293
+ top. The 3- and 4-up rows stay stretched, because a row of counters only
294
+ scans as a row when the boxes match. */
295
+ .g-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); align-items: start; }
296
+ .g-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
297
+ .g-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
298
+ /* The reference layout: two stacked cards on the left, one tall card at right. */
299
+ .g-hero { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1.15fr); }
300
+ .span-2 { grid-column: span 2; }
301
+ .row-2 { grid-row: span 2; }
302
+
303
+ /* ── Numbers ──────────────────────────────────────────────────────────── */
304
+
305
+ .figure { display: grid; gap: 2px; }
306
+ .figure__value {
307
+ font-size: 40px; font-weight: 750; letter-spacing: -0.035em; line-height: 1;
308
+ font-variant-numeric: tabular-nums;
309
+ }
310
+ .figure__value--sm { font-size: 28px; }
311
+ .figure__label { font-size: 12px; color: var(--ink-2); }
312
+ .figure__unit { font-size: 16px; font-weight: 600; color: var(--ink-2); letter-spacing: 0; }
313
+
314
+ /* The reference's ▲/▼ marks: a filled triangle in the meaning colour. */
315
+ .trend { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; }
316
+ .trend__mark { font-size: 10px; line-height: 1; }
317
+ .trend--up .trend__mark { color: var(--lime); }
318
+ .trend--down .trend__mark { color: var(--orange); }
319
+ .trend--flat .trend__mark { color: var(--ink-3); }
320
+ .trend__text { color: var(--ink-2); }
321
+
322
+ .pair { display: flex; gap: var(--s7); flex-wrap: wrap; }
323
+
324
+ /* ── Chips, meters, dots ──────────────────────────────────────────────── */
325
+
326
+ .chip {
327
+ display: inline-flex; align-items: center; gap: 6px;
328
+ padding: 4px 11px; border-radius: var(--r-pill);
329
+ background: var(--surface-3); color: var(--ink-2);
330
+ font-size: 11px; font-weight: 550; white-space: nowrap;
331
+ }
332
+ .chip--lime { background: var(--lime); color: var(--black); }
333
+ .chip--orange { background: var(--orange); color: var(--black); }
334
+ .chip--ghost { background: transparent; border: 1px solid var(--line); }
335
+
336
+ .dot { width: 9px; height: 9px; border-radius: var(--r-pill); flex: none; }
337
+ .dot--lime { background: var(--lime); }
338
+ .dot--orange { background: var(--orange); }
339
+ .dot--white { background: var(--neutral); }
340
+ .dot--ring { background: transparent; box-shadow: inset 0 0 0 2px currentColor; }
341
+
342
+ .meter { height: 8px; border-radius: var(--r-pill); background: var(--track); overflow: hidden; }
343
+ .meter__fill { height: 100%; border-radius: var(--r-pill); background: var(--lime); }
344
+ .meter__fill--orange { background: var(--orange); }
345
+ .meter__fill--white { background: var(--neutral); }
346
+ .meter--thin { height: 5px; }
347
+
348
+ .legend { display: flex; align-items: center; gap: var(--s5); flex-wrap: wrap; font-size: 12px; color: var(--ink-2); }
349
+ .legend__item { display: inline-flex; align-items: center; gap: 7px; }
350
+ .legend__total { margin-left: auto; color: var(--ink-3); }
351
+ .legend__total b { color: var(--ink); font-weight: 650; }
352
+
353
+ /* ── Pillar rows ──────────────────────────────────────────────────────── */
354
+
355
+ .pillars { display: grid; gap: var(--s3); }
356
+ .pillar { display: grid; grid-template-columns: 30px minmax(0, 1fr) auto; gap: var(--s3); align-items: center; }
357
+ .pillar__n { font-size: 14px; font-weight: 700; font-variant-numeric: tabular-nums; }
358
+ .pillar__name { font-size: 12px; color: var(--ink-2); margin-bottom: 5px; }
359
+
360
+ /* ── Buttons ──────────────────────────────────────────────────────────── */
361
+
362
+ .btn {
363
+ display: inline-flex; align-items: center; gap: var(--s2);
364
+ height: 38px; padding: 0 var(--s4);
365
+ border: 0; border-radius: var(--r-pill);
366
+ background: var(--surface-3); color: var(--ink);
367
+ cursor: pointer; font-weight: 550;
368
+ }
369
+ .btn:hover { background: #333; }
370
+ .btn--lime { background: var(--lime); color: var(--black); font-weight: 650; }
371
+ .btn--ghost { background: transparent; box-shadow: inset 0 0 0 1px var(--line); color: var(--ink-2); }
372
+ .btn--sm { height: 30px; padding: 0 var(--s3); font-size: 12px; }
373
+ .btn:disabled { opacity: 0.45; cursor: default; }
374
+
375
+ /* ── Table ────────────────────────────────────────────────────────────── */
376
+
377
+ .table { width: 100%; border-collapse: collapse; }
378
+ .table th {
379
+ text-align: left; padding: 0 var(--s3) var(--s3);
380
+ font-size: 11px; font-weight: 600; letter-spacing: 0.08em;
381
+ text-transform: uppercase; color: var(--ink-3); white-space: nowrap;
382
+ }
383
+ .table td { padding: var(--s3); border-top: 1px solid var(--line); font-size: 13px; }
384
+ .table tbody tr { cursor: pointer; }
385
+ .table tbody tr:hover td { background: var(--surface-3); }
386
+ .table tbody tr:hover td:first-child { border-radius: var(--r-chip) 0 0 var(--r-chip); }
387
+ .table tbody tr:hover td:last-child { border-radius: 0 var(--r-chip) var(--r-chip) 0; }
388
+ .table .r { text-align: right; font-variant-numeric: tabular-nums; }
389
+ .scroll-x { overflow-x: auto; }
390
+ .scroll-y { overflow-y: auto; min-height: 0; }
391
+ /* A sticky header keeps the column names visible while the rows move under it. */
392
+ .scroll-y thead th { position: sticky; top: 0; background: var(--surface); z-index: 1; }
393
+
394
+ /* ── Findings ─────────────────────────────────────────────────────────── */
395
+
396
+ .finding { display: grid; gap: var(--s2); padding: var(--s4) 0; border-top: 1px solid var(--line); }
397
+ .finding:first-child { border-top: 0; padding-top: 0; }
398
+ .finding__head { display: flex; align-items: center; gap: var(--s3); }
399
+ .finding__title { font-weight: 650; font-size: 14px; }
400
+ .finding__evidence { color: var(--ink); font-size: 13px; }
401
+ .finding__fix { color: var(--ink-2); font-size: 12px; display: flex; gap: 7px; }
402
+ .finding__fix::before { content: "→"; color: var(--lime); }
403
+ .finding--muted { opacity: 0.45; }
404
+
405
+ /* ── Charts ───────────────────────────────────────────────────────────── */
406
+
407
+ .chart { display: block; width: 100%; overflow: visible; }
408
+ .chart text { font-family: var(--font); font-size: 11px; fill: var(--ink-3); }
409
+ .chart .grid-line { stroke: var(--line); stroke-dasharray: 3 5; }
410
+ .chart .cap-lime { fill: var(--lime); }
411
+ .chart .cap-orange { fill: var(--orange); }
412
+ .chart .cap-white { fill: var(--neutral); }
413
+ .chart .cap-label { fill: var(--black); font-weight: 700; font-size: 12px; }
414
+ .chart .cap-stem { stroke: var(--line); stroke-width: 1; }
415
+ .chart .series-lime { stroke: var(--lime); fill: none; stroke-width: 2.5; stroke-linecap: round; stroke-linejoin: round; }
416
+ .chart .series-orange { stroke: var(--orange); fill: none; stroke-width: 2.5; stroke-linecap: round; stroke-linejoin: round; }
417
+
418
+ /* ── Unlock screen ────────────────────────────────────────────────────── */
419
+
420
+ .unlock { min-height: calc(100vh - 44px); display: grid; place-items: center; background: var(--black); border-radius: var(--r-shell); }
421
+ .unlock__box { display: grid; gap: var(--s5); justify-items: center; text-align: center; padding: var(--s7); max-width: 380px; }
422
+ .unlock__title { font-size: 30px; font-weight: 800; letter-spacing: -0.02em; text-transform: uppercase; }
423
+ .unlock__note { color: var(--ink-3); font-size: 13px; line-height: 1.6; }
424
+ .unlock__error { color: var(--orange); font-size: 12px; min-height: 16px; }
425
+ .pin {
426
+ height: 52px; width: 200px; text-align: center; letter-spacing: 0.5em;
427
+ border: 0; border-radius: var(--r-pill); background: var(--surface); color: var(--ink);
428
+ font-size: 20px; font-variant-numeric: tabular-nums;
429
+ }
430
+
431
+ /* ── Settings page ────────────────────────────────────────────────────── */
432
+
433
+ .settings { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s4); align-content: start; }
434
+ /* A fixed minimum band, so a one-line row and a two-line row read as the same
435
+ kind of thing and the control sits on the centre line of both. Without it the
436
+ pills drifted relative to the dividers as the label wrapped. */
437
+ .setrow {
438
+ display: flex; align-items: center; gap: var(--s4);
439
+ min-height: 62px; padding: var(--s3) 0;
440
+ border-top: 1px solid var(--line);
441
+ }
442
+ .setrow:first-of-type { border-top: 0; }
443
+ .setrow__label { font-size: 13px; }
444
+ .setrow__help { font-size: 11px; color: var(--ink-3); margin-top: 2px; line-height: 1.5; }
445
+ .setrow__control { margin-left: auto; flex: none; }
446
+
447
+ .optioncard {
448
+ display: flex; align-items: center; gap: var(--s3);
449
+ width: 100%; text-align: left; cursor: pointer;
450
+ padding: var(--s3) var(--s4);
451
+ border: 0; border-radius: var(--r-chip);
452
+ background: var(--surface-2); color: var(--ink);
453
+ }
454
+ .optioncard:hover { background: var(--surface-3); }
455
+ .optioncard[aria-pressed="true"] { background: var(--lime); color: var(--black); }
456
+ .optioncard:disabled { opacity: 0.4; cursor: default; }
457
+ .optioncard__name { font-weight: 650; text-transform: capitalize; }
458
+ .optioncard__desc { font-size: 11px; opacity: 0.75; margin-top: 1px; }
459
+
460
+ /* ── Modal ───────────────────────────────────────────────────────────── */
461
+
462
+ .modal {
463
+ position: fixed; z-index: 70;
464
+ top: 50%; left: 50%; transform: translate(-50%, -50%);
465
+ width: min(400px, calc(100vw - 40px));
466
+ background: var(--surface);
467
+ border-radius: var(--r-card);
468
+ padding: var(--s6);
469
+ display: grid; gap: var(--s4);
470
+ box-shadow: 0 30px 70px rgb(0 0 0 / 0.6);
471
+ }
472
+ .modal__title { font-size: 17px; font-weight: 700; letter-spacing: -0.01em; }
473
+ .modal__note { font-size: 12px; color: var(--ink-3); line-height: 1.6; }
474
+ .modal__input {
475
+ height: 46px; width: 100%;
476
+ padding: 0 var(--s4);
477
+ border: 0; border-radius: var(--r-pill);
478
+ background: var(--surface-2); color: var(--ink);
479
+ font: inherit; font-variant-numeric: tabular-nums;
480
+ }
481
+ .modal__input:focus-visible { outline: 2px solid var(--lime); outline-offset: 2px; }
482
+ .modal__error { font-size: 12px; color: var(--orange); min-height: 15px; }
483
+ .modal__actions { display: flex; gap: var(--s2); justify-content: flex-end; }
484
+
485
+ /* ── Drawer ───────────────────────────────────────────────────────────── */
486
+
487
+ .scrim { position: fixed; inset: 0; background: rgb(0 0 0 / 0.6); z-index: 39; }
488
+ .drawer {
489
+ position: fixed; top: 22px; right: 22px; bottom: 22px;
490
+ width: min(520px, calc(100vw - 44px));
491
+ background: var(--surface); border-radius: var(--r-card);
492
+ z-index: 40; display: flex; flex-direction: column;
493
+ }
494
+ .drawer__head { display: flex; align-items: center; gap: var(--s3); padding: var(--s6) var(--s6) var(--s4); }
495
+ .drawer__title { font-size: 18px; font-weight: 700; letter-spacing: -0.01em; }
496
+ .drawer__body { padding: 0 var(--s6) var(--s6); overflow-y: auto; display: grid; gap: var(--s5); }
497
+
498
+ .kv { display: grid; gap: var(--s3); }
499
+ .kv__row { display: flex; align-items: baseline; gap: var(--s4); }
500
+ .kv__k { font-size: 12px; color: var(--ink-3); min-width: 116px; }
501
+ .kv__v { font-size: 13px; font-variant-numeric: tabular-nums; }
502
+
503
+ .tl { display: grid; gap: 0; }
504
+ .tl__row { display: grid; grid-template-columns: 58px 12px minmax(0, 1fr); gap: var(--s3); align-items: center; padding: 7px 0; border-top: 1px solid var(--line); }
505
+ .tl__row:first-child { border-top: 0; }
506
+ .tl__t { font-size: 11px; color: var(--ink-3); font-variant-numeric: tabular-nums; }
507
+ .tl__x { font-size: 12px; }
508
+ .tl__d { font-size: 12px; color: var(--ink-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
509
+
510
+ /* ── States ───────────────────────────────────────────────────────────── */
511
+
512
+ .empty { padding: var(--s7) var(--s4); text-align: center; color: var(--ink-3); }
513
+ .skel { background: var(--surface); border-radius: var(--r-card); animation: pulse 1.3s ease-in-out infinite; }
514
+ @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
515
+ @media (prefers-reduced-motion: reduce) { .skel { animation: none; } }
516
+
517
+ /* ── Responsive ───────────────────────────────────────────────────────── */
518
+
519
+ @media (max-width: 1240px) {
520
+ .g-hero { grid-template-columns: repeat(2, minmax(0, 1fr)); }
521
+ .g-hero > .row-2 { grid-row: auto; grid-column: span 2; }
522
+ .g-4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
523
+ }
524
+ @media (max-width: 860px) {
525
+ body { padding: 10px; }
526
+ .shell { padding: var(--s5) var(--s4); border-radius: 28px; }
527
+ .body { grid-template-columns: minmax(0, 1fr); }
528
+ .rail { flex-direction: row; overflow-x: auto; }
529
+ .title { font-size: 32px; }
530
+ .g-hero, .g-2, .g-3, .g-4 { grid-template-columns: minmax(0, 1fr); }
531
+ .span-2, .g-hero > .row-2 { grid-column: auto; }
532
+ }