Skip to main content

max / goingson

Rewrite design-system.md and styleguide.md for the frontend that exists Both were written for the JavaScript SPA deleted on 2026-08-22: the GoingsOn.* namespace, index.html, script load order, themes.js, and the classes the render functions wrote. Both carried an out-of-date banner instead of an answer. Neither is rewritten to describe quasi's vocabulary, because quasi documents that and a second copy drifts. What is left is the part that is goingson's own, split so the two files have distinct jobs again: design-system.md where each rule comes from. The four stylesheets and their owners, the layer order and the specificity trap under it, the three kinds of rule styles.css may carry, the seventeen classes it styles, how to grow the vocabulary, and the build guards that make a rule list unnecessary. styleguide.md the visual language. Platinum-informed rather than Neobrute, the intent set and what each intent means, the app-local invariants no theme reaches, the three faces and which one this app names, the logo, and accessibility. Both cut to a third of their length. Every claim checked against the tree rather than carried over: the hover-revealed row actions the old guide documented are gone from every stylesheet, so that rule is now the opposite one. contributing.md went with them, having the same defect in three places: a Rust-vs-JavaScript split that is now two kinds of Rust, a JS module step in the add-a-feature walkthrough, and a frontend/js/ form example. Its snippets are checked against the real Field and Row APIs. lint-frontend.sh pointed at docs/ux-audit/remediation-plan.md, which does not exist.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-24 16:27 UTC
Signed with PGP, not checked
Commit: 0a131ffd65251ca70b678f366c6678579e2b295b
Parent: 2dd5072
5 files changed, +266 insertions, -666 deletions
M Cargo.lock +4 -4
@@ -8493,6 +8493,10 @@
8493 8493 "winnow 1.0.4",
8494 8494 ]
8495 8495
8496 + [[patch.unused]]
8497 + name = "ops-status"
8498 + version = "0.1.0"
8499 +
8496 8500 [[patch.unused]]
8497 8501 name = "quasi-axum"
8498 8502 version = "0.56.0"
@@ -8508,7 +8512,3 @@
8508 8512 [[patch.unused]]
8509 8513 name = "quasi-store"
8510 8514 version = "0.1.0"
8511 -
8512 - [[patch.unused]]
8513 - name = "ops-status"
8514 - version = "0.1.0"
@@ -20,16 +20,18 @@
20 20
21 21 ### Rust Does Heavy Lifting
22 22
23 - All computation happens in Rust. JavaScript only renders and handles interactions.
23 + All computation happens in the domain crates. The described screens under
24 + `src-tauri/src/quasi/` state what a screen is, and never recompute anything.
24 25
25 - | Rust should | JavaScript should |
26 - |-------------|-------------------|
27 - | Filter, sort, group data | Render pre-computed data to DOM |
28 - | Date/time calculations | Handle UI interactions (clicks, keyboard) |
29 - | Business logic (urgency, recurrence) | Call Tauri commands via `GoingsOn.api` |
30 - | Aggregations and statistics | Manage ephemeral UI state (modal open, selection) |
26 + | Rust should | The description should |
27 + |-------------|------------------------|
28 + | Filter, sort, group data | State what a screen is, not how it is drawn |
29 + | Date/time calculations | Name the act a control performs |
30 + | Business logic (urgency, recurrence) | Say structure, and decline to say position |
31 + | Aggregations and statistics | Leave depth, colour and spacing to the design system |
31 32
32 - If you find yourself writing a `for` loop over data in JS, it probably belongs in Rust.
33 + Both halves are Rust now. The split is between the domain crates and
34 + `src-tauri/src/quasi/`, not between two languages.
33 35
34 36 ### Pre-compute Display Values
35 37
@@ -45,10 +47,10 @@
45 47 }
46 48 ```
47 49
48 - ```javascript
49 - // JS just uses the value
50 - element.classList.add(`urgency-${task.urgencyClass}`);
51 - element.textContent = task.dueFormatted;
50 + ```rust
51 + // The description just states the value
52 + Row::new(&task.title)
53 + .token(Tag::badge(task.status.as_str()).tone(status_tone(&task.status)))
52 54 ```
53 55
54 56 ### Single Source of Truth
@@ -56,7 +58,7 @@
56 58 - Business logic: `crates/core/`
57 59 - Persistence: `crates/db-sqlite/`
58 60 - Tauri commands: thin wrappers in `src-tauri/src/commands/`
59 - - JS: renders what Rust sends, never duplicates logic
61 + - Screens: `src-tauri/src/quasi/` describes what the domain computed, and never recomputes it
60 62
61 63 ## Adding a New Feature End-to-End
62 64
@@ -94,20 +96,19 @@
94 96
95 97 Update the response type in `src-tauri/src/commands/project.rs` if the field needs transformation, or it flows through automatically via serde.
96 98
97 - ### 5. Frontend JS
99 + ### 5. Described form
98 100
99 - Update the form fields in `src-tauri/frontend/js/projects.js`:
101 + Add the field to the form builder in `src-tauri/src/quasi/projects.rs`:
100 102
101 - ```javascript
102 - function getFormFields(project = null) {
103 - return [
104 - // ... existing fields
105 - { name: 'notes', type: 'textarea', label: 'Notes', value: project?.notes || '' },
106 - ];
107 - }
103 + ```rust
104 + let mut notes = Field::new(makeover_layout::FieldKind::Textarea, "notes", "Notes");
105 + notes.placeholder = Some("Anything worth remembering".to_owned());
106 + // then push `apply(notes, "notes")` into the `fields` vec, so a rejected
107 + // submission comes back refilled
108 108 ```
109 109
110 - The form modal and API call infrastructure handle the rest.
110 + The renderer draws it and the act carries it back. There is no markup to write
111 + and no API call to add.
111 112
112 113 ### 6. Tests
113 114
@@ -125,9 +126,10 @@
125 126 4. **SQLite implementation** (`crates/db-sqlite/src/repository/new_entity_repo.rs`)
126 127 5. **Migration** (`migrations/sqlite/`)
127 128 6. **Tauri commands** (`src-tauri/src/commands/new_entity.rs`): register in `main.rs`
128 - 7. **API methods** (`frontend/js/api.js`): add to the api object
129 - 8. **JS module** (`frontend/js/new-entity.js`): IIFE pattern, register on `GoingsOn.newEntity`
130 - 9. **Navigation**: add sidebar entry, route handler
129 + 7. **Described screen** (`src-tauri/src/quasi/new_entity.rs`): describe the
130 + screen and the acts it offers
131 + 8. **Route**: register the screen with the router in `src-tauri/src/quasi/mod.rs`
132 + 9. **Navigation**: add the shell entry
131 133
132 134 ## Code Style
133 135
@@ -138,27 +140,30 @@
138 140 - Typed errors (`CoreError`) not string errors
139 141 - Follow existing patterns in `commands/*.rs`
140 142
141 - ### JavaScript
143 + ### Screens
142 144
143 - - Use `GoingsOn.ui.apiCall()` for all API calls with toasts
144 - - Use `GoingsOn.ui.openFormModal()` for all CRUD forms
145 - - Use `GoingsOn.utils.escapeHtml()` / `escapeAttr()` for user data in HTML
146 - - No `window.X` exports, use the `GoingsOn` namespace
147 - - `async/await` over `.then()` chains
145 + - Screens are described in Rust under `src-tauri/src/quasi/`, not written as
146 + markup. `quasi-webview` renders the description
147 + - Grow the vocabulary rather than reaching around it: a need the description
148 + cannot state is a gap filed against quasi
149 + - The five scripts under `frontend/js/` each do something a description cannot
150 + say. Do not add a sixth without that being true of it
148 151
149 152 ### CSS
150 153
151 - - CSS variables from the design system
152 - - Neobrute style (see `docs/styleguide.md`)
153 - - No inline styles except for dynamic values
154 - - `styles.css` is hand-written and loaded directly; the other stylesheets are generated by `build.rs`
154 + - Intent tokens from the design system, never a hex literal and never
155 + `var(--token, #fallback)`
156 + - Platinum-informed (see `docs/styleguide.md`)
157 + - `styles.css` is hand-written; `typography.css`, `geometry.css` and
158 + `layout.css` are generated by `build.rs` and editing them does nothing
159 + - What may go in `styles.css` is a short list: see `docs/design-system.md`
155 160
156 161 ## Testing
157 162
158 163 - **Rust unit tests:** in-file `#[cfg(test)]` modules
159 164 - **Rust integration tests:** `tests/` directory
160 - - **JS tests:** `frontend/js/tests/` (run via the test runner)
161 - - Always verify the IPC round-trip works (Rust command -> JS render)
165 + - **Frontend guards:** `scripts/lint-frontend.sh`, plus the stylesheet checks
166 + `build.rs` runs on every build
162 167
163 168 ## Git Workflow
164 169
@@ -172,6 +177,6 @@
172 177 | Repository traits | `crates/core/src/repository.rs` |
173 178 | SQLite implementations | `crates/db-sqlite/src/repository/` |
174 179 | Tauri commands | `src-tauri/src/commands/` |
175 - | Frontend JS | `src-tauri/frontend/js/` |
180 + | Described screens | `src-tauri/src/quasi/` |
176 181 | Styles | `src-tauri/frontend/css/styles.css` |
177 182 | Migrations | `migrations/sqlite/` |
M docs/design-system.md +106 -271
@@ -1,279 +1,114 @@
1 - # GoingsOn Design System: Charter
1 + # GoingsOn Design System
2 2
3 - > **Out of date since 2026-08-22.** goingson swapped its document that day: the
4 - > Tauri window opens on `quasi://localhost/tasks`, `index.html` and every
5 - > hand-written file under `src-tauri/frontend/js/` are deleted, and the screens
6 - > are described in Rust under `src-tauri/src/quasi/`. What is still true here is
7 - > the part about `styles.css`, which is still served and still styles the
8 - > document; what is not is every reference to a script, a `<script>` tag, or a
9 - > class the JavaScript wrote. Rewriting it is goingson `3c5dae72`.
3 + Where every rule that styles this app comes from, and which of them are the app's
4 + own. For the visual language itself (the palette, the type scale, the logo) see
5 + `styleguide.md`.
10 6
11 - This is the **canonical primitive list** for the GoingsOn frontend. Every JS module that renders markup MUST use the primitive named here. If a render need does not match a primitive, the fix is to extend the primitive, not to fork it locally.
7 + ## The stack
12 8
13 - For visual specs (colors, sizes, shadows, hover behavior) see `styleguide.md`. This file is the inventory and the rules.
9 + Tauri 2 opens its window on `quasi://localhost/tasks`. There is no `index.html`
10 + and no hand-written frontend: the screens are described in Rust under
11 + `src-tauri/src/quasi/`, and `quasi-webview` renders each description to HTML.
12 + `src/quasi/` names zero CSS classes, so every class the document carries comes
13 + from the renderer or from makeover.
14 14
15 - **Stack:** Tauri 2 webview, vanilla HTML / CSS / JS. CSS in `src-tauri/frontend/css/styles.css`. JS modules under `src-tauri/frontend/js/` in IIFE `GoingsOn.*` namespace. Runtime themes in `src-tauri/frontend/themes/helix/`.
15 + Five small scripts survive under `src-tauri/frontend/js/`, each doing something a
16 + description cannot say: `host.js`, `quasi-clock.js`, `quasi-download.js`,
17 + `quasi-fill.js`, `quasi-selection.js`. htmx is vendored under `frontend/vendor/`.
16 18
17 - ---
19 + ## Four stylesheets, three of them generated
18 20
19 - ## Token layer: `styles.css :root`
21 + Served in this order by `src/quasi/mod.rs`, listed in `assets.rs`:
20 22
21 - The only place hex literals are allowed (besides `themes/`). Every JS render path consumes these via CSS classes; never via `var(--…)` in a JS string and never via fallback hex.
22 -
23 - | Axis | Tokens | Notes |
23 + | File | Owner | What it carries |
24 24 |---|---|---|
25 - | Surface color | `--bg-primary`, `--bg-secondary`, `--bg-tertiary`, `--bg-card`, `--bg-hover` | Themeable |
26 - | Text color | `--text-primary`, `--text-secondary`, `--text-muted`, `--text-on-accent` | Themeable |
27 - | Accent color | `--accent-yellow`, `--accent-green`, `--accent-blue`, `--accent-purple`, `--accent-red`, `--accent-cyan` | Themeable |
28 - | Accent alias | `--accent-color`, `--accent-primary` | Themeable |
29 - | Border | `--border-width` (2px), `--border-width-sm`, `--border-color`, `--border-light` | `--border-color` themeable; widths invariant |
30 - | Shadow | `--shadow-offset-xs/sm/md/lg/xl`, `--shadow-brutal-xs/md/lg/xl` | Theme-invariant (Neobrute signature) |
31 - | Radius | `--radius-xs/sm/md/lg/xl/full` | Invariant |
32 - | 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 |
33 - | Type size | `--font-size-xxs` ... `--font-size-4xl` | Invariant |
34 - | Line height | `--line-height-tight/normal/relaxed` | Invariant |
35 - | Font family | `--font-sans`, `--font-serif`, `--font-mono`, `--font-display` | Invariant |
36 - | Layout width | `--width-container`, `--width-modal`, `--width-sidebar` | Invariant |
37 - | Motion | `--transition-fast/normal/slow` | Invariant |
38 - | Cross-layer | `--timeline-slot-h` | Read by `js/day-planning-*` |
39 -
40 - **Rule:** every CSS property `js/themes.js` maps must exist in `:root`. Every property in `:root` that uses color must appear in `js/themes.js`'s mapping or carry a `/* theme-invariant */` comment.
41 -
42 - ---
43 -
44 - ## Component primitives: canonical class is the contract
45 -
46 - Each primitive below lists its **canonical class** (use this, only this). Modifier classes follow `--modifier` or `state-*` patterns. If you find yourself wanting a new modifier, add it here first.
47 -
48 - ### Button: `.button`
49 - Variants: `.button--primary`, `.button--secondary`, `.button--danger`, `.button--icon`, `.button--text`, `.button--link`.
50 - Sizes: `.button--sm` (default size is medium).
51 - State: `.button--loading`.
52 - **Never** style a `<button>` without `.button`. **Never** inline a hex color on a button.
53 - Fill and edge come from the generated `.button` in `css/layout.css`; do not restate them here.
54 -
55 - ### Card: `.card`
56 - Sub-parts: `.card-header`, `.card-title`, `.card-description`, `.card-meta`, `.card-badge`.
57 - Variant: `.card--list-item` (compact, for dense lists).
58 - Container: `.cards-grid`.
59 - Used by: projects-render, contacts-render, dashboard tiles.
60 - **A card is a control.** It answers a click, and the generated rule gives it hover, press, focus and disabled to say so. If the thing does not answer a click, it is a panel.
61 -
62 - ### Panel: `.panel`
63 - The card's box without the control: same border, radius and padding, no cursor and no states. Variants: `.panel--shell` (no padding, flex column, for list wrappers), `.panel--muted` (set back by colour, no edge).
64 - Wear `.raised` alongside it for fill and bevel: `<div class="raised panel review-card">`. `.panel--muted` takes no `.raised` — it is set back rather than raised.
65 - Replaced `.card--static | --shell | --muted` on 2026-08-06, which were cards with the control half cancelled. Never cancel a state a generated rule emits; take a class that does not emit it.
66 -
67 - ### Form field: `.form-group`
68 - Sub-parts: `.form-label`, `.field`, `.form-actions`, `.form-row`.
69 - The kind rides on the element, not on a modifier: `input.field`, `select.field`, `textarea.field`. Modifiers: `.field--ghost`, `.field--compact`. Invalid state is `[aria-invalid="true"]`, not a class, so the visual and the accessible state read one fact.
70 - Canonical render helper (**to be added**): `GoingsOn.ui.renderFormField({ kind, label, value, error, help })`. Until it exists, hand-rolled `.form-group` blocks are tolerated; once it lands they are not.
71 -
72 - ### Badge: `.badge`
73 - There is no `.tag`. It was a second name for the same thing and collapsed into `.badge`.
74 - Color variant: `[data-color="green|yellow|red|cyan|purple|blue|muted"]`.
75 - Status variant: `[data-tone="info|success|warning|danger"]`, from the generated stylesheet. Which tone a status takes is `GoingsOn.utils.statusTone(status)`; an unmapped status gets no attribute and stays neutral. The hand-coloured `.status-*` and `.badge-shared` classes are gone (2026-08-06).
76 - Sizes and intent: `.badge--xs`, `.badge--filled`.
77 - A plain badge is flat: no fill, no edge, text at `--content-muted`, all from the generated `.badge`. An edge on a label says it can be pressed, and no badge in GO is interactive. `[data-tone]` tones the text and leaves the box alone. `[data-color]` is the other axis and does make a pill: it names GO's own per-tag palette, where the colour is the content rather than a state.
78 -
79 - ### Modal: `.modal-overlay` (single global)
80 - Open via `GoingsOn.ui.openModal(title, html, opts)`. Sub-parts: `.modal-container` (+ `.modal-large`), `.modal-header`, `.modal-title`, `.modal-content`, `.modal-close`. Visibility via `.hidden` / `.closing`. **There is only one modal overlay in the DOM** (`#modal-overlay` in `index.html`); never create another.
81 -
82 - ### Toast: `.toast`
83 - Variants: `.toast-info`, `.toast-success`, `.toast-error`, `.toast-undo`.
84 - Undo sub-parts: `.undo-message`, `.undo-countdown`, and a plain `.button.button--sm.button--primary` for the action.
85 - Show via `GoingsOn.ui.showToast(msg, type, opts)` or `GoingsOn.ui.showUndoToast(...)`.
86 - **Rule (to be enforced):** positioning, shadow, and color belong on these classes in CSS. `showToast` MUST NOT inject `style.cssText`. Today's helper violates this; fix in remediation.
87 -
88 - ### Confirm dialog
89 - Render via `GoingsOn.ui.showConfirmDialog(title, message, opts)` or `GoingsOn.ui.confirmDelete(name, action)`. Uses the global modal. **Never** call `window.confirm()` (one offender remains in `contacts.js`, fix in remediation).
90 -
91 - ### Empty state: `.empty-state`
92 - Canonical: `<div class="empty-state"><div class="empty-state-icon">…</div><p class="empty-state-text">…</p><button class="button button--primary">…</button></div>`.
93 - Render via `GoingsOn.ui.renderEmptyState(message, buttonLabel?, onClick?)`.
94 - The non-canonical classes `.empty-dashboard-list`, `.kanban-empty`, `.virtual-scroller-empty` are **deprecated**; consolidate to `.empty-state` with size modifiers in remediation.
95 -
96 - ### Skeleton / loading
97 - Classes: `.skeleton-shimmer`, `.skeleton-row`, `.skeleton-lines`, `.skeleton-line.long | .medium | .short`, `.spinner`, `.loading`.
98 - Canonical helper (**to be added**): `GoingsOn.ui.renderSkeleton(kind, rows)`. No view uses skeletons today; once the helper exists, list views should switch on by default for the first paint after `invoke()`.
99 -
100 - ### Context menu: `.context-menu`
101 - State: `.visible`. Items: `.context-menu-item` (+ `--danger`), `.context-menu-separator`, `.context-menu-header`.
102 - Open via `GoingsOn.ui.showContextMenu(x, y, items)`.
103 -
104 - ### Tab / pill nav: `.tab-navigation` / `.pill-nav`
105 - Chosen state: `.tab.chosen` (scoped to `.tab-navigation`) / `.pill.active`. The two names differ because only `.tab` takes the generated selector rules. Used in shell (`index.html`) only. Feature modules should not introduce new tab styles.
106 -
107 - ### Filter bar: `.filter-bar`
108 - Children: `.filter-select`, `.filter-checkbox`. Used in tasks and emails filter rows.
109 -
110 - ### Progress bar: `.progress` (trough) + `.progress-fill`
111 - Sizes: `.progress--slim`, `.progress--mini`, `.progress--focus`. Tone on the fill: `[data-tone="info|success|warning|danger"]`.
112 - Used in tasks (subtask completion), milestones, and reviews.
113 -
114 - ### Row primitives
115 -
116 - | Kind | Canonical class | Render helper (today) |
117 - |---|---|---|
118 - | Task row | `.row.task-row` (in `.task-table`) | `renderTaskRow(t, index)`, `tasks-render.js` |
119 - | Event row | `.row.event-row-virtual.event-upcoming` or `.event-recurring` | `renderEventRow(e, index, isPast, isRecurring)`, `events.js` |
120 - | Project card | `.card` (in `.cards-grid`) | `renderProjectCard(p)`, `projects.js` |
121 - | Contact card | `.card.contact-card` | inline in `contacts-render.js` |
122 - | Email row | `.row.email-item` (in `.email-list`) | `renderEmailItem(thread)`, `emails-render.js` |
123 -
124 - ### Table columns: `col-<name>` and the column description
125 -
126 - Three tables have columns described in `src-tauri/build.rs`: the task table and the
127 - two event tables (upcoming and recurring, which are two tables and not one). A
128 - column says what it is worth (`Essential`, `Secondary`, `Optional`), never where it
129 - sits. `build.rs` turns each description into two generated files:
130 -
131 - - `css/tables.css`: the `grid-template-columns` track list per breakpoint, plus a
132 - `display: none` on each dropped column *by its own class*. Both halves come out of
133 - one call, so the tracks and the hiding cannot disagree.
134 - - `tables.columns.json`: the same column names, for the tests.
135 -
136 - Every cell wears `col-<name>`, which is what makes a column addressable without
137 - counting. Add a column by adding it to the description, then giving the header in
138 - `index.html` and the row builder a cell with its `col-` class. The suite in
139 - `js/tests/run.js` fails if either one disagrees with the description in content or
140 - in order.
141 -
142 - Never hide a column with `nth-child`, and never write a track list by hand. Both
143 - were how the mobile task rule came to declare four tracks for three surviving cells,
144 - and how the upcoming events row rendered six cells into a five-track grid with every
145 - cell one column left of its label.
146 -
147 - The rows themselves stay hand-built in JS. makeover-webview can emit cell containers
148 - (`list::cells_html`) and deliberately has no webview consumer: the virtual scroller
149 - calls its row builder synchronously while scrolling, so reaching Rust from there
150 - would put an IPC round trip in a 60Hz loop. GO takes the stylesheet half and the
151 - `col-` vocabulary, and nothing else.
152 -
153 - Per-cell classes (`.task-project`, `.event-cell-time`) still carry padding, alignment
154 - and colour. They are not the column; `col-<name>` is.
155 -
156 - ### Hover-revealed row actions: `.row` + `.row-actions`
157 - Both classes are generated (`layout.css`, makeover-layout `RowPart`). `.row-actions`
158 - rests at `opacity: 0` with `pointer-events: none`; `.row:hover` and `.row:focus-within`
159 - bring both back, so the control stays in the DOM, stays focusable, and stays in the
160 - accessibility tree. Put `.row` on the row and `.row-actions` on each control that
161 - hides: `.kebab-btn`, `.task-row-action`, `.month-goal-delete-btn`. Those three classes
162 - carry the look only, never the hiding.
163 -
164 - A control that is always visible does not wear `.row-actions` — the project card's
165 - kebab is one, which is why its card is not a `.row`. Where there is no hover
166 - `.row-actions` is shown unconditionally; that is a capability question, so the
167 - override lives under `(hover: none), (pointer: coarse)` and not under a width, and it
168 - must restore `pointer-events` as well as `opacity`.
169 -
170 - ### Row text parts: `.row-primary` / `.row-secondary` / `.row-meta`
171 - Generated too, and they are only the three content colours (`--content`,
172 - `--content-secondary`, `--content-muted`). Unlike `.row-actions` they are not scoped
173 - to a `.row` ancestor, so a list row that carries no hidden actions can still wear
174 - them: the email search result does.
175 -
176 - Put one on any row text whose colour is the hierarchy and nothing more, and delete
177 - the declaration it replaces. Worn today by the task row's project, due and
178 - recurrence cells and its no-subtasks dash; the email item's from, subject, date and
179 - preview; the event row's date number, time and location.
180 -
181 - A part is not worn where the colour means something other than rank. The overdue
182 - task description and due date, the recurring event's pattern label and every badge
183 - keep their own rule, and each of those out-specifies the part class it sits next to.
184 - A row element with no colour declaration of its own does not gain a part class
185 - either: it already inherits `--content`, so the class would add a declaration
186 - without replacing one.
187 -
188 - **Canonical helper (to be added):** `GoingsOn.ui.renderRow(kind, model, opts)`. Each `renderXxx` above becomes a thin adapter that maps the model to the shared "icon · primary · secondary · meta · actions" slot layout. Surface audits in Phase 1+ assume this exists.
189 -
190 - ### Task row state classes (composed onto `.task-row`)
191 - `.task-overdue`, `.task-completed`, `.task-started`, `.task-snoozed`, `.task-timer-active`, `.priority-high | -medium | -low`, plus badges `.task-badge.has-items`, `.task-time-badge.over-estimate`.
192 -
193 - ### Bulk selection
194 - Bar: `.bulk-actions-bar`. Controls: `.bulk-checkbox`, `.bulk-select-all`, `.bulk-count`. Row state: `.selected`.
195 -
196 - ### Kanban: `.kanban-board`
197 - Children: `.kanban-column`, `.kanban-card`, `.kanban-card-empty`. Used only by tasks-kanban view.
198 -
199 - ### Day-plan timeline: `.timeline-slot`
200 - Blocks: `.time-block`, `.block-focus`, `.block-personal`. Slot height read from `--timeline-slot-h`.
201 -
202 - ### Weekly review grid: `.weekly-grid`
203 - Cells: `.weekly-cell`, `.weekly-day-header`.
204 -
205 - ### Subtasks: `.subtask-item`
206 - Variant: `.subtask-item-linked` (left-border indicator for linked task). Children: `.subtask-checkbox`, `.subtask-text-done`.
207 -
208 - ### Shell: `.app-header` / `.app-body` / `.main-content` / `.page-header` / `.page-title`
209 - Feature modules do not redefine shell classes.
210 -
211 - ---
212 -
213 - ## Theme contract: `js/themes.js`
214 -
215 - Every theme is a TOML file under `themes/helix/` with a `[palette]` block (Helix-style names) and UI-key references. At runtime, `js/themes.js` maps 13 dotted TOML paths to CSS custom properties; selection persists to `localStorage` (`goingson-theme`).
216 -
217 - **Rules:**
218 - 1. A theme overrides **color tokens only**. Spacing, radius, shadow offsets, type are theme-invariant.
219 - 2. Every color token in `:root` either has a mapping in `js/themes.js` or is annotated `/* theme-invariant */`. Adding a new color token requires updating the mapping in the same PR.
220 - 3. JS rendering paths never read theme values directly. They use CSS classes that consume `var(--…)`. No JS string should contain `var(--accent-…, #fallback)` because the fallback bypasses the theme.
221 -
222 - ---
223 -
224 - ## Inline-style rules
225 -
226 - 1. `style="display:none"` in HTML is allowed only on the modal overlay and similar shell-level slots; feature views use `.hidden`.
227 - 2. No `style.cssText` in JS that contains a color, shadow, or border value. Layout-only inline styles (`flex`, `gap`, `min-width`) are tolerated during remediation; the goal is zero.
228 - 3. No hex literal in any file outside `styles.css` and `themes/*.toml`.
229 - 4. No `var(--token, #fallback)`. The fallback defeats theming.
230 -
231 - ---
232 -
233 - ## Cross-cutting rules
234 -
235 - These apply across every surface. Violations caught by reviewer checklist or `scripts/lint-frontend.sh`. Derived from the internal Phase 7 UX audit roll-up.
236 -
237 - ### State communication
238 - Every visual state (active, selected, running, error, success) must pair color with a second non-color signal: shape, position, weight, icon, or text. Color alone is not sufficient. (Phase 7 Pattern 1, 6 surfaces affected.)
239 -
240 - ### Filter & view state in the URL
241 - Every filter, sort, and view-mode setting that changes what the user sees must be mirrored to `location.search` on change and restored on init. Filter state must not live only in the DOM or in module-level JS. A shared `js/query-state.js` helper covers all surfaces. (Pattern 2, 5 surfaces affected.)
242 -
243 - ### Bulk operations always undoable
244 - Every bulk operation (any action touching more than one record at once) must wrap its API call in `GoingsOn.ui.showUndoToast` with a captured pre-state and an inverse operation. Use the shared `bulkActionWithUndo(action, inverse, ids, prevState)` helper. (Pattern 3, 3 surfaces affected.)
245 -
246 - ### Native dialogs forbidden
247 - `window.confirm`, `window.prompt`, and `window.alert` are banned. Use `GoingsOn.ui.showConfirmDialog`, `GoingsOn.ui.showPromptDialog`, and `GoingsOn.ui.showToast`. Native dialogs are disabled on iOS WKWebView and unstyled on all platforms. Lint rule `no-native-dialogs` enforces this.
248 -
249 - ### A described member stays in flow
250 - A group described by makeover-layout keeps every member in flow. A member never positions itself out of the row it shares: out of flow it contributes no width, so nothing can collide with it and nothing prevents the collision. Layering is the closed layer set (modals, the scrim, a drawer), not a member's own `position`. When a row runs out of room the answer is the group's fallback, not `position: absolute`. Lint rule `described-members-in-flow` enforces this for `.run`, `.page-header`, `.subview-head` and `.pill-nav`, and carries an allow-list for anything that has a reason.
251 -
252 - ### Mobile is responsive CSS by default
253 - JS branches on `GoingsOn.touch.isTouchDevice` (or media-query equivalents) require explicit justification documented here. Default is shared component + CSS layout reflow. (Pattern 5, Phase 6 architectural finding.)
254 -
255 - ### Multi-step flows show progress
256 - Any flow with more than two sequential modal steps shows a "Step N of M" indicator in the modal header. Applies to OAuth, encryption setup, plugin import wizards.
257 -
258 - ### Action bars cap at 5 visible
259 - A horizontal action bar has at most 5 visible actions; the rest live in an overflow `Actions â–¾` menu. Primary actions get `.button--primary`; destructive actions go in the overflow.
260 -
261 - ### Justified touch branches
262 - Modules with `isTouchDevice` branches must include a top-of-file comment naming what the branch does and why CSS-only isn't sufficient.
263 -
264 - ---
265 -
266 - ## Success criteria for remediation (input to the pre-Phase-1 plan)
267 -
268 - Phase 1 surface audits start when **all** of the following are true:
269 -
270 - - `GoingsOn.ui.renderRow(kind, model, opts)` exists, and `tasks-render.js`, `projects-render.js`, `contacts-render.js`, `events.js`, `emails.js` all call it (adapters allowed, parallel markup not).
271 - - `GoingsOn.ui.renderFormField({ … })` exists, and every form field in `form-modal.js`, `settings.js`, `email-accounts.js`, `settings-sync.js` is built through it. Error variant works.
272 - - `showToast` injects no `style.cssText`. All toast positioning, color, and shadow live on `.toast` + variant classes in `styles.css`.
273 - - Grep `\bstyle="` across `src-tauri/frontend/` returns no color, shadow, border, or font value; only visibility / layout micro-tweaks (and ideally none of those).
274 - - Grep `#[0-9a-fA-F]{3,8}` across `src-tauri/frontend/js/` and `src-tauri/frontend/*.html` returns zero matches.
275 - - Empty states: deprecate `.empty-dashboard-list`, `.kanban-empty`, `.virtual-scroller-empty`; consolidate to `.empty-state` with `--compact` / `--dashboard` modifiers, or keep them with explicit "use X when Y" rules documented in this charter.
276 - - Every color custom property in `styles.css :root` is either mapped in `js/themes.js` or carries a `/* theme-invariant */` comment.
277 - - `window.confirm()` calls: zero. All confirms route through `GoingsOn.ui.showConfirmDialog`.
278 -
279 - When all criteria hold, Phase 1 (Shell & navigation) may begin.
25 + | `css/typography.css` | generated from `makeover` | `@font-face` for the house faces and Reglo; `--font-mono`, `--font-sans`, `--font-display` |
26 + | `css/geometry.css` | generated from `makeover-geometry` | the `--step-*` scale and the `--gap-*` relationships; touch density; the shell width boundaries |
27 + | `css/layout.css` | generated from `makeover-webview` | depth, fill, edge, focus ring, disabled colour; `.raised`, `.well`, and the vocabulary classes |
28 + | `css/styles.css` | this app, by hand | the reset, the intent token block, and the box model and type this app puts on a vocabulary class |
29 +
30 + Do not edit the first three. They are written by `src-tauri/build.rs` on every
31 + build, and a change made there is gone at the next one.
32 +
33 + ## Layers
34 +
35 + `styles.css` declares three, and the renderer declares `makeover` ahead of them:
36 +
37 + makeover the three generated stylesheets
38 + base the reset and the token block
39 + components what a component owns at rest
40 + responsive every width and capability block
41 +
42 + Layers resolve before specificity, so a base rule in `styles.css` beats a more
43 + specific rule in `@layer makeover`. That is the point, and it is also the one
44 + trap: an app setting a property at rest must restate that property's disabled
45 + variant, or the generated `:disabled` never lands. Sections 9 and 20 of
46 + `styles.css` work both cases.
47 +
48 + ## What may live in `styles.css`
49 +
50 + Three kinds of rule, and the file's own header states them at length:
51 +
52 + - the reset and the token block, which are the app's ground
53 + - box model and type on a vocabulary class, where the design system supplies
54 + depth and colour and declines to supply geometry
55 + - placement for the described shell's chrome, which quasi states as structure
56 + and refuses to state as position, because a terminal has neither
57 +
58 + Depth, fill, edge, focus ring and disabled colour are not here. They come from
59 + `layout.css`, and a rule that restates one is a bug.
60 +
61 + Seventeen classes are styled today: `.badge`, `.button`, `.card`, `.chrome-nav`,
62 + `.chrome-panel`, `.chrome-place`, `.field`, `.form-checkbox-label`,
63 + `.form-error`, `.form-group`, `.form-hint`, `.form-label`, `.progress`,
64 + `.progress-fill`, `.tab`, `.table-heading`, `.toast`. Every one of them is the
65 + renderer's or makeover's.
66 +
67 + ## Adding something
68 +
69 + Ask, in order:
70 +
71 + 1. Does the design system already answer this? If it does, take the answer.
72 + 2. Does the answer belong in the description rather than in CSS? Usually it
73 + does.
74 + 3. Only then, a rule in `styles.css`.
75 +
76 + A new class is not an option. Nothing in this app can emit one. If a screen needs
77 + something the vocabulary cannot say, the gap is filed against quasi and the
78 + vocabulary grows through the cascade: `quasi-router` describes it,
79 + `makeover-layout` names it, `makeover-webview` emits it.
80 +
81 + ## The build is the check
82 +
83 + `src-tauri/build.rs` runs three guards, and they are why this document does not
84 + need a rule list:
85 +
86 + - `check_stylesheet_reaches_markup` asks `quasi-webview` whether anything can
87 + emit each selector in `styles.css`, prints the live and dead counts, and seals
88 + the dead count so it cannot grow. The emittable set is closed, so a selector
89 + outside it is dead rather than unaccounted for.
90 + - `check_vocabulary` catches a rule in `styles.css` that restates what
91 + `geometry.css` or `layout.css` already says.
92 + - the intent check refuses a rule that reads a `--token` the fallback block does
93 + not declare, since the declaration would be invalid until a theme lands.
94 +
95 + `check_vocabulary_use` is not among them. It went with the swap and nothing
96 + replaces it: goingson `43a682b0`.
97 +
98 + `scripts/lint-frontend.sh` holds the three rules that police a stylesheet rather
99 + than a script: `no-var-fallback-hex`, `no-deprecated-empty-states`,
100 + `described-members-in-flow`. Its header records the seven rules the swap
101 + retired and why each one stopped having anything to read.
102 +
103 + ## Two standing rules
104 +
105 + **No `var(--token, #fallback)`.** The fallback defeats the theme. Lint rule
106 + `no-var-fallback-hex`.
107 +
108 + **A described member stays in flow.** A group described by `makeover-layout`
109 + keeps every member in flow; a member never positions itself out of the row it
110 + shares. Out of flow it contributes no width, so nothing can collide with it and
111 + nothing prevents the collision. Layering is the closed layer set (modals, the
112 + scrim, a drawer), never a member's own `position`. Lint rule
113 + `described-members-in-flow`, with an allow-list that requires a reason. Wiki:
114 + `layout-room-and-fallback`.
M docs/styleguide.md +111 -351
@@ -1,603 +1,168 @@
1 1 # GoingsOn Style Guide
2 2
3 - > **Out of date since 2026-08-22.** goingson swapped its document that day: the
4 - > Tauri window opens on `quasi://localhost/tasks`, `index.html` and every
5 - > hand-written file under `src-tauri/frontend/js/` are deleted, and the screens
6 - > are described in Rust under `src-tauri/src/quasi/`. What is still true here is
7 - > the part about `styles.css`, which is still served and still styles the
8 - > document; what is not is every reference to a script, a `<script>` tag, or a
9 - > class the JavaScript wrote. Rewriting it is goingson `3c5dae72`.
3 + The visual language: what the app looks like and which parts of that are its own
4 + rather than the design system's. For where each rule comes from and what may be
5 + written by hand, see `design-system.md`.
10 6
11 - ## Design Language: Neobrute
7 + ## Design language: Platinum-informed
12 8
13 - GoingsOn uses **Neobrute**, a clean neobrutalism design system inspired by [neobrutalism.dev](https://www.neobrutalism.dev/).
9 + Mac OS 8 Platinum, informed rather than copied. Two-tone bevels, square corners,
10 + a neutral ramp, instant state changes, native window chrome.
14 11
15 - ### Core Philosophy
12 + It grew out of the neobrutalist system it replaces, which is why the token shapes
13 + carry over intact: the same border width, the same radius scale, the same offset
14 + shadow. What changed is what they resolve to. A raised object is now read by its
15 + bevel rather than by a heavy black edge and a 4px drop, and only a floating
16 + surface casts a shadow.
16 17
17 - - **Bold and high-contrast**: Black borders, black text, crisp offset shadows with zero blur
18 - - **Blue-dominant palette**: Primary actions use bright blue, yellow reserved for warnings/status
19 - - **Cards have presence**: 4px offset shadows on all cards and containers, hover lift on clickable cards
20 - - **Sans-serif headings**: System sans-serif at weight 700 for all headings
18 + - Depth is a fill and an edge together. Naming them apart is what let them
19 + disagree, so `makeover-layout` states them as one thing and this app states
20 + neither.
21 + - State changes are instant. There is no transition token in this app.
22 + - A card answers a click. If a thing does not answer a click it is a panel.
21 23
22 - ---
24 + ## Colour
23 25
24 - ## Color System
26 + Colour arrives as **intents**, not as named hues. A rule says what a surface is
27 + for and the theme decides its value.
25 28
26 - ### Background Colors
29 + | Intent | Meaning |
30 + |---|---|
31 + | `--surface-page` | the window's ground |
32 + | `--surface-raised` | a panel or card lifted off the page |
33 + | `--surface-sunken` | a recess |
34 + | `--surface-well` | content cut into a raised surface, so a list reads as content in a container rather than as bands on a panel |
35 + | `--surface-overlay` | a floating surface |
36 + | `--bevel-light` / `--bevel-dark` | the two edges of a bevel, derived by makeover from the raised surface |
37 + | `--content` / `--content-secondary` / `--content-muted` | the text hierarchy |
38 + | `--action` | the affirmative control |
39 + | `--danger` / `--success` / `--warning` / `--info` | status |
40 + | `--border` | an edge |
41 + | `--focus-ring` | the focus ring |
42 + | `--hover-surface` | pointer-over fill |
43 + | `--category-one` .. `--category-six` | this app's per-tag palette, where the colour is the content rather than a state |
27 44
28 - | Variable | Hex | Usage |
29 - |----------|-----|-------|
30 - | `--bg-primary` | `#E0E4FA` | Page background (lavender-blue) |
31 - | `--bg-secondary` | `#CDD3F0` | Secondary surfaces, hover states |
32 - | `--bg-tertiary` | `#BAC2E6` | Tertiary surfaces |
33 - | `--bg-card` | `#FFFFFF` | Cards, modals, inputs |
45 + The values in `styles.css :root` are the "goingson" titular theme, and they are
46 + the fallback rather than the theme. A document served while the theme is still
47 + arriving renders with them. `/static/theme.css` loads after and carries whatever
48 + the user chose, resolved by `makeover` at startup. See `src/quasi/theming.rs`.
34 49
35 - ### Text Colors
50 + **A rule that reads a new intent needs a value added to that block too**, or the
51 + whole declaration is invalid until the theme lands. `build.rs` enforces this
52 + rather than asking for it.
36 53
37 - | Variable | Hex | Usage |
38 - |----------|-----|-------|
39 - | `--text-primary` | `#000000` | Headings, primary text |
40 - | `--text-secondary` | `#2D2D2D` | Body text, descriptions |
41 - | `--text-muted` | `#6B6B6B` | Captions, hints, disabled |
54 + **A theme overrides colour only.** Border width, bevel thickness, radius, type
55 + and spacing are not reachable from a theme.
42 56
43 - ### Accent Colors
57 + Following the system is live: a selection of "system" renders both variants, the
58 + dark one behind `prefers-color-scheme`, and the browser picks. A pinned theme
59 + change takes effect at the next launch.
44 60
45 - | Variable | Hex | Usage |
46 - |----------|-----|-------|
47 - | `--accent-blue` | `#6196FF` | **Primary actions**, active states, focus, buttons |
48 - | `--accent-yellow` | `#F7D154` | Warnings, snooze, medium priority, "on hold" status |
49 - | `--accent-green` | `#5CB85C` | Success, active status, tasks |
50 - | `--accent-purple` | `#7B68EE` | Recurrence, essays, special |
51 - | `--accent-red` | `#DC3545` | Errors, high priority, danger |
52 - | `--accent-cyan` | `#17A2B8` | Info, side projects, completed |
61 + ## Type
53 62
54 - ---
63 + Three faces, and the app names only one of them.
55 64
56 - ## Logo
65 + | Token | Face | Where it comes from |
66 + |---|---|---|
67 + | `--font-sans` | Quasi Body | `makeover`, via `typography.css` |
68 + | `--font-mono` | Quasi Mono | `makeover`, via `typography.css` |
69 + | `--font-display` | Reglo Bold | this app's override in its build script |
57 70
58 - ### Full Logo
71 + Quasi Body and Quasi Mono are cut by `quasi-type` from Atkinson Hyperlegible plus
72 + the house glyph set, both variable over weight 200 to 800 in one file.
59 73
60 - The GoingsOn wordmark uses **Reglo Bold** with the following specifications:
74 + Reglo is GoingsOn's brand face: the wordmark and hero headings, bold only.
75 + [Reglo by Sebastien Sanfilippo](https://github.com/nicokant/reglo), OFL, at
76 + `frontend/fonts/Reglo-Bold.woff2` with its licence beside it. `--font-display`
77 + falls back through `--font-serif`, which is the one face stated in `styles.css`,
78 + because the house model has two slots and a serif is not one of them.
61 79
62 - - **Font**: Reglo Bold
63 - - **Color**: `--text-primary` (#000000) on light backgrounds
64 - - **Alternate**: White on dark backgrounds
65 - - **Letter-spacing**: -0.02em (slightly tightened)
80 + `--font-heading` is an alias for `--font-sans`. It is not the brand slot.
66 81
67 - ### Small Logo (Icon)
82 + Scale, all app-local and theme-invariant, over a 16px root:
68 83
69 - The compact logo displays **"GO"** in Reglo Bold, centered within a neobrutalist container:
84 + | Token | Size | Use |
85 + |---|---|---|
86 + | `--font-size-xxs` | 0.65rem | tiny badges, indicators |
87 + | `--font-size-xs` | 0.7rem | small badges, counts |
88 + | `--font-size-sm` | 0.75rem | meta text, timestamps |
89 + | `--font-size-md` | 0.8rem | table headers, compact UI |
90 + | `--font-size-base` | 0.875rem | body |
91 + | `--font-size-lg` | 1rem | emphasised body |
92 + | `--font-size-xl` | 1.1rem | subheadings |
93 + | `--font-size-2xl` | 1.25rem | section titles |
94 + | `--font-size-3xl` | 1.5rem | page titles |
95 + | `--font-size-4xl` | 1.75rem | main headings |
70 96
71 - ```
72 - +-----------------+
73 - | |
74 - | GO |
75 - | |
76 - +-----------------+
77 - ```
78 -
79 - **Specifications:**
80 - - **Background**: `--accent-blue` (#6196FF)
81 - - **Text**: `--text-on-accent` (#FFFFFF)
82 - - **Border**: 2px solid `--border-color` (#000000)
83 - - **Border Radius**: `--radius-sm` (5px)
84 - - **Shadow**: 2px 2px 0 `--border-color` (neobrutalist offset)
85 -
86 - **Files:**
87 - - `media/logo-go.svg` - Small "GO" icon logo
88 - - `media/logo-goingson.svg` - Full wordmark (future)
89 -
90 - ### Usage Guidelines
91 -
92 - | Context | Logo | Min Size |
93 - |---------|------|----------|
94 - | App icon / Favicon | GO icon | 16x16px |
95 - | Sidebar / Header | GO icon | 32x32px |
96 - | Splash / Marketing | Full wordmark | 120px wide |
97 - | Documentation | Either | Context-dependent |
98 -
99 - ---
100 -
101 - ## Typography
102 -
103 - ### Font Families
104 -
105 - ```css
106 - --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
107 - --font-serif: Georgia, 'Times New Roman', serif;
108 - --font-mono: 'SF Mono', 'Consolas', 'Liberation Mono', monospace;
109 - --font-display: 'Reglo', var(--font-serif);
110 - ```
111 -
112 - ### Display Font: Reglo
113 -
114 - The **Reglo** font is used for the logo and prominent H1-style headings. Reglo is an open-source display font with a bold, geometric character that complements the neobrutalist aesthetic.
115 -
116 - - **Source**: [Reglo by Sebastien Sanfilippo](https://github.com/nicokant/reglo) (OFL license)
117 - - **Usage**: Logo wordmark "GoingsOn", hero headings, splash screens
118 - - **Weights**: Bold only (display use)
119 -
120 - ```css
121 - @font-face {
122 - font-family: 'Reglo';
123 - src: url('fonts/Reglo-Bold.woff2') format('woff2');
124 - font-weight: 700;
125 - font-display: swap;
126 - }
127 - ```
128 -
129 - ### Semantic Aliases
130 -
131 - - `--font-display`: Uses `Reglo` for logo and hero headings
132 - - `--font-heading`: Uses `--font-sans` for titles and headings
133 - - `--font-body`: Uses `--font-sans` for body text
134 -
135 - ### Type Scale
136 -
137 - | Element | Size | Weight | Font |
138 - |---------|------|--------|------|
139 - | Page Title | 1.75rem | 700 | Sans |
140 - | Card Title | 1.1rem | 700 | Sans |
141 - | Modal Title | 1.25rem | 700 | Sans |
142 - | Body | 1rem | 400 | Sans |
143 - | Small | 0.875rem | 400 | Sans |
144 - | Caption | 0.75rem | 600 | Sans |
145 -
146 - ---
97 + `--line-height-normal` is 1.5 and is the only line-height token left.
147 98
148 99 ## Spacing
149 100
150 - The spacing system uses a consistent rem-based scale:
101 + Not this app's. Spacing is named by relationship and comes from
102 + `makeover-geometry` via `geometry.css`, because it is invariant and therefore
103 + belongs to every app equally.
151 104
152 - | Name | Value | Usage |
153 - |------|-------|-------|
154 - | xs | 0.25rem | Badge padding, tight gaps |
155 - | sm | 0.5rem | Small gaps, icon spacing |
156 - | md | 0.75rem | Standard padding |
157 - | lg | 1rem | Section padding |
158 - | xl | 1.25rem | Card padding |
159 - | 2xl | 1.5rem | Page margins |
105 + --gap-bound inside one thing
106 + --gap-peer between siblings
107 + --gap-group between groups
108 + --gap-section between sections
109 + --gap-pane between panes
110 + --gap-page page margins
160 111
161 - ---
112 + Prefer a `--gap-*`. Reach for the raw `--step-hair` .. `--step-colossal` scale
113 + only where no relationship describes the distance.
162 114
163 - ## Border & Shadow System
115 + Both scales shift under touch density, which is a capability question rather
116 + than a width one: a narrow desktop window still has a pointer, a full-width
117 + tablet still has a finger.
164 118
165 - ### Border Widths
119 + ## Border, radius, shadow
166 120
167 - | Element | Width |
168 - |---------|-------|
169 - | Cards, Buttons, Modals | 2px (`--border-width`) |
170 - | Inputs, Badges, Tags | 2px |
171 - | Dividers | 2px |
121 + App-local, invariant, and no theme reaches them.
172 122
173 - ### Border Radius
123 + - `--border-width`, `--border-width-sm`: both 1px under Platinum.
124 + - Radius: `--radius-sm` is 4px and is the one place a corner survives, on
125 + buttons, inputs and small cards. `--radius-full` is 50% for circles.
126 + `--radius-xs`, `--radius-md`, `--radius-lg`, `--radius-xl` are all 0.
127 + - Shadow: one, `--shadow-brutal-md`, at a `--shadow-offset-md` of 3px. Only a
128 + floating surface casts it, and always composed with `--bevel-raised`: the
129 + bevel says lit object, the shadow says above the page.
174 130
175 - | Variable | Value | Usage |
176 - |----------|-------|-------|
177 - | `--radius-xs` | 3px | Scrollbars, tiny elements |
178 - | `--radius-sm` | 5px | Buttons, badges, inputs |
179 - | `--radius-md` | 5px | Cards, filter bars (uniform with buttons) |
180 - | `--radius-lg` | 10px | Modals |
181 - | `--radius-xl` | 20px | Pills |
131 + `--bevel-raised` and `--bevel-inset` are not here. They come from `layout.css`,
132 + because which edge is lit and what inverts on press is the description's answer.
182 133
183 - ### Shadow System
134 + `--width-container` is 1400px.
184 135
185 - All shadows use **zero blur** for crisp neobrutalist edges:
136 + ## Logo
186 137
187 - ```css
188 - /* Shadow utilities */
189 - .shadow-sm { box-shadow: 2px 2px 0 var(--border-color); }
190 - .shadow-md { box-shadow: 4px 4px 0 var(--border-color); } /* Default */
191 - .shadow-lg { box-shadow: 6px 6px 0 var(--border-color); }
192 - .shadow-xl { box-shadow: 8px 8px 0 var(--border-color); } /* Modals */
193 - .shadow-none { box-shadow: none; }
194 - ```
138 + The wordmark is "GoingsOn" set in Reglo Bold, letter-spacing -0.02em,
139 + `--content` on light and white on dark.
195 140
196 - ### Offset Shadow Values
141 + The compact mark is "GO" in Reglo Bold on `--action`, text in white, a
142 + `--border-width` edge in `--border`, `--radius-sm`.
197 143
198 - | Element | Shadow Offset | Hover Lift |
199 - |---------|---------------|------------|
200 - | Cards (project) | 4px | Yes (-2px translate) |
201 - | Dashboard items | 4px | Yes |
202 - | Kanban cards | 4px | Yes |
203 - | Saved views | 4px | Yes |
204 - | Tables (task, event, data) | 4px | No |
205 - | Email list | 4px | No |
206 - | Filter bar | 4px | No |
207 - | Stat cards | 4px | No |
208 - | Review sections/cards | 4px | No |
209 - | Milestone cards | 4px | No |
210 - | Buttons (default) | none | No |
211 - | Buttons (primary/danger) | 4px | Yes |
212 - | Modals | 8px | No |
213 - | Dropdowns/Context menus | 3-6px | No |
214 - | Inputs | none | No |
144 + | Context | Mark | Minimum |
145 + |---|---|---|
146 + | App icon, favicon | GO | 16x16 |
147 + | Header | GO | 32x32 |
148 + | Splash, marketing | wordmark | 120px wide |
215 149
216 - ---
217 -
218 - ## Components
219 -
220 - ### Buttons
221 -
222 - ```html
223 - <!-- Primary button (blue background, white text) -->
224 - <button class="button button--primary">Action</button>
225 -
226 - <!-- Secondary button (light background) -->
227 - <button class="button button--secondary">Cancel</button>
228 -
229 - <!-- Small button -->
230 - <button class="button button--sm">Small</button>
231 - ```
232 -
233 - **States (default `.button`):**
234 - - **Default**: Flat (border only, no shadow)
235 - - **Hover**: Background changes to `--bg-secondary`
236 - - **Active**: Background changes to `--bg-tertiary`
237 -
238 - **States (`.button--primary`, `.button--danger`):**
239 - - **Default**: 4px offset shadow
240 - - **Hover**: Lifts up (-2px, -2px), shadow increases
241 - - **Active**: Pushes down (1px, 1px), shadow shrinks
242 -
243 - ### Cards
244 -
245 - ```html
246 - <div class="card">
247 - <div class="card-header">
248 - <h3 class="card-title">Card Title</h3>
249 - </div>
250 - <p class="card-description">Description text</p>
251 - <div class="card-meta">
252 - <span class="badge">Job</span>
253 - <span class="badge" data-tone="info">Active</span>
254 - </div>
255 - </div>
256 - ```
257 -
258 - Cards have:
259 - - White background with 4px offset shadow
260 - - Hover: lifts up (-2px), shadow grows, bg shifts to `--bg-secondary`
261 - - Touch devices: no hover transform
262 -
263 - **A card answers a click.** Read-only surfaces are panels, not cards with the
264 - hover switched off:
265 -
266 - ```html
267 - <div class="raised panel review-card">...</div>
268 - <div class="raised panel panel--shell">...</div>
269 - <div class="panel panel--muted">...</div>
270 - ```
271 -
272 - `.panel` is the card's box (border, radius, padding) with no cursor and no
273 - states; `.raised` alongside it supplies fill and bevel. `.panel--muted` is set
274 - back by colour and wears no `.raised`.
275 -
276 - ### Badges
277 -
278 - There is no `.tag`. It was a second name for the same thing and collapsed into
279 - `.badge`.
280 -
281 - A plain badge is flat: no fill, no edge, text at `--content-muted`. An edge on a
282 - label says it can be pressed, and no badge in GO is interactive.
283 -
284 - ```html
285 - <span class="badge">Label</span>
286 - ```
287 -
288 - **Colour makes it a pill.** A badge carrying `[data-color]` declares its own
289 - fill and border. This is the per-tag palette, where the colour is the content:
290 -
291 - ```html
292 - <span class="badge" data-color="green">Success</span>
293 - <span class="badge" data-color="yellow">Warning</span>
294 - <span class="badge" data-color="red">Error</span>
295 - <span class="badge" data-color="cyan">Info</span>
296 - <span class="badge" data-color="purple">Special</span>
297 - <span class="badge" data-color="blue">Default</span>
298 - <span class="badge" data-color="muted">Muted</span>
299 - ```
300 -
301 - **Status is a tone, not a class.** `[data-tone]` tones the text and leaves the
302 - box flat. Call `GoingsOn.utils.statusTone(status)` for the mapping rather than
303 - picking a tone at the render site; a status with no tone gets no attribute:
304 -
305 - ```html
306 - <span class="badge" data-tone="success">Completed</span>
307 - <span class="badge" data-tone="info">Active</span>
308 - <span class="badge" data-tone="warning">On Hold</span>
309 - <span class="badge">Archived</span>
310 - ```
311 -
312 - **Size and intent:** `.badge--xs` for a smaller pill, `.badge--filled` for a
313 - solid accent fill with no border.
314 -
315 - ### Form Inputs
316 -
317 - ```html
318 - <div class="form-group">
319 - <label class="form-label">Label</label>
320 - <input type="text" class="field" placeholder="Enter text...">
321 - </div>
322 -
323 - <div class="form-group">
324 - <label class="form-label">Select</label>
325 - <select class="field">
326 - <option>Option 1</option>
327 - </select>
328 - </div>
329 -
330 - <div class="form-group">
331 - <label class="form-label">Textarea</label>
332 - <textarea class="field"></textarea>
333 - </div>
334 - ```
335 -
336 - The kind rides on the element, not on a modifier class. `select.field` and
337 - `textarea.field` carry the kind-specific bits, so an element selector cannot be
338 - forgotten at a call site.
339 -
340 - **Focus state**: Blue ring (2px) around the input
341 -
342 - ### Modals
343 -
344 - ```html
345 - <div class="modal-overlay">
346 - <div class="modal-container">
347 - <div class="modal-header">
348 - <h2 class="modal-title">Modal Title</h2>
349 - <button class="modal-close">&times;</button>
350 - </div>
351 - <div class="modal-content">
352 - <!-- Content here -->
353 - </div>
354 - </div>
355 - </div>
356 - ```
357 -
358 - Modals have:
359 - - 8px offset shadow
360 - - 10px border radius
361 - - Semi-transparent overlay
362 -
363 - ### Tables
364 -
365 - ```html
366 - <table class="task-table">
367 - <thead>
368 - <tr>
369 - <th>Column</th>
370 - </tr>
371 - </thead>
372 - <tbody>
373 - <tr>
374 - <td>Data</td>
375 - </tr>
376 - </tbody>
377 - </table>
378 - ```
379 -
380 - Features:
381 - - 4px offset shadow on container
382 - - Uppercase, letter-spaced headers
383 - - Hover state on rows
384 - - Selected state (blue tint background)
385 -
386 - ### Empty & Error States
387 -
388 - ```html
389 - <div class="empty-state">
Lines truncated
@@ -1,6 +1,6 @@
1 1 #!/bin/bash
2 2 # Frontend design-system lint guards.
3 - # See docs/design-system.md "Inline-style rules" and docs/ux-audit/remediation-plan.md Step 10.
3 + # See docs/design-system.md, "Two standing rules".
4 4 # Exit 0 = clean. Exit non-zero = violations found (printed with file:line).
5 5 #
6 6 # SEVEN RULES WENT WITH THE 2026-08-22 SWAP, and what is left is the three that