Skip to main content

max / balanced_breakfast

Move spacing onto makeover-geometry BB's charter already named an invariant layer and said a theme overrides colour only. This does not introduce that idea, it follows it: a layer that never varies by theme has no reason to belong to one app, so --space-1..6 leaves :root for the makeover-geometry crate that GoingsOn and audiofiles already read. build.rs materialises css/geometry.css beside the themes it already writes, and every gap, padding and margin now names what it separates rather than how big it is. Density hangs off @media (hover: none), not off the 768px breakpoint. Density is who is operating the app; the breakpoint is how wide the window is, and a narrow window on a desktop still has a pointer in it. BB already kept a hover:none block for exactly that distinction, so the touch preset lands with the touch overrides that were already there, and the hand-written "larger tap targets" spacing in the mobile block goes away: a preset that opens every gap at once says the same thing better than three hand-picked rules. One lint subtlety worth knowing. theme-token-coverage reads the :root body for anything shaped like a token name, so an explanatory comment inside :root that mentions a token pattern fails it. The note about where spacing went therefore sits below the closing brace, and the generated file is outside what the rule scans: every token in it is invariant by construction, which is the whole reason the file exists. Fixes three stale comments in passing, all in blocks this change already opened: theme-common became makeover, the design-system doc moved to the wiki in July, and the [data-color] variants consume the category intents rather than the accent names that no longer exist. Layout is deliberately not preserved; the vocabulary is tighter than the scale it replaces.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-27 02:32 UTC
Signed with PGP, not checked
Commit: 248444ce2287b880aef7d13c88ec73f8fb452299
Parent: 7ad18bd
7 files changed, +155 insertions, -84 deletions
M .gitignore +3
@@ -34,3 +34,6 @@
34 34
35 35 # Generated by src-tauri/build.rs from makeover's embedded theme set
36 36 /src-tauri/themes/
37 +
38 + # Generated by src-tauri/build.rs from makeover-geometry's scale
39 + /src-tauri/frontend/css/geometry.css
M Cargo.lock +7
@@ -445,6 +445,7 @@
445 445 "chrono",
446 446 "futures",
447 447 "makeover",
448 + "makeover-geometry",
448 449 "open",
449 450 "parking_lot",
450 451 "rand 0.10.2",
@@ -3112,6 +3113,12 @@
3112 3113 "toml 1.1.3+spec-1.1.0",
3113 3114 ]
3114 3115
3116 + [[package]]
3117 + name = "makeover-geometry"
3118 + version = "0.1.0"
3119 + source = "registry+https://github.com/rust-lang/crates.io-index"
3120 + checksum = "9d8b94e51333489b0a79bb63ad4100ca49726f2048e2477f6ee3d6dd822f9327"
3121 +
3115 3122 [[package]]
3116 3123 name = "maplit"
3117 3124 version = "1.0.2"
M Cargo.toml +3
@@ -54,6 +54,9 @@
54 54 chrono = { version = "0.4.43", features = ["serde"] }
55 55 uuid = { version = "1.20.0", features = ["v4", "serde"] }
56 56 makeover = "2.1.0"
57 + # The invariant half of the design system: relational spacing. makeover
58 + # resolves colour, which a theme may override; this carries what no theme may.
59 + makeover-geometry = "0.1.0"
57 60 toml = "1.1"
58 61 dirs = "6.0.0"
59 62
@@ -15,6 +15,7 @@
15 15 [build-dependencies]
16 16 tauri-build = { version = "2.5.5", features = [] }
17 17 makeover.workspace = true
18 + makeover-geometry.workspace = true
18 19
19 20 [dependencies]
20 21 bb-interface.workspace = true
@@ -26,7 +26,51 @@
26 26 }
27 27 }
28 28
29 + /// Write the geometry layer `makeover-geometry` defines into
30 + /// `frontend/css/geometry.css`, which `index.html` links ahead of the
31 + /// stylesheet.
32 + ///
33 + /// Baked at build time rather than applied from `themes.js` the way the intent
34 + /// layer is, because geometry never changes at runtime: no theme may touch it,
35 + /// so there is nothing to re-apply and no second pass over `:root` to pay for
36 + /// on load.
37 + ///
38 + /// The touch preset hangs off `@media (hover: none)` rather than off the
39 + /// 768px breakpoint. Density is about who is operating the app, not how wide
40 + /// the window is: a narrow window on a desktop still has a pointer in it, and
41 + /// a tablet at full width still has a finger. BB happens to already have a
42 + /// `hover: none` block for exactly this reason, so the presets land where the
43 + /// existing touch overrides already live.
44 + ///
45 + /// The file is generated, and gitignored.
46 + fn materialize_geometry() {
47 + use makeover_geometry::{Density, gap_css_overrides, geometry_css_vars};
48 +
49 + let mut css = String::from(
50 + "/* Generated by build.rs from makeover-geometry. Do not edit.\n \
51 + Spacing is named by relationship, not by size. Every token here is\n \
52 + invariant by construction -- the crate exists because none of it may\n \
53 + vary by theme -- which is why they carry no per-line `invariant`\n \
54 + marker the way the :root tokens in styles.css do. */\n",
55 + );
56 + css.push_str(&geometry_css_vars(Density::Pointer));
57 + css.push_str("\n@media (hover: none) {\n");
58 + for line in gap_css_overrides(":root", Density::Touch).lines() {
59 + css.push_str(" ");
60 + css.push_str(line);
61 + css.push('\n');
62 + }
63 + css.push_str("}\n");
64 +
65 + let path = Path::new(env!("CARGO_MANIFEST_DIR"))
66 + .join("frontend")
67 + .join("css")
68 + .join("geometry.css");
69 + std::fs::write(path, css).expect("write geometry.css");
70 + }
71 +
29 72 fn main() {
30 73 materialize_themes();
74 + materialize_geometry();
31 75 tauri_build::build();
32 76 }
@@ -4,6 +4,9 @@
4 4 <meta charset="UTF-8">
5 5 <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
6 6 <title>Balanced Breakfast</title>
7 + <!-- Generated from makeover-geometry by src-tauri/build.rs. First, so the
8 + stylesheet can read --gap-* and --step-* off :root. -->
9 + <link rel="stylesheet" href="css/geometry.css">
7 10 <link rel="stylesheet" href="css/styles.min.css">
8 11 </head>
9 12 <body>
@@ -4,12 +4,15 @@
4 4 Build new UI by COMPOSING the existing vocabulary, in this order of preference:
5 5
6 6 1. Use a UTILITY class for one-off adjustments (.hidden, .sr-only,
7 - .mb-1, .mb-2, .flex-1,
7 + .mb-section, .mb-peer,
8 + .flex-1,
8 9 .text-sm,
9 10 .text-sm-secondary)
10 11 2. Use a LAYOUT PRIMITIVE to position content (.row-flex +
11 - .row-flex-{2,3,4},
12 - .stack + .stack-{2,3,4})
12 + .row-flex-{peer,group,
13 + section}, .stack +
14 + .stack-{peer,group,
15 + section})
13 16 3. Use a COMPONENT PRIMITIVE for a UI element (.btn, .card, .modal,
14 17 .form-input,
15 18 .form-select, .badge,
@@ -31,8 +34,8 @@
31 34 `min-width: 0 !important` to make a row lay out right, the layout itself
32 35 wants a name. See `.condition-row` for the canonical form.
33 36
34 - See `_private/docs/balanced_breakfast/design-system.md` for the primitive
35 - inventory and rules; `styleguide.md` for visual specs.
37 + See the wiki note `bb-design-system` for the primitive inventory and the
38 + charter; `styleguide.md` for visual specs.
36 39
37 40 ----------------------------------------------------------------------------
38 41 FILE STRUCTURE: search by BAND name (uppercase anchors) to navigate.
@@ -121,7 +124,7 @@
121 124 /* === INTENT LAYER (themeable) ===
122 125 Resolved intent tokens applied at runtime by js/themes.js (setProperty on
123 126 :root). Defaults below are "flatwhite" (BB's light default) for first
124 - paint; theme-common resolves every theme incl. the OKLab-derived states,
127 + paint; makeover resolves every theme incl. the OKLab-derived states,
125 128 shared with audiofiles / GoingsOn / MNW. */
126 129 --surface-page: #f1ece4; /* themed */
127 130 --surface-raised: #f7f3ee; /* themed */
@@ -166,16 +169,11 @@
166 169 --shadow-offset: 3px; /* invariant: BB neobrute signature */
167 170 --shadow-brutal: var(--shadow-offset) var(--shadow-offset) 0 var(--shadow-color); /* invariant composition */
168 171
169 - /* Invariant scales (values shared with GO for cross-app portability;
170 - never themed). Per-line `invariant` markers satisfy the lint
171 - theme-token-coverage rule. radius-md intentionally absent (dropped as
172 - a dup of radius-sm). */
173 - --space-1: 0.25rem; /* invariant */
174 - --space-2: 0.5rem; /* invariant */
175 - --space-3: 0.75rem; /* invariant */
176 - --space-4: 1rem; /* invariant */
177 - --space-5: 1.25rem; /* invariant */
178 - --space-6: 1.5rem; /* invariant */
172 + /* Invariant scales (never themed). Per-line `invariant` markers satisfy
173 + the lint theme-token-coverage rule. radius-md intentionally absent
174 + (dropped as a dup of radius-sm).
175 +
176 + Spacing is not in this block any more: see the note under :root. */
179 177 --font-size-xxs: 0.65rem; /* invariant: tiny badges, indicators */
180 178 --font-size-xs: 0.7rem; /* invariant: small badges, counts */
181 179 --font-size-sm: 0.75rem; /* invariant: meta text, timestamps */
@@ -193,6 +191,18 @@
193 191 --transition-normal: 0.15s; /* invariant */
194 192 --transition-slow: 0.3s; /* invariant */
195 193 }
194 + /* Spacing used to live in :root above and no longer does. It was invariant for
195 + the same reason the rest of that block is, which is exactly why it stopped
196 + belonging to this app in particular: it now comes from css/geometry.css,
197 + generated by src-tauri/build.rs from the makeover-geometry crate, and shared
198 + with GoingsOn and audiofiles.
199 +
200 + Reach for a gap token by relationship (bound, peer, group, section, pane,
201 + page); reach for a step token only where no relationship describes the
202 + distance. Every token in that file is invariant by construction, so none
203 + carries the per-line marker the tokens above do, and the file is outside
204 + what theme-token-coverage scans. A theme still overrides colour only, which
205 + is theme contract rule 1, unchanged. */
196 206 /* F5 cleanup (2026-06-02): dropped --bg-surface (themed but never consumed),
197 207 --radius-md (duplicate of --radius-sm), --shadow / --shadow-hover (derived
198 208 but never consumed). themes.js still sets --bg-surface / --shadow / --shadow-hover
@@ -210,17 +220,17 @@
210 220 /* Layout primitives */
211 221 .flex-1 { flex: 1; }
212 222 .stack { display: flex; flex-direction: column; }
213 - .stack-2 { gap: var(--space-2); }
214 - .stack-3 { gap: var(--space-3); }
215 - .stack-4 { gap: var(--space-4); }
223 + .stack-peer { gap: var(--gap-peer); }
224 + .stack-group { gap: var(--gap-group); }
225 + .stack-section { gap: var(--gap-section); }
216 226 .row-flex { display: flex; align-items: center; }
217 - .row-flex-2 { gap: var(--space-2); }
218 - .row-flex-3 { gap: var(--space-3); }
219 - .row-flex-4 { gap: var(--space-4); }
227 + .row-flex-peer { gap: var(--gap-peer); }
228 + .row-flex-group { gap: var(--gap-group); }
229 + .row-flex-section { gap: var(--gap-section); }
220 230
221 231 /* Margin (values mirror GO exactly for drop-in ports) */
222 - .mb-1 { margin-bottom: var(--space-4); }
223 - .mb-2 { margin-bottom: var(--space-2); }
232 + .mb-section { margin-bottom: var(--gap-section); }
233 + .mb-peer { margin-bottom: var(--gap-peer); }
224 234
225 235 /* Text */
226 236 .text-sm { font-size: var(--font-size-sm); }
@@ -253,7 +263,7 @@
253 263 display: flex;
254 264 justify-content: space-between;
255 265 align-items: center;
256 - padding: 0.75rem 1.25rem;
266 + padding: var(--gap-peer) var(--gap-section);
257 267 background-color: var(--surface-overlay);
258 268 border-bottom: var(--border-width) solid var(--border);
259 269 box-shadow: 0 var(--shadow-offset) 0 var(--shadow-color);
@@ -271,12 +281,12 @@
271 281
272 282 .header-actions {
273 283 display: flex;
274 - gap: 0.5rem;
284 + gap: var(--gap-peer);
275 285 align-items: center;
276 286 }
277 287
278 288 .search-input {
279 - padding: 0.4rem 0.75rem;
289 + padding: var(--gap-peer) var(--gap-group);
280 290 border: var(--border-width) solid var(--border);
281 291 border-radius: var(--radius-sm);
282 292 background-color: var(--surface-page);
@@ -310,7 +320,7 @@
310 320 }
311 321
312 322 .sort-select {
313 - padding: 0.4rem 0.75rem;
323 + padding: var(--gap-peer) var(--gap-group);
314 324 border: var(--border-width) solid var(--border);
315 325 border-radius: var(--radius-sm);
316 326 background-color: var(--surface-page);
@@ -333,7 +343,7 @@
333 343 }
334 344
335 345 .sidebar h2 {
336 - padding: 0.75rem 1rem;
346 + padding: var(--gap-peer) var(--gap-group);
337 347 font-size: 0.75rem;
338 348 font-weight: 700;
339 349 color: var(--content-muted);
@@ -357,7 +367,7 @@
357 367 layout (icon / content / actions, flex + gap) plus article appearance +
358 368 read/unread/selected/pinned state. Replaces the legacy .item-* layout. */
359 369 .row--article {
360 - padding: 0.75rem 1.25rem;
370 + padding: var(--gap-peer) var(--gap-section);
361 371 border-bottom: 1px solid var(--border);
362 372 border-left: 3px solid transparent;
363 373 cursor: pointer;
@@ -381,7 +391,7 @@
381 391 .empty-icon {
382 392 display: flex;
383 393 justify-content: center;
384 - margin-bottom: 0.75rem;
394 + margin-bottom: var(--gap-section);
385 395 color: var(--content-muted);
386 396 }
387 397 .empty-icon svg { width: 2.5rem; height: 2.5rem; }
@@ -400,10 +410,10 @@
400 410 align-items: center;
401 411 justify-content: center;
402 412 text-align: center;
403 - padding: 3rem 2rem;
413 + padding: var(--gap-page) var(--gap-pane);
404 414 color: var(--content-muted);
405 415 line-height: 1.6;
406 - gap: 0.75rem;
416 + gap: var(--gap-section);
407 417 }
408 418 .empty-state-icon {
409 419 font-size: 2.5rem;
@@ -415,8 +425,8 @@
415 425 margin: 0;
416 426 }
417 427 .empty-state--compact {
418 - padding: 1.5rem 1rem;
419 - gap: 0.5rem;
428 + padding: var(--gap-pane) var(--gap-group);
429 + gap: var(--gap-peer);
420 430 }
421 431 .empty-state--compact .empty-state-icon { font-size: 1.5rem; }
422 432
@@ -436,8 +446,8 @@
436 446 font-size: 1.1rem;
437 447 color: var(--content-muted);
438 448 transition: color 0.2s, transform 0.15s;
439 - padding: 0.25rem 0.4rem;
440 - margin: -0.25rem -0.4rem;
449 + padding: var(--step-tight) var(--step-snug);
450 + margin: calc(-1 * var(--step-tight)) calc(-1 * var(--step-snug));
441 451 line-height: 1;
442 452 }
443 453 .star-btn:hover { color: var(--category-four); transform: scale(1.1); }
@@ -447,7 +457,7 @@
447 457 width: 7px;
448 458 height: 7px;
449 459 border-radius: 50%;
450 - margin-top: 0.4rem;
460 + margin-top: var(--gap-bound);
451 461 }
452 462 .read-indicator.unread { background-color: var(--danger); }
453 463
@@ -458,7 +468,7 @@
458 468 display: flex;
459 469 justify-content: space-between;
460 470 align-items: flex-start;
461 - margin-bottom: 0.15rem;
471 + margin-bottom: var(--step-hair);
462 472 }
463 473 .article-author {
464 474 font-weight: 700;
@@ -481,11 +491,11 @@
481 491 .row--article .row-secondary {
482 492 color: var(--content-secondary);
483 493 font-size: var(--font-size-md);
484 - margin-top: 0.15rem;
494 + margin-top: var(--step-hair);
485 495 }
486 496 /* .article-source-badge (mobile-only) lives in section 38. */
487 497
488 - .load-more { padding: 1rem; text-align: center; }
498 + .load-more { padding: var(--gap-group); text-align: center; }
489 499
490 500 /* === 8. Detail panel =================================================== */
491 501 .detail-panel {
@@ -501,17 +511,17 @@
501 511 .detail-header {
502 512 display: flex;
503 513 justify-content: flex-end;
504 - gap: 0.3rem;
505 - padding: 0.4rem;
514 + gap: var(--gap-bound);
515 + padding: var(--gap-peer);
506 516 border-bottom: var(--border-width) solid var(--border);
507 517 background-color: var(--surface-sunken);
508 518 }
509 519
510 - .item-detail { padding: 1.25rem; }
520 + .item-detail { padding: var(--gap-group); }
511 521
512 522 .detail-title {
513 523 font-size: 1.15rem;
514 - margin-bottom: 0.75rem;
524 + margin-bottom: var(--gap-section);
515 525 color: var(--content);
516 526 font-weight: 700;
517 527 line-height: 1.4;
@@ -520,19 +530,19 @@
520 530 .detail-meta {
521 531 display: flex;
522 532 flex-wrap: wrap;
523 - gap: 0.6rem;
524 - margin-bottom: 0.75rem;
533 + gap: var(--gap-peer);
534 + margin-bottom: var(--gap-peer);
525 535 color: var(--content-secondary);
526 536 font-size: 0.8rem;
527 - padding-bottom: 0.75rem;
537 + padding-bottom: var(--gap-section);
528 538 border-bottom: var(--border-width) solid var(--border);
529 539 }
530 540
531 541 .detail-tags {
532 542 display: flex;
533 543 flex-wrap: wrap;
534 - gap: 0.4rem;
535 - margin-bottom: 0.75rem;
544 + gap: var(--gap-peer);
545 + margin-bottom: var(--gap-section);
536 546 }
537 547
538 548 /* === 9. Buttons ========================================================
@@ -541,7 +551,7 @@
541 551 Charter target: collapse remaining one-off *-btn classes onto this family.
542 552 ========================================================================== */
543 553 .btn {
544 - padding: 0.4rem 0.75rem;
554 + padding: var(--gap-peer) var(--gap-group);
545 555 border: var(--border-width) solid var(--border);
546 556 border-radius: var(--radius-sm);
547 557 background-color: var(--surface-page);
@@ -568,7 +578,7 @@
568 578 }
569 579 .btn-success:hover { background-color: var(--accent-yellow-light); border-color: var(--accent-yellow-light); }
570 580 /* .btn-sm: canonical small-button size (see design-system.md §Button). */
571 - .btn-sm { padding: 0.2rem 0.5rem; font-size: 0.8rem; }
581 + .btn-sm { padding: var(--gap-bound) var(--gap-peer); font-size: 0.8rem; }
572 582 .btn-sm.active { background: var(--category-four); color: var(--content-on-action); border-color: var(--category-four); }
573 583
574 584 /* .btn-secondary: explicit neutral; today's default `.btn` already plays this
@@ -592,7 +602,7 @@
592 602 /* .btn-icon: square, no padding label space; used for close buttons,
593 603 icon-only toolbar buttons (gear, star, more). */
594 604 .btn-icon {
595 - padding: 0.25rem;
605 + padding: var(--gap-bound);
596 606 line-height: 1;
597 607 min-width: 1.75rem;
598 608 min-height: 1.75rem;
@@ -606,7 +616,7 @@
606 616 .btn-text {
607 617 background: none;
608 618 border: none;
609 - padding: 0.25rem 0.5rem;
619 + padding: var(--gap-bound) var(--gap-peer);
610 620 color: var(--content);
611 621 }
612 622 .btn-text:hover { background-color: var(--surface-overlay); border-color: transparent; }
@@ -634,7 +644,7 @@
634 644 position: absolute;
635 645 top: 50%; left: 50%;
636 646 width: 0.875rem; height: 0.875rem;
637 - margin: -0.4375rem 0 0 -0.4375rem;
647 + margin: -0.4375rem 0 0 -0.4375rem; /* half the spinner, not a gap */
638 648 border: 2px solid currentColor;
639 649 border-top-color: transparent;
640 650 border-radius: 50%;
@@ -661,7 +671,7 @@
661 671 background-color: var(--surface-page);
662 672 border: var(--border-width) solid var(--border);
663 673 border-radius: var(--radius-sm);
664 - padding: 0.75rem;
674 + padding: var(--gap-group);
665 675 box-shadow: var(--shadow-brutal);
666 676 }
667 677 .card--row {
@@ -670,10 +680,10 @@
670 680 border-right: 0;
671 681 border-top: 0;
672 682 box-shadow: none;
673 - padding: 0.6rem 1rem;
683 + padding: var(--gap-peer) var(--gap-group);
674 684 display: flex;
675 685 align-items: center;
676 - gap: 0.75rem;
686 + gap: var(--gap-bound);
677 687 }
678 688 .card--shell {
679 689 background: none;
@@ -696,7 +706,7 @@
696 706 display: flex;
697 707 justify-content: space-between;
698 708 align-items: center;
699 - margin-bottom: 0.5rem;
709 + margin-bottom: var(--gap-peer);
700 710 }
701 711 .card-title {
702 712 font-size: 0.95rem;
@@ -712,8 +722,8 @@
712 722 .card-meta {
713 723 display: flex;
714 724 flex-wrap: wrap;
715 - gap: 0.4rem;
716 - margin-top: 0.5rem;
725 + gap: var(--gap-peer);
726 + margin-top: var(--gap-peer);
717 727 font-size: 0.75rem;
718 728 color: var(--content-muted);
719 729 }
@@ -724,7 +734,7 @@
724 734 .cards-grid {
725 735 display: grid;
726 736 grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
727 - gap: 0.75rem;
737 + gap: var(--gap-group);
728 738 }
729 739
730 740 /* === N. Row primitive (F2-sub addition, 2026-06-02) ===================
@@ -738,7 +748,7 @@
738 748 .row {
739 749 display: flex;
740 750 align-items: flex-start;
741 - gap: 0.75rem;
751 + gap: var(--gap-bound);
742 752 min-width: 0;
743 753 }
744 754 .row-icon {
@@ -758,14 +768,14 @@
758 768 line-height: 1.3;
759 769 display: flex;
760 770 align-items: center;
761 - gap: 0.4rem;
771 + gap: var(--gap-bound);
762 772 flex-wrap: wrap;
763 773 }
764 774 .row-secondary {
765 775 font-size: 0.8rem;
766 776 color: var(--content-secondary);
767 777 line-height: 1.4;
768 - margin-top: 0.15rem;
778 + margin-top: var(--step-hair);
769 779 }
770 780 .row-meta {
771 781 flex-shrink: 0;
@@ -777,7 +787,7 @@
777 787 flex-shrink: 0;
778 788 display: flex;
779 789 align-items: center;
780 - gap: 0.25rem;
790 + gap: var(--gap-peer);
781 791 }
782 792 /* Hover-reveal pattern: actions appear when the row (or any descendant)
783 793 is hovered or focused. Per-surface override with `.row-actions.always`
@@ -804,10 +814,10 @@
804 814 treatment per kind is targetable; add .form-label class on labels;
805 815 add BB.ui.renderFormField helper.
806 816 ========================================================================== */
807 - .form-group { margin-bottom: 0.75rem; }
817 + .form-group { margin-bottom: var(--gap-section); }
808 818 .form-group label {
809 819 display: block;
810 - margin-bottom: 0.35rem;
820 + margin-bottom: var(--gap-bound);
811 821 color: var(--content-secondary);
812 822 font-size: 0.8rem;
813 823 font-weight: 700;
@@ -815,7 +825,7 @@
815 825
816 826 .form-input {
817 827 width: 100%;
818 - padding: 0.6rem;
828 + padding: var(--gap-peer);
819 829 border: var(--border-width) solid var(--border);
820 830 border-radius: var(--radius-sm);
821 831 background-color: var(--surface-page);
@@ -836,14 +846,14 @@
836 846 `<label>` under `.form-group`. */
837 847 .form-label {
838 848 display: block;
839 - margin-bottom: 0.35rem;
849 + margin-bottom: var(--gap-bound);
840 850 color: var(--content-secondary);
841 851 font-size: 0.8rem;
842 852 font-weight: 700;
843 853 }
844 854 .form-select {
845 855 width: 100%;
846 - padding: 0.6rem;
856 + padding: var(--gap-peer);
847 857 border: var(--border-width) solid var(--border);
848 858 border-radius: var(--radius-sm);
849 859 background-color: var(--surface-page);
@@ -859,7 +869,7 @@
859 869 }
860 870 .form-textarea {
861 871 width: 100%;
862 - padding: 0.6rem;
872 + padding: var(--gap-peer);
863 873 border: var(--border-width) solid var(--border);
864 874 border-radius: var(--radius-sm);
865 875 background-color: var(--surface-page);
@@ -878,21 +888,21 @@
878 888 /* .form-row: horizontal grouping of two related fields (e.g. label + button) */
879 889 .form-row {
880 890 display: flex;
881 - gap: 0.5rem;
891 + gap: var(--gap-peer);
882 892 align-items: center;
883 893 }
884 894
885 895 .form-actions {
886 896 display: flex;
887 897 justify-content: flex-end;
888 - gap: 0.5rem;
889 - margin-top: 1rem;
898 + gap: var(--gap-peer);
899 + margin-top: var(--gap-section);
890 900 }
891 901
892 902 .form-hint {
893 903 font-size: 0.7rem;
894 904 color: var(--content-muted);
895 - margin-top: 0.2rem;
905 + margin-top: var(--gap-bound);
896 906 }
897 907
898 908 /* === 11. Modal =========================================================
@@ -924,7 +934,7 @@
924 934 display: flex;
925 935 justify-content: space-between;
926 936 align-items: center;
927 - padding: 0.75rem 1.25rem;
937 + padding: var(--gap-group) var(--gap-pane);
928 938 border-bottom: var(--border-width) solid var(--border);
929 939 background-color: var(--surface-overlay);
930 940 border-radius: var(--radius-lg) var(--radius-lg) 0 0;
@@ -934,8 +944,8 @@
934 944 /* F3 (2026-06-02): step indicator for multi-step modal flows (OAuth,
935 945 plugin import, encryption setup). Set via BB.ui.setModalStep(n, of). */
936 946 .modal-step-indicator {
937 - margin-left: 0.75rem;
938 - padding: 0.1rem 0.5rem;
947 + margin-left: var(--gap-bound);
948 + padding: var(--step-hair) var(--step-base);
939 949 background-color: var(--surface-sunken);
940 950 border: var(--border-width) solid var(--border);
941 951 border-radius: var(--radius-xl);
@@ -945,15 +955,15 @@
945 955 white-space: nowrap;
946 956 }
947 957
948 - .modal-body { padding: 1.25rem; }
958 + .modal-body { padding: var(--gap-pane); }
949 959
950 960 .plugin-list { list-style: none; }
951 961
952 962 .plugin-item {
953 - padding: 0.75rem;
963 + padding: var(--gap-group);
954 964 border: var(--border-width) solid var(--border);
955 965 border-radius: var(--radius-sm);
956 - margin-bottom: 0.5rem;
966 + margin-bottom: var(--gap-peer);
957 967 cursor: pointer;
958 968 transition: all 0.2s;
959 969 background-color: var(--surface-page);
@@ -973,7 +983,7 @@
973 983 (replaces inline `style="margin-bottom:1rem;color:var(--content-secondary)"`
974 984 in feeds.js plugin picker). */
975 985 .modal-intro {
976 - margin-bottom: 1rem;
986 + margin-bottom: var(--gap-section);
977 987 color: var(--content-secondary);
978 988 font-size: 0.875rem;
979 989 }
@@ -991,7 +1001,7 @@
991 1001 flex-direction: column;
992 1002 align-items: center;
993 1003 text-align: center;
994 - padding: 1.5rem 1rem;
1004 + padding: var(--gap-pane) var(--gap-group);
995 1005 color: var(--content-muted);
996 1006 font-size: 0.8rem;
997 1007 line-height: 1.5;
@@ -1000,7 +1010,7 @@
1000 1010 .source-item-empty-icon {
Lines truncated