Skip to main content

max / balanced_breakfast

29.6 KB · 786 lines History Blame Raw
1 /* === 9. Buttons ========================================================
2 Canonical: .button (+ .button--primary, .button--secondary, .button--success, .button--danger,
3 .button--icon, .button--text, .button--link; size .button--sm; state .button--loading).
4 Charter target: collapse remaining one-off *-btn classes onto this family.
5 ========================================================================== */
6 /* The fill and the bevel are gone from here, along with the whole
7 :active rule: .button, .button:hover and .button:active come from
8 makeover-webview, which emits the fill and the edge together so they
9 cannot disagree. What stays is everything that is not depth -- box
10 model, type, cursor -- plus the disabled overrides below.
11
12 Two visible consequences, both the description's answer rather than
13 this file's: hover reads as --hover-surface instead of
14 --surface-sunken, and pressing reads as a well. */
15 .button {
16 padding: var(--gap-peer) var(--gap-group);
17 border: var(--border-width) solid var(--border);
18 border-radius: var(--radius-sm);
19 color: var(--content);
20 cursor: pointer;
21 font-weight: 600;
22 font-size: var(--font-size-base);
23 }
24 .button:hover {
25 border-color: var(--border-strong);
26 }
27 /* Disabled keeps its bevel. The object is still there; greying the content
28 is what says the action is unavailable -- and the greying is the generated
29 sheet's, along with the cursor. Only the edge is this app's. */
30 .button:disabled {
31 border-color: var(--content-muted);
32 }
33 /* The `.button:disabled:hover, .button:disabled:active` pair that used to
34 sit here is gone. It existed to out-specify the generated hover and
35 pressed rules, in those words, and makeover-webview 0.10.0 emits
36 disabled after both and restates the rest depth, so source order settles
37 it at equal specificity. Nothing has to reach (0,3,0) to win any more,
38 which matters because raising a selector is what the cascade layer would
39 have had to unpick.
40
41 The colour, border colour and cursor above stay, and are not a candidate
42 for the same deletion. Layers resolve before specificity, so the
43 generated `.button:disabled` in `makeover` loses to `.button` in this
44 file whatever its specificity. An app has to state the disabled variant
45 of every property it sets at rest; makeover can only supply what the app
46 is silent about. */
47 .button--primary {
48 background-color: var(--action);
49 border-color: var(--action);
50 color: var(--content-on-action);
51 }
52 .button--primary:hover { background-color: var(--action-hover); border-color: var(--action-hover); }
53 .button--success {
54 background-color: var(--category-four);
55 border-color: var(--category-four);
56 color: var(--content-on-action);
57 }
58 .button--success:hover { background-color: var(--accent-yellow-light); border-color: var(--accent-yellow-light); }
59 /* .button--sm: canonical small-button size (see wiki note `bb-design-system`). */
60 .button--sm { padding: var(--gap-bound) var(--gap-peer); font-size: var(--font-size-md); }
61 .button--sm.active { background: var(--category-four); color: var(--content-on-action); border-color: var(--category-four); }
62
63 /* .button--danger: destructive action. Consumes the previously-unused
64 --accent-red-hover token. Replaces .hp-action-danger in F5. */
65 .button--danger {
66 background-color: var(--danger);
67 border-color: var(--danger);
68 color: var(--content-on-action);
69 }
70 .button--danger:hover { background-color: var(--accent-red-hover); border-color: var(--accent-red-hover); }
71
72 /* === N. Row primitive (F2-sub addition, 2026-06-02) ===================
73 Canonical slot layout: [icon] [primary ยท badges] [meta] [actions].
74 Used via BB.ui.renderRow(model). Per-surface classes (.plugin-item,
75 .source-item, .item, .bookmark-item, etc.) sit alongside .row on the
76 outer element and add their own padding / border / hover treatment.
77 F5 retires per-surface name/desc classes (.plugin-name, .plugin-desc)
78 that duplicate .row-primary / .row-secondary.
79 ========================================================================== */
80 .row {
81 display: flex;
82 align-items: flex-start;
83 gap: var(--gap-bound);
84 min-width: 0;
85 }
86 .row-icon {
87 flex-shrink: 0;
88 display: flex;
89 align-items: center;
90 justify-content: center;
91 }
92 .row-content {
93 flex: 1;
94 min-width: 0;
95 }
96 /* The three row text colours come from makeover-webview, which derives
97 them from the same content ramp this file used to spell out by hand.
98 Identical values, so nothing moves; what changes is that there is one
99 place to change them. */
100 .row-primary {
101 font-weight: 600;
102 font-size: var(--font-size-base);
103 line-height: 1.3;
104 display: flex;
105 align-items: center;
106 gap: var(--gap-bound);
107 flex-wrap: wrap;
108 }
109 .row-secondary {
110 font-size: var(--font-size-md);
111 line-height: 1.4;
112 margin-top: var(--step-hair);
113 }
114 .row-meta {
115 flex-shrink: 0;
116 font-size: var(--font-size-sm);
117 align-self: center;
118 }
119 .row-actions {
120 flex-shrink: 0;
121 display: flex;
122 align-items: center;
123 gap: var(--gap-peer);
124 }
125 /* The hover-reveal comes from makeover-webview now. It also toggles
126 pointer-events, which the hand-written pair did not: a hidden action
127 used to be invisible and still clickable, so the row carried dead
128 targets a cursor could find. The `.always` escape hatch went with the
129 rules it was written against; nothing ever set it. */
130
131 /* F2-sub addition: form-hint error variant (used by renderFormField when
132 passed a `field.error`). */
133 .form-hint--error {
134 color: var(--danger);
135 font-weight: 600;
136 }
137
138 /* === 10. Forms =========================================================
139 Canonical: .form-group + .field.
140
141 One class for every field kind, with the kind riding on the element
142 (`select.field`, `textarea.field`) rather than on a second class. The F2
143 split into .form-input / .form-select / .form-textarea is retired: all
144 twenty-odd call sites had .form-input on an <input>, .form-select on a
145 <select> and .form-textarea on a <textarea>, with no exceptions, so the
146 class was saying what the tag already said, and an element selector cannot
147 be forgotten at a call site the way a modifier class can.
148
149 Fill and inset edge now come from the generated layout.css, where a field
150 is a Depth::Well. Every rule here used to write `background-color:
151 var(--surface-page)` under `box-shadow: var(--bevel-inset)` -- a page fill
152 beneath a recessed edge -- and so did .search-input, .sort-select and (with
153 no bevel at all) the two mobile fields. Five hand-written answers to one
154 question. goingson had the same pair and retired it the same way; its
155 .field block carries the long form of this note.
156 ========================================================================== */
157 .form-group { margin-bottom: var(--gap-section); }
158
159 /* Shape only. The wider padding is what .search-input and .sort-select
160 carried; a field inside a form group keeps the tighter one, below. */
161 .field {
162 padding: var(--gap-peer) var(--gap-group);
163 border: var(--border-width) solid var(--border);
164 border-radius: var(--radius-sm);
165 color: var(--content);
166 font-size: var(--font-size-base);
167 }
168
169 /* A field in a form group fills it. The three fields outside one carry their
170 own width, which is why this is not on .field itself. */
171 .form-group .field {
172 width: 100%;
173 padding: var(--gap-peer);
174 }
175
176 select.field { cursor: pointer; }
177
178 /* layout.css draws the disabled field -- muted content, not-allowed cursor --
179 but the two rules above set colour and cursor at rest in an unlayered sheet,
180 so the resting rules win and the generated disabled arm never applies. An
181 app must state the disabled variant of every property it sets at rest.
182 Found by adopting the generated states, not reported. The fill and the edge
183 come from layout.css and are not restated here. */
184 .field:disabled,
185 .field[aria-disabled="true"] {
186 color: var(--content-muted);
187 cursor: not-allowed;
188 }
189
190 textarea.field {
191 font-family: inherit;
192 resize: vertical;
193 min-height: 4rem;
194 }
195
196 /* .form-label replaces the unclassed `<label>` under `.form-group`. */
197 .form-group label,
198 .settings-content .form-group label,
199 .form-label {
200 display: block;
201 margin-bottom: var(--gap-bound);
202 color: var(--content-secondary);
203 font-size: var(--font-size-md);
204 font-weight: 700;
205 }
206 /* .form-row: horizontal grouping of two related fields (e.g. label + button) */
207
208 .form-actions {
209 display: flex;
210 justify-content: flex-end;
211 gap: var(--gap-peer);
212 margin-top: var(--gap-section);
213 }
214
215 .form-hint {
216 font-size: var(--font-size-xs);
217 color: var(--content-muted);
218 margin-top: var(--gap-bound);
219 }
220
221 /* === 11. Modal =========================================================
222 Canonical: .modal-overlay (single global at #modal-overlay) +
223 .modal-content (will rename to .modal-container for parity in F5).
224 .tag block (13. Tag) lives below this block in file order.
225 ========================================================================== */
226 .modal-overlay {
227 position: fixed;
228 top: 0; left: 0; right: 0; bottom: 0;
229 background-color: color-mix(in srgb, var(--content) 40%, transparent);
230 display: none;
231 align-items: center;
232 justify-content: center;
233 z-index: 1000;
234 }
235
236 .modal-content {
237 background-color: var(--surface-raised);
238 border-radius: var(--radius-lg);
239 width: 90%;
240 max-width: 500px;
241 max-height: 80vh;
242 overflow-y: auto;
243 /* Floating surfaces carry both: the bevel says the panel is a lit
244 object, the drop shadow says it is above the page. */
245 box-shadow: var(--bevel-raised), var(--shadow-brutal-lg);
246 }
247
248 .modal-header {
249 display: flex;
250 justify-content: space-between;
251 align-items: center;
252 padding: var(--gap-group) var(--gap-pane);
253 border-bottom: var(--border-width) solid var(--border);
254 background-color: var(--surface-overlay);
255 border-radius: var(--radius-lg) var(--radius-lg) 0 0;
256 }
257 .modal-header h2 { font-size: var(--font-size-xl); font-weight: 700; color: var(--content); }
258
259 /* F3 (2026-06-02): step indicator for multi-step modal flows (OAuth,
260 plugin import, encryption setup). Set via BB.ui.setModalStep(n, of). */
261 .modal-step-indicator {
262 margin-left: var(--gap-bound);
263 padding: var(--step-hair) var(--step-base);
264 background-color: var(--surface-sunken);
265 border: var(--border-width) solid var(--border);
266 border-radius: var(--radius-xl);
267 font-size: var(--font-size-xs);
268 font-weight: 600;
269 color: var(--content-muted);
270 white-space: nowrap;
271 }
272
273 .modal-body { padding: var(--gap-pane); }
274
275
276
277 /* Carries `.card`: a pressable raised object with a hover fill and an inset
278 pressed state, which is what these three rules wrote by hand. The fill, the
279 bevel, the hover and the press all come from layout.css now, wrapped in
280 (hover: hover) and (pointer: fine) there so a fingertip gets no phantom
281 hover. What is left here is the box and the accent border. */
282 .plugin-item {
283 padding: var(--gap-group);
284 border: var(--border-width) solid var(--border);
285 border-radius: var(--radius-sm);
286 margin-bottom: var(--gap-peer);
287 cursor: pointer;
288 }
289 .plugin-item:hover {
290 border-color: var(--category-four);
291 }
292
293 /* F5 (2026-06-02): dropped .plugin-name and .plugin-desc; plugin-list
294 migrated to BB.ui.renderRow in F2-sub; .row-primary / .row-secondary
295 carry the typography now. */
296
297 /* F2-sub (2026-06-02): intro paragraph at the top of multi-step modals
298 (replaces inline `style="margin-bottom:1rem;color:var(--content-secondary)"`
299 in feeds.js plugin picker). */
300 .modal-intro {
301 margin-bottom: var(--gap-section);
302 color: var(--content-secondary);
303 font-size: var(--font-size-base);
304 }
305
306 /* F4 additions (2026-06-02): replace inline styles previously hard-coded
307 in JS render paths. Each rule below retires a specific style="..." or
308 style.cssText violation flagged in the F4 catalogue. */
309
310 /* sources.js empty-state (no feeds yet). Was inline cssText + emoji
311 wrapper. F5 may further consolidate onto the .empty-state block once
312 the row primitive migration lands; this fix lives in the sidebar list
313 so it stays a styled <li>. */
314 .source-item--empty {
315 cursor: default;
316 flex-direction: column;
317 align-items: center;
318 text-align: center;
319 padding: var(--gap-pane) var(--gap-group);
320 color: var(--content-muted);
321 font-size: var(--font-size-md);
322 line-height: 1.5;
323 }
324 .source-item--empty:hover { background-color: transparent; }
325 .source-item-empty-icon {
326 display: flex;
327 justify-content: center;
328 margin-bottom: var(--gap-peer);
329 color: var(--content-muted);
330 }
331 .source-item-empty-icon svg { width: 1.5rem; height: 1.5rem; }
332 .source-item-empty-strong { color: var(--category-four); font-weight: 700; }
333 .source-item-empty-link { color: var(--category-four); }
334
335 /* sources.js "+ Saved Filter" button label. Was inline color. */
336
337 /* settings-sync.js renderConnect block. Was three inline styles. */
338 .sync-connect {
339 text-align: center;
340 padding: var(--gap-pane) 0;
341 }
342 .sync-connect p { margin-bottom: var(--gap-section); }
343 .sync-connect p + p { margin-bottom: var(--gap-pane); color: var(--content-secondary); }
344
345 /* settings-sync.js showCodeEntry spinner. Was three inline styles. */
346 .sync-auth-spinner {
347 text-align: center;
348 padding: var(--gap-group) 0;
349 color: var(--content-secondary);
350 }
351
352 /* settings-sync.js pricing fine-print. Class already existed but carried
353 inline styles; rules moved here. */
354 .sync-sub-fine-print {
355 font-size: var(--font-size-base);
356 opacity: 0.75;
357 margin-top: var(--gap-bound);
358 }
359
360 /* settings-sync.js subscribe button row. Layout-only; class-ifying for
361 consistency. */
362 .sync-sub-actions {
363 display: flex;
364 gap: var(--gap-peer);
365 margin-top: var(--gap-peer);
366 }
367
368 /* mobile.js action sheets: stacked full-width buttons. Replaces inline
369 `style.display='block'; style.width='100%'; style.marginBottom='0.5rem'`. */
370 .button--stacked {
371 display: block;
372 width: 100%;
373 margin-bottom: var(--gap-peer);
374 }
375 .button--stacked:last-child { margin-bottom: 0; }
376
377 /* app.js settings theme-import/export row. Layout-only; class-ifying. */
378 .theme-actions {
379 display: flex;
380 gap: var(--gap-peer);
381 margin-top: var(--gap-peer);
382 }
383
384 /* Default body paragraph rhythm: retires the per-`<p>` inline
385 `margin-bottom: 0.75rem` pattern in feeds.js plugin-warning. */
386 .modal-body p + p { margin-top: var(--gap-section); }
387
388 /* === 12. Toast =========================================================
389 Canonical: .toast (+ .toast.success / .toast.error today;
390 rename to .toast-success / .toast-error in F5, add .toast-info,
391 .toast-undo). Helper: BB.ui.showToast; must inject zero style.cssText.
392 ========================================================================== */
393 .toast-container {
394 position: fixed;
395 bottom: 1rem;
396 right: 1rem;
397 z-index: 1001;
398 }
399
400 .toast {
401 background-color: var(--surface-raised);
402 border: var(--border-width) solid var(--border);
403 border-radius: var(--radius-sm);
404 padding: var(--gap-peer) var(--gap-group);
405 margin-top: var(--gap-peer);
406 box-shadow: var(--bevel-raised), var(--shadow-brutal);
407 animation: slideIn 0.3s ease-out;
408 font-weight: 500;
409 font-size: var(--font-size-base);
410 }
411 .toast.success { border-left: 4px solid var(--success); background-color: color-mix(in srgb, var(--success) 8%, var(--surface-page)); }
412 .toast.error { border-left: 4px solid var(--danger); background-color: color-mix(in srgb, var(--danger) 8%, var(--surface-page)); }
413 .toast-undo {
414 border-left: 4px solid var(--category-four);
415 background-color: color-mix(in srgb, var(--category-four) 10%, var(--surface-page));
416 display: flex;
417 align-items: center;
418 gap: var(--gap-bound);
419 }
420 .undo-message { flex: 1; }
421 .undo-btn {
422 background: none;
423 border: var(--border-width) solid currentColor;
424 border-radius: var(--radius-sm);
425 color: var(--content);
426 cursor: pointer;
427 padding: var(--step-tight) var(--step-roomy);
428 font-size: var(--font-size-md);
429 font-weight: 600;
430 }
431 .undo-btn:hover { background-color: color-mix(in srgb, var(--category-four) 20%, transparent); }
432 .undo-countdown {
433 font-size: var(--font-size-xs);
434 color: var(--content-muted);
435 min-width: 1.5rem;
436 text-align: right;
437 font-variant-numeric: tabular-nums;
438 }
439
440 @keyframes slideIn {
441 from { transform: translateX(100%); opacity: 0; }
442 to { transform: translateX(0); opacity: 1; }
443 }
444
445 /* === 13. Tag ===========================================================
446 Canonical: .tag[data-color], display chip (detail panel, source/bookmark
447 tags). Size modifier .tag--xs. .tag-chip is the separate interactive
448 filter-chip (tag bars): it toggles (cursor + .active), unlike a display tag.
449 Source tags use .tag.tag--xs[data-color="muted"]; .bookmark-tag composes on
450 .tag with a click-to-filter modifier.
451 ========================================================================== */
452 /* Carries `.raised`, which is where the fill and the bevel come from now.
453 Left standing on purpose: a tag is a label and not pressable, which is the
454 argument that flattened `.badge` on 2026-08-05, so `.tag` being an object at
455 all is worth a second look. That is a decision about how the app should
456 look, not a place the design system was being restated, so it is recorded
457 rather than taken here. */
458 .tag {
459 border: var(--border-width) solid var(--border);
460 border-radius: var(--radius-sm);
461 padding: var(--step-hair) var(--step-base);
462 font-size: var(--font-size-sm);
463 color: var(--content-secondary);
464 }
465
466 /* F2 additions (2026-06-02): color variants via [data-color] attribute.
467 Consumes --category-five and --category-six.
468
469 .badge dropped off these selectors when badges moved to [data-tone]:
470 nothing sets [data-color] on a badge any more, so the badge halves were
471 matching nothing. .tag keeps the axis, since a tag names a category
472 rather than a state and green-because-green is the right contract for
473 it. That is also why the two stopped sharing: they were never saying
474 the same thing, only spelling it the same way. */
475 .tag[data-color="green"] { border-color: var(--category-two); color: var(--category-two); background-color: color-mix(in srgb, var(--category-two) 10%, transparent); }
476 .tag[data-color="yellow"] { border-color: var(--category-four); color: var(--content); background-color: color-mix(in srgb, var(--category-four) 12%, transparent); }
477 .tag[data-color="red"] { border-color: var(--category-one); color: var(--category-one); background-color: color-mix(in srgb, var(--category-one) 10%, transparent); }
478 .tag[data-color="blue"] { border-color: var(--category-three); color: var(--category-three); background-color: color-mix(in srgb, var(--category-three) 10%, transparent); }
479 .tag[data-color="cyan"] { border-color: var(--category-six); color: var(--category-six); background-color: color-mix(in srgb, var(--category-six) 10%, transparent); }
480 .tag[data-color="purple"] { border-color: var(--category-five); color: var(--category-five); background-color: color-mix(in srgb, var(--category-five) 10%, transparent); }
481 .tag[data-color="muted"] { border-color: var(--border); color: var(--content-muted); background-color: var(--surface-sunken); }
482 .tag--xs { font-size: var(--font-size-xxs); padding: 0 var(--step-snug); } /* mirrors .badge--xs */
483
484 /* .badge: smaller and more compact than .tag; used for counts and inline
485 status indicators.
486
487 The colour axis is [data-tone], which makeover-webview emits, replacing
488 the [data-color] axis this file used to carry. The old axis named the
489 paint (green, yellow, red, blue) and the new one names the meaning, so
490 a badge now says what it is rather than what colour it came out. The
491 swap is close to free here because three of the four category tokens
492 were already the same value under a second name: category-one is
493 danger, category-two is success, category-four is warning.
494
495 The base text colour is the generated one. What stays below is the box:
496 the border and the type.
497
498 The raised fill and bevel this rule used to add are gone (2026-08-05).
499 Token::Badge is Depth::Flat because a label is not pressable, GoingsOn's
500 badges are flat, and BB was the only app dressing one as an object. A
501 divergence nothing else shares is drift, whatever the comment above it
502 said. */
503 .badge {
504 display: inline-flex;
505 align-items: center;
506 gap: var(--gap-bound);
507 border: var(--border-width) solid var(--border);
508 border-radius: var(--radius-sm);
509 padding: 0 var(--step-snug);
510 font-size: var(--font-size-xs);
511 font-weight: 600;
512 line-height: 1.3;
513 white-space: nowrap;
514 }
515 /* Filled is this app's own variant and has no counterpart in the
516 description, which tones the text and leaves the fill alone. It is kept
517 because a filled badge is how a recommendation reads at a glance, and it
518 is driven off the same tone attribute so there is still one axis. */
519 .badge--filled { border: none; }
520 .badge--filled[data-tone="info"] { background-color: var(--info); color: var(--content-on-action); }
521 .badge--filled[data-tone="success"] { background-color: var(--success); color: var(--content-on-action); }
522 .badge--filled[data-tone="warning"] { background-color: var(--warning); color: var(--content); }
523 .badge--filled[data-tone="danger"] { background-color: var(--danger); color: var(--content-on-action); }
524
525 .detail-body {
526 color: var(--content);
527 line-height: 1.7;
528 font-size: var(--font-size-base);
529 overflow-wrap: break-word;
530 word-break: break-word;
531 }
532 .detail-body p { margin-bottom: 0.5em; }
533 .detail-body pre {
534 white-space: pre-wrap;
535 background-color: var(--surface-sunken);
536 padding: var(--gap-peer);
537 border-radius: var(--radius-sm);
538 overflow-x: auto;
539 }
540
541 .detail-actions {
542 display: flex;
543 gap: var(--gap-peer);
544 margin-top: var(--gap-section);
545 padding-top: var(--gap-section);
546 border-top: var(--border-width) solid var(--border);
547 }
548
549 /* === 14. Context menu ==================================================
550 Canonical: .context-menu (state: .visible). Items: .context-menu-item
551 (+ .context-menu-item.danger; rename to --danger in F5). Charter
552 target: add BB.ui.showContextMenu(x, y, items) helper.
553 ========================================================================== */
554 .context-menu {
555 position: fixed;
556 z-index: 1100;
557 background: var(--surface-raised);
558 border: var(--border-width) solid var(--border);
559 border-radius: var(--radius-sm);
560 box-shadow: var(--bevel-raised), var(--shadow-brutal);
561 padding: var(--gap-bound);
562 min-width: 140px;
563 }
564 .context-menu-item {
565 display: block;
566 width: 100%;
567 padding: var(--gap-peer) var(--gap-group);
568 border: none;
569 border-radius: var(--radius-sm);
570 background: none;
571 color: var(--content);
572 font-size: var(--font-size-md);
573 text-align: left;
574 cursor: pointer;
575 }
576 .context-menu-item:hover {
577 background-color: var(--surface-sunken);
578 }
579
580 /* === 15. Health indicator ==============================================
581 STATE-BY-COLOR-ONLY VIOLATION (F3): pair color with shape (--broken
582 = hollow ring, --rate_limited = dashed) + aria-label. F5 fix.
583 ========================================================================== */
584 .health-dot {
585 display: inline-block;
586 width: 8px;
587 height: 8px;
588 border-radius: var(--radius-full);
589 margin-right: var(--gap-bound);
590 flex-shrink: 0;
591 }
592 /* F5 (2026-06-02): dropped legacy color-only .health-* classes; render
593 site (sources.js) and the sources render tests updated to use [data-health]. */
594
595 /* F3 fix (2026-06-02): state-by-color violation retired. Each state now
596 pairs color with a SHAPE modifier via [data-health], so the indicator
597 is distinguishable in monochrome / for color-blind users. Render
598 site must also pass an `aria-label` for screen readers. */
599 .health-dot[data-health="green"] { background-color: var(--success); }
600 .health-dot[data-health="yellow"],
601 .health-dot[data-health="rate_limited"] {
602 background-color: var(--category-four);
603 border: 1.5px dashed var(--shadow-color);
604 box-sizing: border-box;
605 }
606 .health-dot[data-health="red"] { background-color: var(--danger); position: relative; }
607 .health-dot[data-health="red"]::after,
608 .health-dot[data-health="auth_error"]::after,
609 .health-dot[data-health="config_error"]::after {
610 content: '!';
611 position: absolute;
612 top: 50%; left: 50%;
613 transform: translate(-50%, -55%);
614 color: var(--content-on-action);
615 font-size: 7px;
616 font-weight: 900;
617 line-height: 1;
618 pointer-events: none;
619 }
620 .health-dot[data-health="auth_error"],
621 .health-dot[data-health="config_error"] {
622 background-color: var(--danger);
623 position: relative;
624 }
625 .health-dot[data-health="circuit_broken"] {
626 background-color: transparent;
627 border: 2px solid var(--content-muted);
628 box-sizing: border-box;
629 }
630
631 /* === 16. Skeleton & loading ============================================
632 Canonical: .skeleton-item, .skeleton-line.{short,medium,long}. Charter
633 target (F5): add BB.ui.renderSkeleton helper; widen first-paint usage
634 beyond items panel.
635 ========================================================================== */
636 .skeleton-item {
637 display: flex;
638 padding: var(--gap-peer) var(--gap-section);
639 border-bottom: var(--border-width) solid var(--border);
640 animation: shimmer 1.5s infinite;
641 }
642 .skeleton-item .skeleton-indicators { width: 24px; margin-right: var(--gap-bound); }
643 .skeleton-item .skeleton-content { flex: 1; }
644 .skeleton-line {
645 height: 0.75rem;
646 border-radius: var(--radius-sm);
647 background: linear-gradient(90deg, var(--surface-sunken) 25%, var(--border) 50%, var(--surface-sunken) 75%);
648 background-size: 200% 100%;
649 animation: shimmer 1.5s infinite;
650 }
651 .skeleton-line.short { width: 40%; }
652 .skeleton-line.long { width: 90%; margin-top: var(--gap-peer); }
653
654 @keyframes shimmer {
655 0% { background-position: 200% 0; }
656 100% { background-position: -200% 0; }
657 }
658
659 /* === 17. Progress bar ================================================== */
660 .progress-bar-container {
661 position: absolute;
662 bottom: 0;
663 left: 0;
664 right: 0;
665 height: 3px;
666 background-color: var(--border);
667 box-shadow: var(--bevel-inset);
668 overflow: hidden;
669 }
670 /* Kept motion: the fill moves through space, which is period-correct. State
671 feedback that only recolors is not, and was dropped across the file. */
672 .progress-bar {
673 height: 100%;
674 background-color: var(--category-four);
675 transition: width var(--transition-slow) ease;
676 }
677 /* The two numbers the bar is drawn from, said out loud. A 3px bar at the foot
678 of the header reports roughly-a-quarter and nothing else; this says which
679 quarter of what. Empty until something has counted, so a refresh that has not
680 heard from a feed yet shows a trough and no claim. */
681 .progress-reading {
682 position: absolute;
683 right: var(--gap-section);
684 bottom: var(--gap-bound);
685 color: var(--content-muted);
686 font-size: var(--font-size-xs);
687 }
688
689 /* Toast with action */
690 .toast-action {
691 display: inline-block;
692 margin-left: var(--gap-bound);
693 background: none;
694 border: var(--border-width) solid currentColor;
695 border-radius: var(--radius-sm);
696 color: inherit;
697 cursor: pointer;
698 padding: var(--step-hair) var(--step-base);
699 font-size: var(--font-size-md);
700 font-weight: 600;
701 }
702 .toast-action:hover { opacity: 0.8; }
703
704 /* === 18. Update banner ================================================
705 z-index 1200: above mobile nav (1100/1101) and context menus, below
706 any future fullscreen overlay. F5: lowered from 9999 (was unnecessarily
707 high; sat above modals which made it impossible to dismiss when a
708 modal opened post-banner-show).
709 ========================================================================== */
710 .update-banner {
711 position: fixed;
712 bottom: 1rem;
713 right: 1rem;
714 background: var(--surface-overlay);
715 border: var(--border-width) solid var(--border);
716 border-radius: var(--radius-sm);
717 padding: var(--gap-peer) var(--gap-group);
718 z-index: 1200;
719 max-width: 320px;
720 box-shadow: var(--bevel-raised), var(--shadow-brutal);
721 font-size: var(--font-size-base);
722 }
723
724 /* === 19. Health popover ================================================
725 Floating panel from gear icon. Actions use the .button family (.hp-actions .button
726 fills the row). .hp-row is a one-off label/value line (kept).
727 ========================================================================== */
728 .health-popover {
729 position: fixed;
730 z-index: 100;
731 background: var(--surface-overlay);
732 border: var(--border-width) solid var(--border);
733 border-radius: var(--radius-sm);
734 box-shadow: var(--bevel-raised), var(--shadow-brutal);
735 padding: var(--gap-group);
736 min-width: 220px;
737 max-width: 320px;
738 font-size: var(--font-size-md);
739 }
740 .hp-title {
741 font-weight: 700;
742 margin-bottom: var(--gap-peer);
743 padding-bottom: var(--gap-peer);
744 border-bottom: var(--border-width) solid var(--border);
745 }
746 .hp-row {
747 display: flex;
748 justify-content: space-between;
749 gap: var(--gap-bound);
750 padding: var(--gap-bound) 0;
751 }
752 .hp-row-error {
753 flex-direction: column;
754 gap: var(--step-hair);
755 }
756 .hp-label {
757 color: var(--content-muted);
758 flex-shrink: 0;
759 }
760 .hp-value {
761 text-align: right;
762 word-break: break-word;
763 }
764 .hp-row-error .hp-value {
765 text-align: left;
766 font-size: var(--font-size-sm);
767 color: var(--content-secondary);
768 }
769 .hp-green { color: var(--success); }
770 .hp-yellow { color: var(--category-four); }
771 .hp-red { color: var(--danger); }
772 .hp-grey { color: var(--content-muted); }
773
774 .hp-actions {
775 display: flex;
776 gap: var(--gap-peer);
777 margin-top: var(--gap-section);
778 padding-top: var(--gap-peer);
779 border-top: var(--border-width) solid var(--border);
780 }
781 /* Popover actions are the .button family (Mark Read / Edit / Tags / Delete);
782 they fill the row equally. */
783 .hp-actions .button { flex: 1; }
784
785
786