@qball-inc/tokens 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.
package/components.css ADDED
@@ -0,0 +1,848 @@
1
+ /* ============================================================
2
+ ashaykubal.com — App components
3
+ Layered ON TOP of colors_and_type.css. Editorial/marketing
4
+ styles stay in that file; this is the application UI kit
5
+ (data surfaces, overlays, controls) for product screens.
6
+ Append new components here as the gap list is built out.
7
+ ============================================================ */
8
+
9
+ /* ----- Semantic badge / pill -----------------------------------
10
+ Status + finance semantics. Sage/amber map to brand signal +
11
+ highlight; up/down/warn/info map to the finance data palette. */
12
+ .badge {
13
+ display: inline-flex;
14
+ align-items: center;
15
+ gap: 5px;
16
+ font-family: var(--font-ui);
17
+ font-size: 11px;
18
+ font-weight: 500;
19
+ line-height: 1;
20
+ letter-spacing: 0.2px;
21
+ padding: 4px 9px;
22
+ border-radius: var(--radius-sm);
23
+ color: var(--text-secondary);
24
+ background: var(--bg-surface);
25
+ white-space: nowrap;
26
+ }
27
+ .badge--up { color: var(--data-up); background: var(--data-up-bg); }
28
+ .badge--down { color: var(--data-down); background: var(--data-down-bg); }
29
+ .badge--warn { color: var(--data-warn); background: var(--data-warn-bg); }
30
+ .badge--info { color: var(--data-info); background: var(--data-info-bg); }
31
+ .badge--signal { color: var(--color-signal); background: var(--signal-bg); }
32
+ .badge--highlight{ color: var(--color-highlight); background: var(--highlight-bg); }
33
+ /* Semantic status (success/error) + finance flat. Mirror the
34
+ toast--success/error mapping: success = gain green, error = loss red; flat is
35
+ neutral stone on the base surface. Color is paired with a non-color cue in the
36
+ Badge component for the finance variants (FR4). */
37
+ .badge--success { color: var(--data-up); background: var(--data-up-bg); }
38
+ .badge--error { color: var(--data-down); background: var(--data-down-bg); }
39
+ .badge--flat { color: var(--data-flat); }
40
+ .badge__dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
41
+
42
+ /* ----- Data table ----------------------------------------------
43
+ Denser than the editorial layout: compact rows, hairline
44
+ dividers, Berkeley Mono tabular numerics, row-hover actions. */
45
+ .dt { width: 100%; border-collapse: collapse; font-family: var(--font-ui); }
46
+ .dt thead th {
47
+ font-size: 12.5px;
48
+ letter-spacing: 1px;
49
+ text-transform: uppercase;
50
+ color: var(--text-secondary);
51
+ font-weight: 700;
52
+ text-align: left;
53
+ padding: 11px 18px;
54
+ border-bottom: 1px solid var(--border-strong);
55
+ white-space: nowrap;
56
+ vertical-align: middle;
57
+ }
58
+ .dt thead th.num, .dt td.num { text-align: right; }
59
+ .dt tbody td {
60
+ padding: 15px 18px;
61
+ border-bottom: 0.5px solid var(--border-default);
62
+ font-size: 14.5px;
63
+ color: var(--text-secondary);
64
+ vertical-align: middle;
65
+ }
66
+ .dt tbody tr { transition: background var(--dur-fast) var(--ease-out); }
67
+ .dt tbody tr:hover { background: var(--bg-surface); }
68
+ .dt tbody tr:last-child td { border-bottom: 0; }
69
+ .dt .num {
70
+ font-family: var(--font-display);
71
+ font-variant-numeric: tabular-nums;
72
+ font-feature-settings: "tnum" 1;
73
+ color: var(--text-primary);
74
+ white-space: nowrap;
75
+ }
76
+ .dt .sym {
77
+ font-family: var(--font-display);
78
+ font-weight: 600;
79
+ color: var(--text-primary);
80
+ font-size: 16px;
81
+ letter-spacing: 0;
82
+ }
83
+ .dt .co {
84
+ display: block;
85
+ font-family: var(--font-body);
86
+ font-size: 12.5px;
87
+ color: var(--text-muted);
88
+ margin-top: 2px;
89
+ letter-spacing: 0;
90
+ }
91
+ .dt .up { color: var(--data-up); }
92
+ .dt .down { color: var(--data-down); }
93
+ .dt .flat { color: var(--data-flat); }
94
+ .dt__spark { display: block; }
95
+ .dt__remove {
96
+ width: 26px; height: 26px;
97
+ display: inline-grid; place-items: center;
98
+ border-radius: var(--radius-sm);
99
+ border: 0; background: transparent;
100
+ color: var(--text-muted);
101
+ cursor: pointer;
102
+ opacity: 0;
103
+ transition: opacity var(--dur-fast) var(--ease-out),
104
+ color var(--dur-base) var(--ease-out),
105
+ background var(--dur-base) var(--ease-out);
106
+ }
107
+ .dt tbody tr:hover .dt__remove { opacity: 1; }
108
+ .dt__remove:hover { color: var(--data-down); background: var(--data-down-bg); }
109
+
110
+ /* Selected row (owner decision): sage-tinted selection — the
111
+ system's active accent, matching .card--interactive and the segmented
112
+ aria-pressed cue. Background-only keeps it DESIGN-conformant (no box-shadow,
113
+ no border-induced layout shift) and overrides the neutral :hover tint so a
114
+ selected row stays sage even on hover. */
115
+ .dt tbody tr[aria-selected="true"],
116
+ .dt tbody tr[aria-selected="true"]:hover { background: var(--signal-bg); }
117
+
118
+ /* Always-visible row actions: opt-in modifier that pins the
119
+ hover-reveal .dt__remove visible. Default remains hover-reveal (the oracle). */
120
+ .dt--actions-visible .dt__remove { opacity: 1; }
121
+
122
+ /* Mobile card-list reflow: at narrow widths the table reflows into
123
+ stacked cards — thead hidden, each row a card, each cell a label-above-value
124
+ line via td[data-label]::before (the component sets data-label from the column
125
+ header / meta.label, since CSS cannot read thead text). Keyed to the .dt-wrap
126
+ host's CONTAINER width (container-type: inline-size) so it reflows when the
127
+ table sits in a narrow column, not only on a narrow viewport. Purely additive:
128
+ the desktop .dt surface above is unchanged and >640px renders identically. */
129
+ .dt-wrap { container-type: inline-size; }
130
+
131
+ @container (max-width: 640px) {
132
+ .dt thead { display: none; }
133
+ .dt tbody tr {
134
+ display: block;
135
+ padding: 12px 18px;
136
+ border-bottom: 1px solid var(--border-strong);
137
+ }
138
+ .dt tbody tr:last-child { border-bottom: 0; }
139
+ /* Each cell becomes its own block line; numerics drop the right-align so the
140
+ label+value reads left-aligned. Finance .up/.down/.flat color is inherited
141
+ (no override) and the [aria-selected] sage tint still tints the whole card. */
142
+ .dt tbody td,
143
+ .dt tbody td.num { display: block; text-align: left; padding: 4px 0; border-bottom: 0; }
144
+ .dt tbody td[data-label]::before {
145
+ content: attr(data-label);
146
+ display: block;
147
+ margin-bottom: 2px;
148
+ font-size: 12.5px;
149
+ letter-spacing: 1px;
150
+ text-transform: uppercase;
151
+ font-weight: 700;
152
+ color: var(--text-secondary);
153
+ }
154
+ /* No hover on touch: pin the row actions visible AND enlarge to a comfortable
155
+ tap target (the desktop 26px hover-reveal control is too small for touch). */
156
+ .dt tbody .dt__remove { opacity: 1; width: 44px; height: 44px; }
157
+ }
158
+
159
+ /* ----- Form field wrapper --------------------------------------
160
+ Label + control + helper + error. Drives 400/422 validation. */
161
+ .field { display: flex; flex-direction: column; gap: 6px; max-width: 360px; font-family: var(--font-ui); }
162
+ .field__label {
163
+ font-size: 11px; font-weight: 500; letter-spacing: 0.5px;
164
+ color: var(--text-secondary); display: inline-flex; gap: 5px; align-items: center;
165
+ }
166
+ .field__req { color: var(--data-down); }
167
+ .field__help { font-size: 11px; line-height: 1.4; color: var(--text-muted); }
168
+ .field__error {
169
+ font-size: 11px; line-height: 1.4; color: var(--data-down);
170
+ display: inline-flex; gap: 5px; align-items: center;
171
+ }
172
+
173
+ /* ----- Text input ----------------------------------------------
174
+ Follows the brand's border-only focus (no elevation ring). */
175
+ .input {
176
+ font-family: var(--font-ui); font-size: 13px;
177
+ padding: 9px 12px;
178
+ border: 1px solid var(--border-strong);
179
+ background: var(--bg-primary); color: var(--text-primary);
180
+ border-radius: var(--radius-sm);
181
+ outline: none; width: 100%; box-sizing: border-box;
182
+ transition: border-color var(--dur-base) var(--ease-out),
183
+ background var(--dur-base) var(--ease-out);
184
+ }
185
+ .input::placeholder { color: var(--text-muted); }
186
+ .input:focus { border-color: var(--color-signal); }
187
+ .input:disabled { opacity: 0.5; cursor: not-allowed; background: var(--bg-surface); }
188
+ .input--error { border-color: var(--data-down); }
189
+ .input.num { font-family: var(--font-display); font-variant-numeric: tabular-nums; }
190
+
191
+ /* affordance wrapper: trailing icon / button inside the field */
192
+ .input-wrap { position: relative; }
193
+ .input-wrap .input { padding-right: 38px; }
194
+ .input-wrap__affix {
195
+ position: absolute; top: 50%; right: 4px; transform: translateY(-50%);
196
+ width: 28px; height: 28px; display: inline-grid; place-items: center;
197
+ border: 0; background: transparent; color: var(--text-muted);
198
+ border-radius: var(--radius-sm); cursor: pointer;
199
+ transition: color var(--dur-base) var(--ease-out), background var(--dur-base) var(--ease-out);
200
+ }
201
+ .input-wrap__affix:hover { color: var(--color-signal); background: var(--signal-bg); }
202
+
203
+ /* ----- Select / dropdown ---------------------------------------
204
+ Native control, chevron via inline SVG (Stone #78716C reads on
205
+ both themes). Custom open panel mocked in the preview. */
206
+ .select {
207
+ font-family: var(--font-ui); font-size: 13px;
208
+ padding: 9px 34px 9px 12px;
209
+ border: 1px solid var(--border-strong);
210
+ background-color: var(--bg-primary); color: var(--text-primary);
211
+ border-radius: var(--radius-sm); outline: none; cursor: pointer;
212
+ width: 100%; box-sizing: border-box; appearance: none;
213
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%2378716C' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
214
+ background-repeat: no-repeat; background-position: right 10px center;
215
+ transition: border-color var(--dur-base) var(--ease-out);
216
+ }
217
+ .select:focus { border-color: var(--color-signal); }
218
+ .select:disabled { opacity: 0.5; cursor: not-allowed; background-color: var(--bg-surface); }
219
+ .menu {
220
+ font-family: var(--font-ui); font-size: 13px;
221
+ border: 1px solid var(--border-strong);
222
+ background: var(--bg-primary); border-radius: var(--radius-sm);
223
+ padding: 4px; min-width: 180px; box-sizing: border-box;
224
+ }
225
+ .menu__item {
226
+ display: flex; align-items: center; justify-content: space-between; gap: 10px;
227
+ padding: 8px 10px; border-radius: var(--radius-sm);
228
+ color: var(--text-secondary); cursor: pointer;
229
+ }
230
+ .menu__item:hover { background: var(--bg-surface); color: var(--color-signal); }
231
+ .menu__item[aria-selected="true"] { color: var(--color-signal); background: var(--signal-bg); font-weight: 500; }
232
+ .menu__check { color: var(--color-signal); }
233
+
234
+ /* ----- Toggle / switch -----------------------------------------
235
+ Default is the brand-native rounded-rect switch (4px). A
236
+ segmented control covers 2-3 options; a pill switch is offered
237
+ as a documented exception only. */
238
+ .switch { display: inline-flex; align-items: center; gap: 10px; cursor: pointer; font-family: var(--font-ui); font-size: 13px; color: var(--text-secondary); }
239
+ .switch input { position: absolute; opacity: 0; width: 0; height: 0; }
240
+ .switch__ui {
241
+ width: 44px; height: 24px; flex: none;
242
+ border-radius: var(--radius-sm);
243
+ background: var(--bg-surface); border: 1px solid var(--border-strong);
244
+ position: relative;
245
+ transition: background var(--dur-base) var(--ease-out), border-color var(--dur-base) var(--ease-out);
246
+ }
247
+ .switch__ui::after {
248
+ content: ""; position: absolute; top: 2px; left: 2px;
249
+ width: 18px; height: 18px; border-radius: 3px;
250
+ background: var(--text-muted);
251
+ transition: transform var(--dur-base) var(--ease-out), background var(--dur-base) var(--ease-out);
252
+ }
253
+ .switch input:checked + .switch__ui { background: var(--signal-bg); border-color: var(--color-signal); }
254
+ .switch input:checked + .switch__ui::after { transform: translateX(20px); background: var(--color-signal); }
255
+ .switch input:focus-visible + .switch__ui { border-color: var(--color-signal); }
256
+ .switch input:disabled + .switch__ui { opacity: 0.4; }
257
+
258
+ .segmented { display: inline-grid; grid-auto-flow: column; grid-auto-columns: 1fr; border: 1px solid var(--border-strong); border-radius: var(--radius-sm); overflow: hidden; font-family: var(--font-ui); }
259
+ .segmented button {
260
+ font-size: 12px; padding: 7px 16px; border: 0; text-align: center;
261
+ border-right: 1px solid var(--border-default);
262
+ background: transparent; color: var(--text-muted); cursor: pointer;
263
+ transition: background var(--dur-base) var(--ease-out), color var(--dur-base) var(--ease-out);
264
+ }
265
+ .segmented button:last-child { border-right: 0; }
266
+ .segmented button:hover { color: var(--text-secondary); }
267
+ .segmented button[aria-pressed="true"] { background: var(--signal-bg); color: var(--color-signal); }
268
+
269
+ /* ----- Scrim / overlay backdrop --------------------------------
270
+ The dim veil behind a modal/dialog — the brand's no-shadow lift
271
+ partner. Full-viewport fixed layer; no blur (the preview oracle
272
+ dims with a flat tone). Reused by the Scrim overlay component. */
273
+ .scrim {
274
+ position: fixed;
275
+ inset: 0;
276
+ background: var(--color-scrim);
277
+ }
278
+
279
+ /* ----- Modal / dialog ------------------------------------------
280
+ No shadow. Lifted off the page by a heavier border + dim scrim
281
+ (the brand's stated alternative to elevation). 12px radius is
282
+ reserved for exactly this kind of surface. */
283
+ .modal {
284
+ background: var(--bg-primary);
285
+ border: 1px solid var(--border-strong);
286
+ border-radius: var(--radius-lg);
287
+ width: 100%; box-sizing: border-box;
288
+ font-family: var(--font-ui);
289
+ }
290
+ .modal__head { padding: 18px 20px 0; display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
291
+ .modal__title { font-family: var(--font-heading); font-size: 16px; font-weight: 600; color: var(--text-primary); margin: 0; }
292
+ .modal__sub { font-size: 12px; color: var(--text-muted); margin: 4px 0 0; line-height: 1.45; }
293
+ .modal__x {
294
+ flex: none; width: 30px; height: 30px; margin: -4px -6px 0 0;
295
+ display: inline-grid; place-items: center; border: 0; background: transparent;
296
+ color: var(--text-muted); border-radius: var(--radius-sm); cursor: pointer;
297
+ transition: color var(--dur-base) var(--ease-out), background var(--dur-base) var(--ease-out);
298
+ }
299
+ .modal__x:hover { color: var(--color-signal); background: var(--bg-surface); }
300
+ .modal__body { padding: 16px 20px; display: flex; flex-direction: column; gap: 14px; }
301
+ .modal__foot { padding: 14px 20px 18px; display: flex; justify-content: flex-end; gap: 8px; border-top: 0.5px solid var(--border-default); }
302
+
303
+ /* ----- Toast / transient notification --------------------------
304
+ Semantic left-accent (the brand's accent-edge treatment: 0
305
+ radius on the accented edge). Stacks vertically. */
306
+ .toast {
307
+ display: flex; gap: 10px; align-items: flex-start;
308
+ background: var(--bg-primary);
309
+ border: 0.5px solid var(--border-default);
310
+ border-left: 3px solid var(--data-info);
311
+ border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
312
+ padding: 12px 14px; min-width: 300px; max-width: 360px; box-sizing: border-box;
313
+ font-family: var(--font-ui);
314
+ }
315
+ .toast--success { border-left-color: var(--data-up); }
316
+ .toast--warning { border-left-color: var(--data-warn); }
317
+ .toast--error { border-left-color: var(--data-down); }
318
+ .toast--info { border-left-color: var(--data-info); }
319
+ .toast__icon { flex: none; margin-top: 1px; color: var(--data-info); }
320
+ .toast--success .toast__icon { color: var(--data-up); }
321
+ .toast--warning .toast__icon { color: var(--data-warn); }
322
+ .toast--error .toast__icon { color: var(--data-down); }
323
+ .toast__body { flex: 1; }
324
+ .toast__title { font-family: var(--font-ui); font-size: 12.5px; font-weight: 600; color: var(--text-primary); margin: 0 0 2px; }
325
+ .toast__msg { font-size: 12px; color: var(--text-secondary); line-height: 1.45; margin: 0; }
326
+ .toast__x {
327
+ flex: none; width: 22px; height: 22px; margin: -2px -4px 0 0;
328
+ display: inline-grid; place-items: center; border: 0; background: transparent;
329
+ color: var(--text-muted); border-radius: var(--radius-sm); cursor: pointer;
330
+ }
331
+ .toast__x:hover { color: var(--text-primary); }
332
+
333
+ /* ----- Banner / inline callout ---------------------------------
334
+ Tinted semantic surface + accent edge. Hosts the v1 disclaimer
335
+ (persistent) and degraded-data / outage messages (dismissible). */
336
+ .callout {
337
+ display: flex; gap: 12px; align-items: flex-start;
338
+ padding: 13px 16px;
339
+ border-left: 3px solid var(--data-info);
340
+ background: var(--data-info-bg);
341
+ border-radius: 0 var(--radius-md) var(--radius-md) 0;
342
+ font-family: var(--font-ui);
343
+ }
344
+ .callout--warn { border-left-color: var(--data-warn); background: var(--data-warn-bg); }
345
+ .callout--error { border-left-color: var(--data-down); background: var(--data-down-bg); }
346
+ .callout--neutral { border-left-color: var(--text-muted); background: var(--bg-surface); }
347
+ .callout__icon { flex: none; margin-top: 1px; color: var(--data-info); }
348
+ .callout--warn .callout__icon { color: var(--data-warn); }
349
+ .callout--error .callout__icon { color: var(--data-down); }
350
+ .callout--neutral .callout__icon { color: var(--text-muted); }
351
+ .callout__body { flex: 1; }
352
+ .callout__title { font-size: 12.5px; font-weight: 600; color: var(--text-primary); margin: 0 0 2px; }
353
+ .callout__msg { font-size: 12px; color: var(--text-secondary); line-height: 1.5; margin: 0; }
354
+ .callout__msg a { color: var(--color-signal); }
355
+ .callout__x {
356
+ flex: none; width: 22px; height: 22px; margin: -2px -4px 0 0;
357
+ display: inline-grid; place-items: center; border: 0; background: transparent;
358
+ color: var(--text-muted); border-radius: var(--radius-sm); cursor: pointer;
359
+ }
360
+ .callout__x:hover { color: var(--text-primary); }
361
+
362
+ /* ----- Spinner -------------------------------------------------
363
+ The one sanctioned loop. Use only for true indeterminate waits;
364
+ prefer skeletons for content. currentColor so it inherits. */
365
+ .spinner {
366
+ display: inline-block; width: 18px; height: 18px;
367
+ border: 1.5px solid var(--border-strong);
368
+ border-top-color: var(--color-signal);
369
+ border-radius: 50%;
370
+ animation: spin 0.7s linear infinite;
371
+ }
372
+ .spinner--sm { width: 13px; height: 13px; }
373
+ .spinner--lg { width: 28px; height: 28px; border-width: 2px; }
374
+ @keyframes spin { to { transform: rotate(360deg); } }
375
+ /* Reduced-motion: stop the loop entirely (static ring, no spin) — NFR5.
376
+ Mirrors the .skel reduced-motion fallback below. */
377
+ @media (prefers-reduced-motion: reduce) {
378
+ .spinner { animation: none; }
379
+ }
380
+ .load-inline { display: inline-flex; align-items: center; gap: 9px;
381
+ font-family: var(--font-ui); font-size: 12px; color: var(--text-muted); }
382
+
383
+ /* ----- Skeleton ------------------------------------------------
384
+ Preferred over spinners for content. Subtle shimmer left→right;
385
+ honors reduced-motion (falls back to a static tint). */
386
+ .skel {
387
+ display: block; border-radius: var(--radius-sm);
388
+ background: linear-gradient(90deg,
389
+ var(--bg-surface) 0%, var(--border-default) 50%, var(--bg-surface) 100%);
390
+ background-size: 200% 100%;
391
+ animation: skel-shimmer 1.3s ease-in-out infinite;
392
+ }
393
+ .skel--text { height: 11px; margin: 4px 0; }
394
+ .skel--line { height: 13px; }
395
+ .skel--title { height: 18px; }
396
+ .skel--block { height: 100%; }
397
+ .skel--circle { border-radius: 50%; }
398
+ @keyframes skel-shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
399
+ @media (prefers-reduced-motion: reduce) {
400
+ .skel { animation: none; background: var(--bg-surface); }
401
+ }
402
+
403
+ /* ----- Empty / error / status states ---------------------------
404
+ Centered figure: icon chip, headline, supporting line, action.
405
+ --error reserves the red for genuine failures; --empty stays
406
+ neutral (an empty watchlist is not an error). */
407
+ .state-fig {
408
+ display: flex; flex-direction: column; align-items: center; text-align: center;
409
+ gap: 12px; padding: 32px 24px; font-family: var(--font-ui); max-width: 360px; margin: 0 auto;
410
+ }
411
+ .state-fig__icon {
412
+ width: 44px; height: 44px; border-radius: var(--radius-md);
413
+ display: grid; place-items: center;
414
+ background: var(--bg-surface); color: var(--text-muted);
415
+ border: 0.5px solid var(--border-default);
416
+ }
417
+ .state-fig--error .state-fig__icon { background: var(--data-down-bg); color: var(--data-down); border-color: transparent; }
418
+ .state-fig--warn .state-fig__icon { background: var(--data-warn-bg); color: var(--data-warn); border-color: transparent; }
419
+ .state-fig__title { font-family: var(--font-heading); font-size: 15px; font-weight: 600; color: var(--text-primary); margin: 0; }
420
+ .state-fig__msg { font-size: 12px; line-height: 1.55; color: var(--text-muted); margin: 0; }
421
+ .state-fig__actions { display: flex; gap: 8px; margin-top: 4px; }
422
+
423
+ /* ----- Grounding guardrail -------------------------------------
424
+ Two inline annotations for AI answers, on the brand's two
425
+ accents (no new hues): sage = cited/sourced, amber = unverified.
426
+ Both share a hover tooltip (.gtip). Each conversation direction
427
+ styles its own trigger glyph; these are the canonical chips. */
428
+ .gwrap { position: relative; display: inline-flex; vertical-align: middle; cursor: help; }
429
+ .gtip {
430
+ position: absolute; left: 50%; bottom: calc(100% + 8px); transform: translateX(-50%);
431
+ width: max-content; max-width: 232px; text-align: left;
432
+ background: var(--bg-primary); border: 1px solid var(--border-strong);
433
+ border-radius: var(--radius-sm); padding: 9px 11px;
434
+ font-family: var(--font-ui); font-size: 11px; line-height: 1.5; color: var(--text-secondary);
435
+ opacity: 0; pointer-events: none; transition: opacity var(--dur-fast) var(--ease-out);
436
+ z-index: 30;
437
+ }
438
+ .gwrap:hover .gtip, .gwrap:focus-within .gtip { opacity: 1; }
439
+ .gtip--below { top: calc(100% + 8px); bottom: auto; }
440
+ /* edge-aware alignment so the bubble never clips against a panel wall */
441
+ .gtip--alignL { left: 0; right: auto; transform: none; }
442
+ .gtip--alignR { left: auto; right: 0; transform: none; }
443
+ .gtip__k { display: block; font-size: 9.5px; letter-spacing: 1px; text-transform: uppercase;
444
+ font-weight: 600; margin-bottom: 4px; }
445
+ .gtip__src { color: var(--anno-source); font-family: var(--font-display); }
446
+ .gtip--cited .gtip__k { color: var(--anno-source); }
447
+ .gtip--unverified .gtip__k { color: var(--color-highlight); }
448
+
449
+ /* canonical inline chips */
450
+ .ground {
451
+ display: inline-flex; align-items: center; gap: 3px;
452
+ font-family: var(--font-ui); font-size: 10px; line-height: 1;
453
+ padding: 2px 7px; border-radius: 999px;
454
+ }
455
+ .ground--cited { color: var(--anno-source); background: var(--anno-source-bg); }
456
+ .ground--unverified{ color: var(--color-highlight); background: var(--highlight-bg); }
457
+
458
+ /* Wave-shimmer trigger ------------------------------------------
459
+ A bright band sweeps left→right across the letters + brackets so
460
+ source / unverified flags pull the eye instead of blending into
461
+ the answer. Gradient is clipped to the glyphs; --wave-base adapts
462
+ per theme via the annotation tokens. Honors reduced-motion. */
463
+ .ground-wave {
464
+ font-weight: 600;
465
+ background-image: linear-gradient(100deg,
466
+ var(--wave-base) 0%, var(--wave-base) 38%,
467
+ var(--wave-hi) 50%,
468
+ var(--wave-base) 62%, var(--wave-base) 100%);
469
+ background-size: 220% 100%;
470
+ -webkit-background-clip: text; background-clip: text;
471
+ -webkit-text-fill-color: transparent; color: transparent;
472
+ animation: ground-wave 2.6s linear infinite;
473
+ }
474
+ .ground-wave--source { --wave-base: var(--anno-source); --wave-hi: #BFE0F2; }
475
+ .ground-wave--unverified { --wave-base: var(--color-highlight); --wave-hi: #F0C07A; }
476
+ @keyframes ground-wave { from { background-position: 120% 0; } to { background-position: -120% 0; } }
477
+ @media (prefers-reduced-motion: reduce) {
478
+ .ground-wave { animation: none; -webkit-text-fill-color: var(--wave-base); color: var(--wave-base); }
479
+ }
480
+
481
+ /* ----- App shell / top nav -------------------------------------
482
+ Signed-in chrome. Hairline-bottom bar, brand mark left, primary
483
+ nav, then the right cluster: search, notification bell, and the
484
+ user menu. The theme switcher is a GLOBAL control and lives in
485
+ the user menu — never on individual surfaces. */
486
+ .appbar {
487
+ display: flex; align-items: center; gap: 18px;
488
+ padding: 0 20px; height: 56px;
489
+ border-bottom: 0.5px solid var(--border-default);
490
+ background: var(--bg-primary); font-family: var(--font-ui);
491
+ }
492
+ .appbar__brand { display: flex; align-items: center; gap: 9px; font-family: var(--font-heading);
493
+ font-size: 15px; font-weight: 700; color: var(--text-primary); letter-spacing: -0.3px; white-space: nowrap; }
494
+ .appbar__mark {
495
+ width: 26px; height: 26px; border-radius: var(--radius-sm); flex: none;
496
+ display: grid; place-items: center;
497
+ background: var(--text-primary); color: var(--bg-primary);
498
+ font-family: var(--font-display); font-weight: 700; font-size: 13px;
499
+ }
500
+ .appnav { display: flex; align-items: center; gap: 2px; }
501
+ .appnav a {
502
+ font-size: 12.5px; color: var(--text-muted); text-decoration: none;
503
+ padding: 7px 11px; border-radius: var(--radius-sm);
504
+ transition: color var(--dur-base) var(--ease-out), background var(--dur-base) var(--ease-out);
505
+ }
506
+ .appnav a:hover { color: var(--text-secondary); background: var(--bg-surface); }
507
+ .appnav a[aria-current="page"] { color: var(--color-signal); background: var(--signal-bg); }
508
+ .appbar__spacer { flex: 1; }
509
+ .appbar__right { display: flex; align-items: center; gap: 8px; }
510
+
511
+ /* icon button used in the bar (search, bell) */
512
+ .iconbtn {
513
+ position: relative; width: 34px; height: 34px; flex: none;
514
+ display: inline-grid; place-items: center;
515
+ border: 0; background: transparent; color: var(--text-muted);
516
+ border-radius: var(--radius-sm); cursor: pointer;
517
+ transition: color var(--dur-base) var(--ease-out), background var(--dur-base) var(--ease-out);
518
+ }
519
+ .iconbtn:hover { color: var(--color-signal); background: var(--bg-surface); }
520
+ .iconbtn[aria-expanded="true"] { color: var(--color-signal); background: var(--signal-bg); }
521
+
522
+ /* unread badge on the bell */
523
+ .badge-count {
524
+ position: absolute; top: 4px; right: 4px; min-width: 15px; height: 15px;
525
+ padding: 0 4px; box-sizing: border-box;
526
+ display: grid; place-items: center;
527
+ font-family: var(--font-display); font-size: 9px; font-weight: 600; line-height: 1;
528
+ color: var(--bg-primary); background: var(--data-down);
529
+ border: 1.5px solid var(--bg-primary); border-radius: 999px;
530
+ }
531
+
532
+ /* user menu trigger */
533
+ .usermenu { display: flex; align-items: center; gap: 7px; padding: 4px 8px 4px 4px;
534
+ border: 0; background: transparent; cursor: pointer; border-radius: var(--radius-md);
535
+ transition: background var(--dur-base) var(--ease-out); }
536
+ .usermenu:hover, .usermenu[aria-expanded="true"] { background: var(--bg-surface); }
537
+ .usermenu svg { color: var(--text-muted); }
538
+
539
+ /* avatar (image or initials) */
540
+ .avatar {
541
+ width: 28px; height: 28px; flex: none; border-radius: var(--radius-sm);
542
+ display: grid; place-items: center; overflow: hidden;
543
+ background: var(--signal-bg); color: var(--color-signal);
544
+ font-family: var(--font-display); font-size: 11px; font-weight: 700;
545
+ }
546
+ .avatar img { width: 100%; height: 100%; object-fit: cover; }
547
+ .avatar--lg { width: 38px; height: 38px; font-size: 14px; }
548
+ .avatar--round { border-radius: 50%; }
549
+
550
+ /* dropdown panel shared by user menu + notification center */
551
+ .dropdown {
552
+ background: var(--bg-primary); border: 1px solid var(--border-strong);
553
+ border-radius: var(--radius-md); font-family: var(--font-ui);
554
+ width: 232px; box-sizing: border-box; overflow: hidden;
555
+ }
556
+ .dropdown__head { padding: 12px 14px; border-bottom: 0.5px solid var(--border-default);
557
+ display: flex; align-items: center; gap: 10px; }
558
+ .dropdown__name { font-size: 12.5px; font-weight: 600; color: var(--text-primary); }
559
+ .dropdown__mail { font-family: var(--font-display); font-size: 10.5px; color: var(--text-muted); }
560
+ .dropdown__sec { padding: 6px; }
561
+ .dropdown__row {
562
+ display: flex; align-items: center; gap: 10px; width: 100%;
563
+ padding: 8px 10px; border: 0; background: transparent; cursor: pointer;
564
+ border-radius: var(--radius-sm); font-family: var(--font-ui); font-size: 12.5px;
565
+ color: var(--text-secondary); text-align: left;
566
+ }
567
+ .dropdown__row:hover { background: var(--bg-surface); color: var(--text-primary); }
568
+ .dropdown__row svg { color: var(--text-muted); flex: none; }
569
+ .dropdown__row--danger { color: var(--data-down); }
570
+ .dropdown__row--danger svg { color: var(--data-down); }
571
+ .dropdown__rule { height: 0; border: 0; border-top: 0.5px solid var(--border-default); margin: 0; }
572
+ /* the theme switcher row: label + segmented sits inline */
573
+ .dropdown__theme { display: flex; align-items: center; justify-content: space-between;
574
+ padding: 8px 10px; font-size: 12.5px; color: var(--text-secondary); }
575
+
576
+ /* ----- Notification center -------------------------------------
577
+ SSE-driven dropdown. Wider than the user menu; unread rows carry
578
+ a sage dot + tinted surface; semantics use the finance/status
579
+ colors via a small left mark. */
580
+ .notif { width: 320px; }
581
+ .notif__head { display: flex; align-items: center; justify-content: space-between;
582
+ padding: 12px 14px; border-bottom: 0.5px solid var(--border-default); }
583
+ .notif__title { font-family: var(--font-heading); font-size: 13px; font-weight: 600; color: var(--text-primary); }
584
+ .notif__clear { font-family: var(--font-ui); font-size: 11px; color: var(--color-signal);
585
+ background: transparent; border: 0; cursor: pointer; }
586
+ .notif__list { max-height: 320px; overflow-y: auto; }
587
+ .notif__item {
588
+ display: grid; grid-template-columns: 26px 1fr; gap: 10px;
589
+ padding: 11px 14px; border-bottom: 0.5px solid var(--border-default);
590
+ cursor: pointer; transition: background var(--dur-fast) var(--ease-out);
591
+ }
592
+ .notif__item:last-child { border-bottom: 0; }
593
+ .notif__item:hover { background: var(--bg-surface); }
594
+ .notif__item--unread { background: var(--signal-bg); }
595
+ .notif__item--unread:hover { background: color-mix(in srgb, var(--signal-bg) 70%, var(--bg-surface)); }
596
+ .notif__mark {
597
+ width: 26px; height: 26px; border-radius: var(--radius-sm); flex: none;
598
+ display: grid; place-items: center; background: var(--bg-surface); color: var(--text-muted);
599
+ }
600
+ .notif__mark--up { background: var(--data-up-bg); color: var(--data-up); }
601
+ .notif__mark--down { background: var(--data-down-bg); color: var(--data-down); }
602
+ .notif__mark--warn { background: var(--data-warn-bg); color: var(--data-warn); }
603
+ .notif__mark--info { background: var(--data-info-bg); color: var(--data-info); }
604
+ .notif__txt { font-size: 12px; line-height: 1.45; color: var(--text-secondary); }
605
+ .notif__txt b { color: var(--text-primary); font-weight: 600; }
606
+ .notif__num { font-family: var(--font-display); font-variant-numeric: tabular-nums; color: var(--text-primary); }
607
+ .notif__time { font-family: var(--font-display); font-size: 10px; color: var(--text-muted); margin-top: 3px; display: block; }
608
+ .notif__foot { padding: 10px 14px; text-align: center; border-top: 0.5px solid var(--border-default); }
609
+ .notif__foot a { font-family: var(--font-ui); font-size: 11.5px; color: var(--color-signal); text-decoration: none; }
610
+
611
+ /* ----- Stocky — CRT-bot brand mascot icon ----------------------
612
+ A little cathode-ray TV with an antenna and two cute eyes. The
613
+ screen "simmers": a scanline sweeps and the eyes blink. Body +
614
+ antenna use currentColor (theme text); the screen is always a
615
+ dark CRT tone with sage phosphor so it reads in both themes. */
616
+ .stocky-icon { display: inline-grid; place-items: center; line-height: 0; color: var(--text-secondary); }
617
+ .stocky-icon svg { width: 100%; height: 100%; display: block; overflow: visible; }
618
+ .stocky-screen { fill: #161410; }
619
+ .stocky-eye { fill: var(--color-signal); transform-box: fill-box; transform-origin: center;
620
+ animation: stocky-blink 4.6s infinite both; }
621
+ .stocky-eye--r { animation-delay: 0.04s; }
622
+ .stocky-scan { fill: var(--color-signal); fill-opacity: 0.55; transform-box: fill-box;
623
+ animation: stocky-scan 2.6s linear infinite; }
624
+ @keyframes stocky-blink { 0%, 90%, 100% { transform: scaleY(1); } 94% { transform: scaleY(0.12); } }
625
+ @keyframes stocky-scan {
626
+ 0% { transform: translateY(0); opacity: 0; }
627
+ 14% { opacity: 0.6; } 86% { opacity: 0.6; }
628
+ 100% { transform: translateY(11px); opacity: 0; }
629
+ }
630
+ @media (prefers-reduced-motion: reduce) {
631
+ .stocky-eye, .stocky-scan { animation: none; }
632
+ .stocky-scan { opacity: 0.4; }
633
+ }
634
+ /* the icon-button variant pulses a faint sage glow to invite a tap */
635
+ .iconbtn--stocky:hover .stocky-icon { color: var(--color-signal); }
636
+
637
+ /* ----- Single-click theme toggle (sun ⇄ moon) ------------------ */
638
+ .theme-toggle .ic-moon { display: none; }
639
+ html[data-theme="dark"] .theme-toggle .ic-sun { display: none; }
640
+ html[data-theme="dark"] .theme-toggle .ic-moon { display: inline; }
641
+
642
+ /* ----- Command island / dock -----------------------------------
643
+ A floating action cluster (search · ask Stocky · add) anchored
644
+ bottom-center. Squared (radius-lg, never a pill). Each action
645
+ opens a modal that pops ABOVE the dock. */
646
+ .dock {
647
+ position: absolute; left: 50%; bottom: 24px; transform: translateX(-50%);
648
+ display: flex; align-items: center; gap: 8px; padding: 10px;
649
+ background: color-mix(in srgb, var(--bg-primary) 70%, transparent);
650
+ -webkit-backdrop-filter: blur(16px) saturate(1.3);
651
+ backdrop-filter: blur(16px) saturate(1.3);
652
+ border: 1px solid var(--border-strong);
653
+ border-radius: var(--radius-lg); z-index: 15;
654
+ transition: transform 0.34s var(--ease-out), opacity 0.28s var(--ease-out);
655
+ }
656
+ .dock__div { width: 0.5px; align-self: stretch; background: var(--border-default); margin: 7px 3px; }
657
+ .dock .btn--icon { width: 52px; height: 52px; }
658
+ .dock .btn--icon svg { width: 26px; height: 26px; }
659
+ .dock .stocky-icon { width: 34px; height: 34px; }
660
+ /* hidden while actively scrolling; springs back when scroll stops */
661
+ .dock--hidden { transform: translateX(-50%) translateY(165%); opacity: 0; pointer-events: none; }
662
+ /* popover that floats above the dock */
663
+ .dock-pop {
664
+ position: absolute; left: 50%; bottom: 98px; transform: translateX(-50%);
665
+ z-index: 16;
666
+ }
667
+ .dock-pop::after {
668
+ content: ""; position: absolute; left: 50%; bottom: -6px; transform: translateX(-50%) rotate(45deg);
669
+ width: 10px; height: 10px; background: var(--bg-primary);
670
+ border-right: 1px solid var(--border-strong); border-bottom: 1px solid var(--border-strong);
671
+ }
672
+
673
+ /* ----- Stat / metric tile --------------------------------------
674
+ A single number with label, optional delta and inline sparkline.
675
+ Delta uses the finance palette; the number is Berkeley Mono. */
676
+ .stat {
677
+ border: 0.5px solid var(--border-default); border-radius: var(--radius-md);
678
+ background: var(--bg-primary); padding: 16px 18px; font-family: var(--font-ui);
679
+ display: flex; flex-direction: column; gap: 8px;
680
+ }
681
+ .stat__label { font-size: 11px; letter-spacing: 0.6px; text-transform: uppercase; color: var(--text-muted); }
682
+ .stat__row { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
683
+ .stat__value {
684
+ font-family: var(--font-display); font-variant-numeric: tabular-nums;
685
+ font-size: 28px; font-weight: 600; color: var(--text-primary); line-height: 1; letter-spacing: -0.5px;
686
+ }
687
+ .stat__unit { font-family: var(--font-display); font-size: 13px; color: var(--text-muted); }
688
+ .stat__delta {
689
+ display: inline-flex; align-items: center; gap: 3px;
690
+ font-family: var(--font-display); font-variant-numeric: tabular-nums;
691
+ font-size: 13px; font-weight: 600;
692
+ }
693
+ .stat__delta--up { color: var(--data-up); }
694
+ .stat__delta--down { color: var(--data-down); }
695
+ .stat__delta--flat { color: var(--data-flat); }
696
+ .stat__foot { font-size: 11.5px; color: var(--text-muted); }
697
+ .stat__spark { margin-top: 2px; }
698
+
699
+ /* ----- Tooltip (general-purpose) -------------------------------
700
+ Reuses the grounding-tooltip mechanics for any hover hint. */
701
+ .tip { position: relative; display: inline-flex; cursor: help; }
702
+ .tip__pop {
703
+ position: absolute; left: 50%; bottom: calc(100% + 8px); transform: translateX(-50%);
704
+ width: max-content; max-width: 220px; text-align: center;
705
+ background: var(--text-primary); color: var(--bg-primary);
706
+ border-radius: var(--radius-sm); padding: 7px 10px;
707
+ font-family: var(--font-ui); font-size: 11px; line-height: 1.45;
708
+ opacity: 0; pointer-events: none; transition: opacity var(--dur-fast) var(--ease-out); z-index: 30;
709
+ }
710
+ .tip__pop::after {
711
+ content: ""; position: absolute; left: 50%; top: 100%; transform: translateX(-50%);
712
+ border: 5px solid transparent; border-top-color: var(--text-primary);
713
+ }
714
+ .tip:hover .tip__pop, .tip:focus-within .tip__pop { opacity: 1; }
715
+
716
+ /* Portal variant of .tip__pop for the React <Tooltip> (Radix). The React
717
+ tooltip portals its content to <body> so it escapes transformed ancestors
718
+ (the .dock uses transform + backdrop-filter); a node at <body> cannot be
719
+ positioned by the parent-anchored .tip__pop rules, so this class carries the
720
+ SAME inverted-bubble visual MINUS positioning — Radix's popper owns placement
721
+ and Radix mounts the content only while open (no opacity/pointer-events gate).
722
+ Same look as the .tip__pop oracle (background var(--text-primary), no shadow).
723
+ The caret is Radix's <Tooltip.Arrow>, filled to match the bubble. */
724
+ .tip-pop {
725
+ width: max-content; max-width: 220px; text-align: center;
726
+ background: var(--text-primary); color: var(--bg-primary);
727
+ border-radius: var(--radius-sm); padding: 7px 10px;
728
+ font-family: var(--font-ui); font-size: 11px; line-height: 1.45;
729
+ z-index: 30;
730
+ }
731
+ .tip-pop__arrow { fill: var(--text-primary); }
732
+
733
+ /* ----- Sparkline frame -----------------------------------------
734
+ The sparkline itself is drawn (SVG/canvas) by the app; this is
735
+ the sizing + caption wrapper and the trend coloring helpers. */
736
+ .sparkline { display: inline-flex; flex-direction: column; gap: 3px; }
737
+ .sparkline svg { display: block; }
738
+ .sparkline__cap { font-family: var(--font-display); font-size: 10px; color: var(--text-muted); }
739
+ .trend-up { color: var(--data-up); }
740
+ .trend-down { color: var(--data-down); }
741
+ .trend-flat { color: var(--data-flat); }
742
+
743
+ /* ----- Usage / progress meter ----------------------------------
744
+ Token usage and any bounded quantity. Track + fill; fill tints
745
+ to warn/over as it approaches/exceeds the limit. */
746
+ .meter { font-family: var(--font-ui); display: flex; flex-direction: column; gap: 6px; }
747
+ .meter__head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
748
+ .meter__label { font-size: 12px; color: var(--text-secondary); }
749
+ .meter__val { font-family: var(--font-display); font-variant-numeric: tabular-nums; font-size: 12px; color: var(--text-muted); }
750
+ .meter__track { height: 7px; border-radius: 999px; background: var(--bg-surface); overflow: hidden; }
751
+ .meter__fill { height: 100%; border-radius: 999px; background: var(--color-signal);
752
+ transition: width var(--dur-slow) var(--ease-out); }
753
+ .meter--warn .meter__fill { background: var(--data-warn); }
754
+ .meter--over .meter__fill { background: var(--data-down); }
755
+ .meter--over .meter__val { color: var(--data-down); }
756
+
757
+ /* ----- Avatar extensions ---------------------------------------
758
+ (base .avatar lives in the app-shell block). Adds sizes + a
759
+ neutral and a sage tone for initials, plus a stacked group. */
760
+ .avatar--sm { width: 22px; height: 22px; font-size: 9px; }
761
+ .avatar--xl { width: 52px; height: 52px; font-size: 18px; }
762
+ .avatar--neutral { background: var(--bg-surface); color: var(--text-secondary); }
763
+ .avatar-group { display: inline-flex; }
764
+ .avatar-group .avatar { border: 1.5px solid var(--bg-primary); }
765
+ .avatar-group .avatar + .avatar { margin-left: -8px; }
766
+ .avatar__status {
767
+ position: absolute; right: -1px; bottom: -1px; width: 9px; height: 9px;
768
+ border-radius: 50%; border: 1.5px solid var(--bg-primary); background: var(--data-up);
769
+ }
770
+ /* Status variants. Base .avatar__status is online (gain green). These are
771
+ presence states, not finance signals, so the dot shape itself is the cue —
772
+ no paired arrow needed (see Avatar JSDoc). Tokens reused from the palette:
773
+ offline = neutral stone, busy = loss red, away = caution gold. */
774
+ .avatar__status--offline { background: var(--data-flat); }
775
+ .avatar__status--busy { background: var(--data-down); }
776
+ .avatar__status--away { background: var(--data-warn); }
777
+ .avatar { position: relative; }
778
+
779
+ /* ----- Auth surfaces -------------------------------------------
780
+ Centered card for magic-link request, check-your-email, and the
781
+ verify/redirect loader. No password fields anywhere — this app
782
+ is magic-link only. */
783
+ .auth { max-width: 380px; margin: 0 auto; font-family: var(--font-ui);
784
+ display: flex; flex-direction: column; align-items: center; text-align: center; gap: 14px; }
785
+ .auth__mark {
786
+ width: 46px; height: 46px; border-radius: var(--radius-md);
787
+ display: grid; place-items: center; color: var(--text-primary); margin-bottom: 2px;
788
+ }
789
+ .auth__title { font-family: var(--font-heading); font-size: 19px; font-weight: 600;
790
+ color: var(--text-primary); margin: 0; letter-spacing: -0.3px; }
791
+ .auth__sub { font-size: 12.5px; line-height: 1.55; color: var(--text-muted); margin: 0; }
792
+ .auth__sub b { color: var(--text-secondary); font-weight: 600; }
793
+ .auth__form { width: 100%; display: flex; flex-direction: column; gap: 10px; margin-top: 4px; }
794
+ .auth__foot { font-size: 11px; color: var(--text-muted); margin: 2px 0 0; }
795
+ .auth__link { background: 0; border: 0; padding: 0; cursor: pointer;
796
+ color: var(--color-signal); font-family: var(--font-ui); font-size: 11.5px; }
797
+ /* status icon chip for check-email / success / error states */
798
+ .auth__icon { width: 52px; height: 52px; border-radius: 50%; display: grid; place-items: center; }
799
+ .auth__icon--ok { background: var(--data-up-bg); color: var(--data-up); }
800
+ .auth__icon--mail { background: var(--signal-bg); color: var(--color-signal); }
801
+ .auth__icon--err { background: var(--data-down-bg); color: var(--data-down); }
802
+
803
+ /* ----- Briefing / digest card ----------------------------------
804
+ Periodic LLM-narrated market briefing (≈3/user/session-day).
805
+ States: unread (sage accent + dot), read (muted), loading
806
+ (skeleton lines), empty. Narrated numbers may carry the
807
+ grounding annotations (.ground-wave / .gwrap) inline. */
808
+ .digest { border: 0.5px solid var(--border-default); border-radius: var(--radius-md);
809
+ background: var(--bg-primary); padding: 16px 18px; display: flex; flex-direction: column; gap: 10px;
810
+ font-family: var(--font-ui); }
811
+ .digest--unread { border-left: 3px solid var(--color-signal); }
812
+ .digest__head { display: flex; align-items: center; gap: 8px; }
813
+ .digest__dot { width: 7px; height: 7px; border-radius: 50%; background: var(--color-signal); flex: none; }
814
+ .digest__period { font-size: 10px; letter-spacing: 1px; text-transform: uppercase; color: var(--text-muted); }
815
+ .digest__title { font-family: var(--font-heading); font-size: 14.5px; font-weight: 600;
816
+ color: var(--text-primary); margin: 0; }
817
+ .digest__body { font-size: 13px; line-height: 1.65; color: var(--text-secondary); margin: 0; }
818
+ .digest__body b { font-family: var(--font-display); color: var(--text-primary); font-weight: 600; }
819
+ .digest__foot { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 2px; }
820
+ .digest__time { font-family: var(--font-display); font-size: 10.5px; color: var(--text-muted); }
821
+ .digest__actions { display: flex; gap: 4px; }
822
+ .digest--read { opacity: 0.85; }
823
+ .digest--read .digest__title { color: var(--text-secondary); font-weight: 500; }
824
+ .digest--read .digest__dot { display: none; }
825
+ .digest__skel { display: flex; flex-direction: column; gap: 9px; }
826
+
827
+ /* ----- Scrollbar (design-system default) ------------------------
828
+ Thin, unobtrusive, token-driven. Track is transparent; the thumb
829
+ uses the strong border token and darkens on hover. Applies to any
830
+ scrollable surface so overflow chrome matches the brand instead
831
+ of falling back to the heavy native bar. */
832
+ * {
833
+ scrollbar-width: thin;
834
+ scrollbar-color: var(--border-strong) transparent;
835
+ }
836
+ *::-webkit-scrollbar { width: 10px; height: 10px; }
837
+ *::-webkit-scrollbar-track { background: transparent; }
838
+ *::-webkit-scrollbar-thumb {
839
+ background: var(--border-strong);
840
+ border-radius: 999px;
841
+ border: 3px solid transparent;
842
+ background-clip: padding-box;
843
+ }
844
+ *::-webkit-scrollbar-thumb:hover {
845
+ background: var(--text-muted);
846
+ background-clip: padding-box;
847
+ }
848
+ *::-webkit-scrollbar-corner { background: transparent; }