Skip to main content

max / goingson

Move spacing onto makeover-geometry Spacing was the last part of the design system each app maintained its own copy of. It is invariant -- no theme may touch it -- which is exactly why it belongs to every app equally rather than to this one, so it now comes from the makeover-geometry crate the way colour comes from makeover. build.rs materialises css/geometry.css next to the themes it already writes, carrying the pointer preset in :root and the touch preset scoped to ui-mode-mobile. Baked at build time rather than applied from themes.js: geometry has no picker, so there is nothing to re-apply on load. The sweep is the point, not the plumbing. Every gap, padding and margin now names what it separates instead of how big it is: --gap-bound for a control and the thing it belongs to, --gap-peer for items of a kind, up through --gap-page for the outer shell. --step-* only survives where no relationship describes the distance, which is almost entirely inline padding on badges and kbd. The local --space-1..8 scale is gone, along with the size-named layout utilities, which are now .stack-peer / .row-flex-section and so on. Fourteen ui-mode-mobile rules are deleted and six trimmed. They existed to shave a value for touch, and the touch preset does that for every gap at once. The mobile rules that reflow a layout kept their spacing and took tokens. Layout is deliberately not preserved. The HIG vocabulary the crate translates is tighter than the linear 4px scale it replaces, so containers lost some breathing room; that is the direction, and wave 1 of the Platinum note is where it gets eyeballed.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-27 02:02 UTC
Signed with PGP, not checked
Commit: a1012ebe8dc9b73215c6af30d24b0ad806132785
Parent: 7744e11
24 files changed, +178 insertions, -128 deletions
M .gitignore +3
@@ -45,3 +45,6 @@
45 45
46 46 # Generated by src-tauri/build.rs from makeover's embedded theme set
47 47 /src-tauri/themes/
48 +
49 + # Generated by src-tauri/build.rs from makeover-geometry's scale
50 + /src-tauri/frontend/css/geometry.css
M Cargo.lock +7
@@ -2276,6 +2276,7 @@
2276 2276 "libsqlite3-sys",
2277 2277 "mailparse",
2278 2278 "makeover",
2279 + "makeover-geometry",
2279 2280 "notify",
2280 2281 "notify-debouncer-mini",
2281 2282 "open",
@@ -3426,6 +3427,12 @@
3426 3427 "toml 1.1.3+spec-1.1.0",
3427 3428 ]
3428 3429
3430 + [[package]]
3431 + name = "makeover-geometry"
3432 + version = "0.1.0"
3433 + source = "registry+https://github.com/rust-lang/crates.io-index"
3434 + checksum = "9d8b94e51333489b0a79bb63ad4100ca49726f2048e2477f6ee3d6dd822f9327"
3435 +
3429 3436 [[package]]
3430 3437 name = "maplit"
3431 3438 version = "1.0.2"
M Cargo.toml +3
@@ -82,6 +82,9 @@
82 82 notify = "8.2"
83 83 notify-debouncer-mini = "0.7"
84 84 makeover = "2.1.1"
85 + # The invariant half of the design system. makeover resolves colour, which a
86 + # theme may override; this carries spacing, which no theme may touch.
87 + makeover-geometry = "0.1.0"
85 88 alloy_tui = "2.0.0"
86 89 toml = "1.1"
87 90
@@ -21,7 +21,7 @@
21 21 | Border | `--border-width` (2px), `--border-width-sm`, `--border-color`, `--border-light` | `--border-color` themeable; widths invariant |
22 22 | Shadow | `--shadow-offset-xs/sm/md/lg/xl`, `--shadow-brutal-xs/md/lg/xl` | Theme-invariant (Neobrute signature) |
23 23 | Radius | `--radius-xs/sm/md/lg/xl/full` | Invariant |
24 - | Spacing | `--space-1` ... `--space-6` (0.25 → 1.5rem) | Invariant |
24 + | Spacing | `--gap-bound/peer/group/section/pane/page` over `--step-hair` ... `--step-colossal` | Invariant, and not in this file: `css/geometry.css`, generated by `src-tauri/build.rs` from the `makeover-geometry` crate |
25 25 | Type size | `--font-size-xxs` ... `--font-size-4xl` | Invariant |
26 26 | Line height | `--line-height-tight/normal/relaxed` | Invariant |
27 27 | Font family | `--font-sans`, `--font-serif`, `--font-mono`, `--font-display` | Invariant |
@@ -15,6 +15,7 @@
15 15 [build-dependencies]
16 16 tauri-build = { workspace = true }
17 17 makeover.workspace = true
18 + makeover-geometry.workspace = true
18 19
19 20 [dependencies]
20 21 goingson-core = { workspace = true }
@@ -26,7 +26,42 @@
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 JS the way themes are, because
34 + /// geometry never changes at runtime: there is no geometry picker, so nothing
35 + /// to re-apply and no reason to pay for a second pass over `:root` on load.
36 + ///
37 + /// The desktop preset goes in `:root`; the touch preset overrides only the
38 + /// relational layer, scoped to the `ui-mode-mobile` class `bootstrap-uimode.js`
39 + /// puts on `<html>` before the stylesheet evaluates. Both selectors match the
40 + /// root element with the same specificity, so the override has to come second,
41 + /// which is the order these are written in.
42 + ///
43 + /// The file is generated, and gitignored.
44 + fn materialize_geometry() {
45 + use makeover_geometry::{Density, gap_css_overrides, geometry_css_vars};
46 +
47 + let mut css = String::from(
48 + "/* Generated by build.rs from makeover-geometry. Do not edit.\n \
49 + Spacing is named by relationship, not by size: see the crate's README\n \
50 + and wiki note makeover-geometry. */\n",
51 + );
52 + css.push_str(&geometry_css_vars(Density::Pointer));
53 + css.push('\n');
54 + css.push_str(&gap_css_overrides(".ui-mode-mobile", Density::Touch));
55 +
56 + let path = Path::new(env!("CARGO_MANIFEST_DIR"))
57 + .join("frontend")
58 + .join("css")
59 + .join("geometry.css");
60 + std::fs::write(path, css).expect("write geometry.css");
61 + }
62 +
29 63 fn main() {
30 64 materialize_themes();
65 + materialize_geometry();
31 66 tauri_build::build();
32 67 }
@@ -4,6 +4,7 @@
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>Compose Email</title>
7 + <link rel="stylesheet" href="css/geometry.css">
7 8 <link rel="stylesheet" href="css/styles.min.css">
8 9 </head>
9 10 <body class="compose-window">
@@ -28,6 +28,9 @@
28 28 phase 5 of ui_mode_separation_plan.md. -->
29 29 <script src="js/bootstrap-uimode.js"></script>
30 30
31 + <!-- Generated from makeover-geometry by src-tauri/build.rs. First, so the
32 + stylesheet can read --gap-* and --step-* off :root. -->
33 + <link rel="stylesheet" href="css/geometry.css">
31 34 <link rel="stylesheet" href="css/styles.min.css">
32 35 </head>
33 36 <body>
@@ -50,7 +53,7 @@
50 53 </a>
51 54 </nav>
52 55 <div class="header-actions">
53 - <button class="sync-indicator row-flex row-flex-2 hidden" id="sync-indicator" data-act="settings.openCloudSync" title="Cloud Sync" aria-label="Cloud sync status">
56 + <button class="sync-indicator row-flex row-flex-peer hidden" id="sync-indicator" data-act="settings.openCloudSync" title="Cloud Sync" aria-label="Cloud sync status">
54 57 <span class="sync-dot" id="sync-dot"></span>
55 58 <span class="sync-label" id="sync-label"></span>
56 59 </button>
@@ -73,7 +76,7 @@
73 76 <div id="tasks-view" class="subview">
74 77 <div class="page-header">
75 78 <h2 class="page-title">Tasks</h2>
76 - <div class="row-flex row-flex-4">
79 + <div class="row-flex row-flex-section">
77 80 <div class="view-toggle" id="task-view-toggle">
78 81 <button class="view-toggle-btn active" data-mode="list" data-act="tasks.setViewMode" data-a1="list">List</button>
79 82 <button class="view-toggle-btn" data-mode="board" data-act="tasks.setViewMode" data-a1="board">Board</button>
@@ -220,7 +223,7 @@
220 223 <!-- Project Dashboard Sub-View -->
221 224 <div id="project-dashboard-view" class="subview hidden">
222 225 <div class="page-header">
223 - <div class="row-flex row-flex-4">
226 + <div class="row-flex row-flex-section">
224 227 <button class="btn btn-secondary" data-act="projects.closeDashboard">&larr; Back</button>
225 228 <h2 class="page-title" id="project-dashboard-title">Project</h2>
226 229 </div>
@@ -270,7 +273,7 @@
270 273 <!-- Task Overview Sub-View -->
271 274 <div id="task-overview-view" class="subview hidden">
272 275 <div class="page-header">
273 - <div class="row-flex row-flex-4">
276 + <div class="row-flex row-flex-section">
274 277 <button class="btn btn-secondary" data-act="taskOverview.close">&larr; Back</button>
275 278 <h2 class="page-title" id="task-overview-title">Task Overview</h2>
276 279 </div>
@@ -467,12 +470,12 @@
467 470 <div id="emails-view" class="subview" role="tabpanel" aria-labelledby="emails-tab">
468 471 <div class="page-header">
469 472 <h2 class="page-title">Emails</h2>
470 - <div class="row-flex row-flex-2">
473 + <div class="row-flex row-flex-peer">
471 474 <button class="btn btn-secondary mobile-hide" data-act="emails.openDrafts">Drafts</button>
472 475 <button class="btn btn-primary" data-act="emails.openCompose" title="Compose email (n)">+ Compose</button>
473 476 </div>
474 477 </div>
475 - <div class="email-filter-row mb-2">
478 + <div class="email-filter-row mb-peer">
476 479 <input type="text" class="form-input" id="email-search" placeholder="Search emails..." data-input="emails.search">
477 480 <select class="form-select" id="email-folder-filter" data-change="emails.filterByFolder">
478 481 <option value="">All folders</option>
@@ -538,7 +541,7 @@
538 541 <!-- Contact Dashboard Sub-View -->
539 542 <div id="contact-dashboard-view" class="subview hidden">
540 543 <div class="page-header">
541 - <div class="row-flex row-flex-4">
544 + <div class="row-flex row-flex-section">
542 545 <button class="btn btn-secondary" data-act="contactDashboard.close">&larr; Back</button>
543 546 <h2 class="page-title" id="contact-dashboard-title">Contact</h2>
544 547 </div>
@@ -564,7 +567,7 @@
564 567 <div id="settings-view" class="settings-overlay-card">
565 568 <div class="settings-page-layout">
566 569 <nav class="settings-sidebar">
567 - <button class="btn-link mb-1 settings-back" data-act="settings.goBack" aria-label="Close settings">&larr; Close</button>
570 + <button class="btn-link mb-section settings-back" data-act="settings.goBack" aria-label="Close settings">&larr; Close</button>
568 571 <h2 class="sr-only" id="settings-overlay-title">Settings</h2>
569 572 <div class="settings-nav-items">
570 573 <button class="settings-nav-item active" data-section="appearance" data-act="settings.showSection" data-a1="appearance">Appearance</button>
@@ -3,10 +3,11 @@
3 3
4 4 Build new UI by COMPOSING the existing vocabulary, in this order of preference:
5 5
6 - 1. Use a UTILITY class for one-off adjustments (.mb-1, .mb-2, .flex-1,
6 + 1. Use a UTILITY class for one-off adjustments (.mb-section, .mb-peer, .flex-1,
7 7 .text-sm-secondary, .hidden)
8 - 2. Use a LAYOUT PRIMITIVE to position content (.row-flex + .row-flex-{2,3,4},
9 - .row, .stack-{2,3,4})
8 + 2. Use a LAYOUT PRIMITIVE to position content (.row-flex + .row-flex-{peer,
9 + group,section}, .row,
10 + .stack-{peer,group,section})
10 11 3. Use a COMPONENT PRIMITIVE for a UI element (.card, .btn, .form-input,
11 12 .form-select, .modal, .badge,
12 13 .tag, .toggle-switch)
@@ -265,9 +266,16 @@
265 266
266 267 /* --- APP-LOCAL (not theme intents)
267 268 GoingsOn's component CSS references the intent tokens above directly;
268 - no brand aliases. Geometry stays app-local by contract: a theme
269 - overrides color only, so border width, bevel thickness, radius,
270 - density and type never leave this file. */
269 + no brand aliases. None of this is themeable: a theme overrides color
270 + only, so border width, bevel thickness, radius and type stay here and
271 + no theme can reach them.
272 +
273 + Spacing used to live here too and no longer does. It is the same
274 + contract from the other side -- invariant, so it belongs to every app
275 + equally rather than to this one -- and it now comes from
276 + makeover-geometry via css/geometry.css, which build.rs generates.
277 + Use --gap-* by relationship; --step-* only where no relationship
278 + describes the distance. */
271 279 --border-width: 1px;
272 280 --border-width-sm: 1px;
273 281
@@ -311,19 +319,8 @@
311 319 --width-modal: 560px;
312 320 --width-sidebar: 280px;
313 321
314 - /* Spacing scale */
315 - --space-1: 0.25rem;
316 - --space-2: 0.5rem;
317 - --space-3: 0.75rem;
318 - --space-4: 1rem;
319 - --space-5: 1.25rem;
320 - --space-6: 1.5rem;
321 - /* The tail is coarser on purpose. Steps 1 to 6 climb by 0.25rem because
322 - that is the granularity UI density needs; above 1.5rem the values are
323 - layout gutters and page shells, where a 4px distinction is noise.
324 - 1.75rem and 2.5rem each had a single site and snap into this. */
325 - --space-7: 2rem;
326 - --space-8: 3rem;
322 + /* Spacing: see css/geometry.css. --gap-bound, --gap-peer, --gap-group,
323 + --gap-section, --gap-pane, --gap-page, over a --step-* scale. */
327 324
328 325 --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
329 326 --font-serif: Georgia, 'Times New Roman', serif;
@@ -365,16 +362,16 @@
365 362 .text-sm-secondary { font-size: 0.875rem; color: var(--content-secondary); }
366 363 .text-xs-secondary { font-size: 0.75rem; color: var(--content-secondary); }
367 364 .text-accent-red { color: var(--danger); }
368 - .mb-1 { margin-bottom: 1rem; }
369 - .mb-2 { margin-bottom: var(--space-2); }
365 + .mb-section { margin-bottom: var(--gap-section); }
366 + .mb-peer { margin-bottom: var(--gap-peer); }
370 367
371 368 /* --- FORM SIZING UTILITIES */
372 369 .form-select--compact { width: auto; min-width: 120px; }
373 370
374 371 /* --- SETTINGS UTILITIES */
375 - .settings-divider { margin-top: var(--space-6); padding-top: var(--space-6); border-top: var(--border-width) solid var(--border); }
376 - .settings-heading { margin-bottom: 1rem; font-family: var(--font-heading); }
377 - .settings-desc { font-size: 0.875rem; color: var(--content-secondary); margin-bottom: 1rem; }
372 + .settings-divider { margin-top: var(--gap-pane); padding-top: var(--gap-pane); border-top: var(--border-width) solid var(--border); }
373 + .settings-heading { margin-bottom: var(--gap-section); font-family: var(--font-heading); }
374 + .settings-desc { font-size: 0.875rem; color: var(--content-secondary); margin-bottom: var(--gap-section); }
378 375
379 376 /* --- SUBTASK UTILITIES */
380 377 /* .subtask-item is the base row; --linked is a modifier for subtasks that
@@ -383,11 +380,11 @@
383 380 .subtask-item {
384 381 display: flex;
385 382 align-items: center;
386 - gap: 0.5rem;
387 - padding: 0.5rem;
383 + gap: var(--gap-bound);
384 + padding: var(--gap-peer);
388 385 background: var(--surface-overlay);
389 386 border-radius: var(--radius-sm);
390 - margin-bottom: 0.5rem;
387 + margin-bottom: var(--gap-peer);
391 388 }
392 389 .subtask-item--linked {
393 390 background: var(--surface-sunken);
@@ -445,7 +442,7 @@
445 442 .ui-mode-desktop .app-header {
446 443 background: var(--surface-raised);
447 444 border-bottom: var(--border-width) solid var(--border);
448 - padding: var(--space-3) var(--space-6);
445 + padding: var(--gap-group) var(--gap-pane);
449 446 display: flex;
450 447 justify-content: space-between;
451 448 align-items: center;
@@ -454,13 +451,13 @@
454 451 .ui-mode-desktop .header-content {
455 452 display: flex;
456 453 align-items: center;
457 - gap: 0.75rem;
454 + gap: var(--gap-bound);
458 455 }
459 456
460 457 .ui-mode-desktop .header-actions {
461 458 display: flex;
462 459 align-items: center;
463 - gap: 0.5rem;
460 + gap: var(--gap-peer);
464 461 }
465 462
466 463 .ui-mode-desktop .app-title {
@@ -487,14 +484,14 @@
487 484 .ui-mode-desktop .tab-navigation {
488 485 display: flex;
489 486 justify-content: center;
490 - gap: 0.5rem;
487 + gap: var(--gap-peer);
491 488 }
492 489
493 490 .ui-mode-desktop .tab {
494 491 display: flex;
495 492 align-items: center;
496 - gap: 0.5rem;
497 - padding: 0.75rem 1.25rem;
493 + gap: var(--gap-bound);
494 + padding: var(--gap-peer) var(--gap-section);
498 495 text-decoration: none;
499 496 color: var(--content);
500 497 background: var(--surface-raised);
@@ -537,14 +534,14 @@
537 534 .ui-mode-desktop .pill-nav {
538 535 display: flex;
539 536 align-items: center;
540 - gap: var(--space-1);
537 + gap: var(--gap-peer);
541 538 padding: 0;
542 - margin-bottom: 1rem;
539 + margin-bottom: var(--gap-section);
543 540 min-height: 2rem;
544 541 }
545 542
546 543 .ui-mode-desktop .pill {
547 - padding: var(--space-1) var(--space-3);
544 + padding: var(--gap-bound) var(--gap-section);
548 545 border-radius: var(--radius-xl);
549 546 border: var(--border-width-sm) solid var(--border);
550 547 background: var(--surface-raised);
@@ -572,7 +569,7 @@
572 569 max-width: var(--width-container);
573 570 width: 100%;
574 571 margin: 0 auto;
575 - padding: var(--space-6) var(--space-7) var(--space-7);
572 + padding: var(--gap-pane) var(--gap-page) var(--gap-page);
576 573 }
577 574
578 575 /* Page Header */
@@ -580,8 +577,8 @@
580 577 display: flex;
581 578 justify-content: space-between;
582 579 align-items: center;
583 - gap: 0.5rem;
584 - margin-bottom: 1rem;
580 + gap: var(--gap-section);
581 + margin-bottom: var(--gap-section);
585 582 }
586 583
587 584 .page-title {
@@ -595,7 +592,7 @@
595 592 .page-header-actions {
596 593 display: flex;
597 594 align-items: center;
598 - gap: var(--space-3);
595 + gap: var(--gap-peer);
599 596 }
600 597
601 598 /* --- Merged pill + page-header toolbar --- */
@@ -620,7 +617,7 @@
620 617 #project-dashboard-view > .page-header,
621 618 #day-plan-view > .page-header {
622 619 position: static;
623 - margin-bottom: 1rem;
620 + margin-bottom: var(--gap-section);
624 621 }
625 622
626 623 #project-dashboard-view .page-title {
@@ -632,8 +629,8 @@
632 629 display: inline-flex;
633 630 align-items: center;
634 631 justify-content: center;
635 - gap: 0.5rem;
636 - padding: 0.625rem 1.25rem;
632 + gap: var(--gap-bound);
633 + padding: var(--gap-peer) var(--gap-section);
637 634 border: var(--border-width) solid var(--border);
638 635 border-radius: var(--radius-sm);
639 636 font-size: 0.9rem;
@@ -704,7 +701,7 @@
704 701 }
705 702
706 703 .btn-sm {
707 - padding: 0.375rem 0.75rem;
704 + padding: var(--gap-bound) var(--gap-group);
708 705 font-size: 0.8rem;
709 706 }
710 707
@@ -716,7 +713,7 @@
716 713 .cards-grid {
717 714 display: grid;
718 715 grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
719 - gap: 1.25rem;
716 + gap: var(--gap-group);
720 717 }
721 718
722 719 .cards-grid-note,
@@ -737,7 +734,7 @@
737 734 background-color: var(--surface-raised);
738 735 border: var(--border-width) solid var(--border);
739 736 border-radius: var(--radius-md);
740 - padding: 1.25rem;
737 + padding: var(--gap-group);
741 738 box-shadow: var(--bevel-raised);
742 739 cursor: pointer;
743 740 }
@@ -755,8 +752,8 @@
755 752 /* Compact card for use in dense vertical lists (e.g. Project Dashboard).
756 753 Tighter padding, adds list spacing. */
757 754 .card--list-item {
758 - padding: 0.75rem;
759 - margin-bottom: 0.5rem;
755 + padding: var(--gap-peer);
756 + margin-bottom: var(--gap-peer);
760 757 }
761 758
762 759 /* Container shell, keeps card chrome (border/radius/shadow) but strips
@@ -805,7 +802,7 @@
805 802 display: flex;
806 803 justify-content: space-between;
807 804 align-items: flex-start;
808 - margin-bottom: 0.75rem;
805 + margin-bottom: var(--gap-section);
809 806 }
810 807
811 808 .card-title {
@@ -818,7 +815,7 @@
818 815 .card-description {
819 816 font-size: 0.9rem;
820 817 color: var(--content-secondary);
821 - margin-bottom: 1rem;
818 + margin-bottom: var(--gap-section);
822 819 }
823 820
824 821 .markdown-content {
@@ -841,7 +838,7 @@
841 838
842 839 .card-meta {
843 840 display: flex;
844 - gap: 0.5rem;
841 + gap: var(--gap-peer);
845 842 flex-wrap: wrap;
846 843 }
847 844
@@ -850,7 +847,7 @@
850 847 .tag, .badge {
851 848 display: inline-flex;
852 849 align-items: center;
853 - padding: 0.25rem 0.625rem;
850 + padding: var(--step-tight) var(--step-roomy);
854 851 border: var(--border-width-sm) solid var(--border);
855 852 border-radius: var(--radius-sm);
856 853 font-size: 0.8125rem;
@@ -891,8 +888,8 @@
891 888 }
892 889
893 890 /* Size modifiers */
894 - .badge--xs { padding: 0.1rem 0.5rem; font-size: 0.75rem; }
895 - .badge--sm { padding: 0.15rem 0.55rem; font-size: 0.78rem; }
891 + .badge--xs { padding: var(--step-hair) var(--step-base); font-size: 0.75rem; }
892 + .badge--sm { padding: var(--step-hair) var(--step-roomy); font-size: 0.78rem; }
896 893
897 894 /* Filled intent, solid accent fill, no border. Flat by intent, so no bevel
898 895 either: the fill is the signal and an edge would fight it. Pairs with
@@ -913,8 +910,8 @@
913 910 font-family: var(--font-mono, monospace);
914 911 font-size: 0.65rem;
915 912 font-weight: 600;
916 - padding: 0.1rem 0.35rem;
917 - margin-left: 0.35rem;
913 + padding: var(--step-hair) var(--step-snug);
914 + margin-left: var(--gap-bound);
918 915 border: var(--border-width) solid currentColor;
919 916 border-radius: var(--radius-xs);
920 917 opacity: 0.6;
@@ -951,8 +948,8 @@
951 948 /* Inline marker on a task row whose project is shared. */
952 949 .shared-badge {
953 950 display: inline-block;
954 - margin-left: var(--space-2);
955 - padding: 0 var(--space-2);
951 + margin-left: var(--gap-bound);
952 + padding: 0 var(--step-base);
956 953 border-radius: var(--radius-sm);
957 954 font-size: var(--font-size-xs);
958 955 line-height: 1.4;
@@ -975,7 +972,7 @@
975 972
976 973 .data-table th,
977 974 .data-table td {
978 - padding: 1rem 1.25rem;
975 + padding: var(--gap-peer) var(--gap-section);
979 976 text-align: left;
980 977 border-bottom: var(--border-width) solid var(--border);
981 978 }
@@ -1025,18 +1022,18 @@
1025 1022 (a single-letter H/M/L body) is widened just enough to fit "Priority". */
1026 1023 grid-template-columns: minmax(200px, 1fr) 140px 80px 110px 90px 100px 90px;
1027 1024 align-items: center;
1028 - gap: 0.75rem;
1025 + gap: var(--gap-group);
1029 1026 }
1030 1027
1031 1028 /* Task Header Row */
1032 1029 .task-header-row {
1033 1030 background-color: var(--surface-overlay);
1034 1031 border-bottom: var(--border-width) solid var(--border);
1035 - padding: 0 1.25rem;
1032 + padding: 0 var(--gap-section);
1036 1033 }
1037 1034
1038 1035 .task-header-row .task-cell {
1039 - padding: 0.75rem 0;
1036 + padding: var(--gap-peer) 0;
1040 1037 font-size: 0.8rem;
1041 1038 font-weight: 700;
1042 1039 text-transform: uppercase;
@@ -1054,7 +1051,7 @@
1054 1051
1055 1052 /* Task Row */
1056 1053 .task-row {
1057 - padding: 0.75rem 1.25rem;
1054 + padding: var(--gap-peer) var(--gap-section);
1058 1055 border-bottom: var(--border-width) solid var(--border);
1059 1056 cursor: pointer;
1060 1057 }
@@ -1097,7 +1094,7 @@
1097 1094 display: flex;
1098 1095 flex-wrap: wrap;
1099 1096 align-items: center;
1100 - gap: 0.25rem 0.5rem;
1097 + gap: var(--gap-bound) var(--gap-peer);
1101 1098 }
1102 1099
1103 1100 .task-description-text {
@@ -1119,7 +1116,7 @@
1119 1116 .priority-medium,
1120 1117 .priority-low {
1121 1118 display: inline-block;
1122 - padding: 0.25rem 0.5rem;
1119 + padding: var(--step-tight) var(--step-base);
1123 1120 border-radius: var(--radius-xs);
1124 1121 font-weight: 700;
1125 1122 text-align: center;
@@ -1156,7 +1153,7 @@
1156 1153 .sort-arrow {
1157 1154 display: inline-block;
1158 1155 width: 0.8em;
1159 - margin-left: 0.25rem;
1156 + margin-left: var(--gap-bound);
1160 1157 opacity: 0.3;
1161 1158 }
1162 1159
@@ -1189,14 +1186,14 @@
1189 1186
1190 1187 .task-tags {
1191 1188 display: flex;
1192 - gap: 0.25rem;
1189 + gap: var(--gap-peer);
1193 1190 flex-wrap: wrap;
1194 1191 }
1195 1192
1196 1193 .task-tag {
1197 1194 background-color: var(--surface-sunken);
1198 1195 color: var(--content);
1199 - padding: 0.125rem 0.5rem;
1196 + padding: var(--step-hair) var(--step-base);
1200 1197 border-radius: var(--radius-xs);
1201 1198 font-size: 0.75rem;
1202 1199 font-weight: 600;
@@ -1212,7 +1209,7 @@
1212 1209 .annotation-badge {
1213 1210 background-color: var(--warning);
1214 1211 color: var(--content);
1215 - padding: 0.125rem 0.5rem;
1212 + padding: var(--step-hair) var(--step-base);
1216 1213 border-radius: var(--radius-xs);
1217 1214 font-size: 0.7rem;
1218 1215 font-weight: 700;
@@ -1222,12 +1219,12 @@
1222 1219 .subtask-badge {
1223 1220 background-color: var(--surface-overlay);
1224 1221 color: var(--content);
1225 - padding: 0.125rem 0.5rem;
1222 + padding: var(--step-hair) var(--step-base);
1226 1223 border-radius: var(--radius-xs);
1227 1224 font-size: 0.7rem;
1228 1225 font-weight: 700;
1229 1226 border: var(--border-width-sm) solid var(--border);
1230 - margin-left: 0.25rem;
1227 + margin-left: var(--gap-bound);
1231 1228 }
1232 1229
1233 1230 .task-pending { }
@@ -1246,7 +1243,7 @@
1246 1243 font-weight: 700;
1247 1244 background: #fde8ea; /* Fallback for browsers without color-mix() */
1248 1245 background: color-mix(in srgb, var(--danger) 15%, var(--surface-raised));
1249 - padding: 0.25rem 0.5rem;
1246 + padding: var(--step-tight) var(--step-base);
1250 1247 border-radius: var(--radius-xs);
1251 1248 }
1252 1249 .due-today {
@@ -1254,7 +1251,7 @@
1254 1251 font-weight: 700;
1255 1252 background: #fef8e6; /* Fallback for browsers without color-mix() */
1256 1253 background: color-mix(in srgb, var(--warning) 15%, var(--surface-raised));
1257 - padding: 0.25rem 0.5rem;
1254 + padding: var(--step-tight) var(--step-base);
1258 1255 border-radius: var(--radius-xs);
1259 1256 }
1260 1257 .due-soon { color: var(--content-secondary); }
@@ -1266,7 +1263,7 @@
1266 1263 flex-direction: column;
1267 1264 flex: 1;
1268 1265 min-height: 0;
1269 - gap: 1rem;
1266 + gap: var(--gap-peer);
1270 1267 }
1271 1268
1272 1269 /* Event Table - CSS Grid based for virtual scrolling */
@@ -1289,7 +1286,7 @@
1289 1286 display: grid;
1290 1287 grid-template-columns: 100px 80px 1fr 150px 40px;
1291 1288 align-items: center;
1292 - gap: 0.5rem;
1289 + gap: var(--gap-group);
1293 1290 }
1294 1291
1295 1292 /* Event Header Row */
@@ -1300,7 +1297,7 @@
1300 1297 }
1301 1298
1302 1299 .event-header-row .event-cell {
1303 - padding: 1rem 1.25rem;
1300 + padding: var(--gap-peer) var(--gap-section);
1304 1301 font-size: 0.8rem;
1305 1302 font-weight: 700;
1306 1303 text-transform: uppercase;
@@ -1318,7 +1315,7 @@
1318 1315
1319 1316 /* Event Row */
1320 1317 .event-row-virtual {
1321 - padding: 0.75rem 1.25rem;
1318 + padding: var(--gap-peer) var(--gap-section);
1322 1319 border-bottom: var(--border-width) solid var(--border);
1323 1320 cursor: pointer;
1324 1321 }
@@ -1354,19 +1351,19 @@
1354 1351 font-weight: 700;
1355 1352 font-size: 0.9rem;
1356 1353 color: var(--content);
1357 - margin-right: 0.5rem;
1354 + margin-right: var(--gap-bound);
1358 1355 }
1359 1356
1360 1357 .event-date-badge {
1361 1358 display: inline-block;
1362 - padding: 0.15rem 0.4rem;
1359 + padding: var(--step-hair) var(--step-snug);
1363 1360 background: var(--success);
1364 1361 color: var(--content-on-action);
1365 1362 font-size: 0.7rem;
1366 1363 font-weight: 700;
1367 1364 text-transform: uppercase;
1368 1365 border-radius: var(--radius-xs);
1369 - margin-right: 0.5rem;
1366 + margin-right: var(--gap-bound);
1370 1367 }
1371 1368
1372 1369
@@ -1410,21 +1407,21 @@
1410 1407
1411 1408 .no-upcoming-events {
1412 1409 text-align: center;
1413 - padding: var(--space-7);
1410 + padding: var(--gap-pane);
1414 1411 color: var(--content-secondary);
1415 1412 font-style: italic;
1416 1413 }
1417 1414
1418 1415 /* Past Events Collapsible Section */
1419 1416 .past-events-section {
1420 - margin-top: 0.5rem;
1417 + margin-top: var(--gap-section);
1421 1418 }
1422 1419
1423 1420 .past-events-toggle {
1424 1421 display: flex;
1425 1422 align-items: center;
1426 - gap: 0.75rem;
1427 - padding: 0.75rem 1rem;
1423 + gap: var(--gap-bound);
1424 + padding: var(--gap-peer) var(--gap-group);
1428 1425 background: var(--surface-overlay);
1429 1426 border: var(--border-width-sm) solid var(--border);
1430 1427 border-radius: var(--radius-sm);
@@ -1460,12 +1457,12 @@
1460 1457 background: var(--content-muted);
1461 1458 color: var(--content-on-action);
1462 1459 font-size: 0.75rem;
1463 - padding: 0.15rem 0.5rem;
1460 + padding: var(--step-hair) var(--step-base);
1464 1461 border-radius: var(--radius-sm);
1465 1462 }
1466 1463
1467 1464 .past-events-section .event-table-past {
1468 - margin-top: 0.75rem;
1465 + margin-top: var(--gap-peer);
1469 1466 opacity: 0.85;
1470 1467 }
1471 1468
@@ -1478,7 +1475,7 @@
1478 1475 and isn't dimmed. */
1479 1476 #recurring-events-section .event-table-virtual {
1480 1477 opacity: 1;
1481 - margin-top: 0.75rem;
1478 + margin-top: var(--gap-peer);
1482 1479 }
Lines truncated
@@ -404,7 +404,7 @@
404 404 return;
405 405 }
406 406 const items = attachedFiles.map((f, i) => `
407 - <div class="compose-attachment-item row-flex row-flex-2">
407 + <div class="compose-attachment-item row-flex row-flex-peer">
408 408 <span class="compose-attachment-name" title="${escAttr(f.path)}">${esc(f.name)}</span>
409 409 <span class="compose-attachment-size">${formatBytes(f.size || 0)}</span>
410 410 <button type="button" class="compose-attachment-remove" data-compose-remove-attachment="${i}" title="Remove">&times;</button>
@@ -65,7 +65,7 @@
65 65 else if (item.kind === 'email') act = 'emails.open';
66 66
67 67 return `
68 - <div class="contact-timeline-item row-flex row-flex-3" ${act ? `data-act="${act}" data-a1="${escAttr(item.id)}"` : ''} role="button" tabindex="0">
68 + <div class="contact-timeline-item row-flex row-flex-group" ${act ? `data-act="${act}" data-a1="${escAttr(item.id)}"` : ''} role="button" tabindex="0">
69 69 <span class="contact-timeline-icon">${icon}</span>
70 70 <span class="contact-timeline-title">${esc(item.title)}</span>
71 71 ${badge}
@@ -102,8 +102,8 @@
102 102 <div class="avatar avatar--lg">${esc(initials)}</div>
103 103 <div class="contact-header-info">
104 104 <h3 style="margin: 0;">${esc(name)}</h3>
105 - ${companyTitle ? `<div style="margin-top: 0.25rem;">${companyTitle}</div>` : ''}
106 - ${tags ? `<div style="margin-top: 0.5rem;">${tags}</div>` : ''}
105 + ${companyTitle ? `<div style="margin-top: var(--gap-bound);">${companyTitle}</div>` : ''}
106 + ${tags ? `<div style="margin-top: var(--gap-peer);">${tags}</div>` : ''}
107 107 </div>
108 108 </div>
109 109 `;
@@ -684,11 +684,11 @@
684 684 </div>
685 685 ${customFieldSummary}
686 686 </div>
687 - <div style="margin-top: 0.5rem;">
687 + <div style="margin-top: var(--gap-peer);">
688 688 <button type="button" class="btn btn-sm btn-secondary" data-act="contacts.open" data-a1="${escAttr(id)}">View Full Detail</button>
689 689 </div>
690 690 </div>
691 - <div style="margin-top: 1rem;">
691 + <div style="margin-top: var(--gap-section);">
692 692 <button type="button" class="btn btn-danger" data-act="contacts.deleteContact" data-a1="${escAttr(id)}">Delete Contact</button>
693 693 </div>
694 694 `,
@@ -160,7 +160,7 @@
160 160 </div>
161 161
162 162 <div id="paint-time-preview" class="form-hint paint-time-preview"></div>
163 - <div class="form-row" style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;">
163 + <div class="form-row" style="display: grid; grid-template-columns: 1fr 1fr; gap: var(--gap-group);">
164 164 <div class="form-group">
165 165 <label class="form-label">Start</label>
166 166 <input type="datetime-local" class="form-input" name="start_time" value="${startISO}"
@@ -83,7 +83,7 @@
83 83 <form id="compose-modal-form" data-submit="ui.noop">
84 84 <span id="reply-indicator" class="reply-indicator"></span>
85 85 ${fieldsHtml}
86 - <div class="form-actions" style="margin-top: 0.75rem;">
86 + <div class="form-actions" style="margin-top: var(--gap-section);">
87 87 <button type="button" class="btn btn-secondary" id="compose-modal-attach">Attach File</button>
88 88 <button type="button" class="btn btn-secondary" id="compose-modal-save-draft">Save Draft</button>
89 89 <div class="flex-1"></div>
@@ -26,7 +26,7 @@
26 26 .split(/\s+/).map(w => w[0]).join('').substring(0, 2).toUpperCase();
27 27 const company = contact.company ? esc(contact.company) : '';
28 28 return `
29 - <div class="email-sender-contact row-flex row-flex-2">
29 + <div class="email-sender-contact row-flex row-flex-peer">
30 30 <div class="avatar avatar--sm">${initials}</div>
31 31 <div class="email-sender-info">
32 32 <span class="email-sender-name">${esc(name)}</span>
@@ -38,7 +38,7 @@
38 38 }
39 39 if (!parsed.email) return '';
40 40 return `
41 - <div class="email-sender-contact row-flex row-flex-2">
41 + <div class="email-sender-contact row-flex row-flex-peer">
42 42 <div class="avatar avatar--sm avatar--unknown">?</div>
43 43 <div class="email-sender-info">
44 44 <span class="email-sender-name">${esc(parsed.name || parsed.email)}</span>