Skip to main content

max / goingson

Normalize comments and UI copy to house style styles.css: strip the ==== header frame and === section banners down to `/* --- label */` markers, swap the aligned section-index em-dash columns to `|`, and convert prose em-dashes to ASCII punctuation. Also drops connective em-dashes from UI toast/status strings, MCP tool descriptions, and CLI banners. Follows the code-comment-style wiki note. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-24 17:45 UTC
Commit: 0a72738a3eb5cdb11e800087bc56e317e33e2c24
Parent: fb5dc3b
17 files changed, +64 insertions, -70 deletions
@@ -84,7 +84,7 @@ fn next(it: &mut impl Iterator<Item = String>, flag: &str) -> Result<String, Str
84 84
85 85 fn print_help() {
86 86 eprintln!(
87 - "go-mcp — MCP server for GoingsOn tasks/projects\n\n\
87 + "go-mcp: MCP server for GoingsOn tasks/projects\n\n\
88 88 Usage: go-mcp [--db <path>] [--stdio] [--host <ip>] [--port <n>]\n\
89 89 \x20 [--grant <cap>]... | [--grant-all] [--compact]\n\n\
90 90 Transport: default is Streamable HTTP; --stdio speaks newline-delimited\n\
@@ -172,7 +172,7 @@ impl Tool for UpdateProjectTool {
172 172 "update_project"
173 173 }
174 174 fn description(&self) -> &str {
175 - "Update an existing project, resolved by `project` (its name). Only the fields you pass change. `status` is Active|OnHold|Completed|Archived — Archived is the non-destructive way to retire a project. `type` is Job|SideProject|Company|Essay|Article|Painting|Other. Renaming is not offered: the name is how create_task and bulk_import_tasks resolve a project, so a rename would strand callers."
175 + "Update an existing project, resolved by `project` (its name). Only the fields you pass change. `status` is Active|OnHold|Completed|Archived. Archived is the non-destructive way to retire a project. `type` is Job|SideProject|Company|Essay|Article|Painting|Other. Renaming is not offered: the name is how create_task and bulk_import_tasks resolve a project, so a rename would strand callers."
176 176 }
177 177 fn kind(&self) -> ToolKind {
178 178 ToolKind::Write(caps::project_update())
@@ -42,7 +42,7 @@ impl Tool for ListTasks {
42 42 "list_tasks"
43 43 }
44 44 fn description(&self) -> &str {
45 - "List tasks as compact rows. Optional filters: `project` (name), `status` (Pending|Started|Completed), `tag`. Paged: `limit` (default 50, max 200) and `offset`. Long descriptions are clipped and the row marked `truncated` — use `get_task` for the full text. The reply carries `total` (rows matching the filters) and, when more remain, `next_offset`."
45 + "List tasks as compact rows. Optional filters: `project` (name), `status` (Pending|Started|Completed), `tag`. Paged: `limit` (default 50, max 200) and `offset`. Long descriptions are clipped and the row marked `truncated`; use `get_task` for the full text. The reply carries `total` (rows matching the filters) and, when more remain, `next_offset`."
46 46 }
47 47 fn kind(&self) -> ToolKind {
48 48 ToolKind::Read
@@ -1066,9 +1066,7 @@ fn parse_db_arg() -> Result<Option<PathBuf>> {
1066 1066 db = Some(PathBuf::from(v));
1067 1067 }
1068 1068 "-h" | "--help" => {
1069 - println!(
1070 - "got — read-first terminal board for GoingsOn\n\nUsage: got [--db <path>]"
1071 - );
1069 + println!("got: read-first terminal board for GoingsOn\n\nUsage: got [--db <path>]");
1072 1070 std::process::exit(0);
1073 1071 }
1074 1072 other => anyhow::bail!("unknown argument: {other}"),
@@ -1,4 +1,4 @@
1 - /* ============================================================================
1 + /*
2 2 GoingsOn: Neobrute Theme
3 3
4 4 Build new UI by COMPOSING the existing vocabulary, in this order of preference:
@@ -13,7 +13,7 @@
13 13 4. Extend a primitive with a modifier (.btn-primary, .btn-sm,
14 14 .form-select--compact,
15 15 .subtask-item--linked)
16 - 5. ONLY THEN consider a new class — and add it to the right
16 + 5. ONLY THEN consider a new class | and add it to the right
17 17 BAND below
18 18
19 19 A new class is a smell, not a goal. Before writing one:
@@ -26,14 +26,13 @@
26 26 others size to content) belongs in a NAMED LAYOUT with an explicit
27 27 `grid-template-columns`, not in `.row-flex` + utilities. The utility form
28 28 encodes intent in browser flex-shrink math rather than in CSS that reads
29 - as intent — especially fragile when the row contains a native `<select>`,
29 + as intent, especially fragile when the row contains a native `<select>`,
30 30 whose intrinsic-min-content sizing fights `flex: 1`. Smell test: if you
31 31 find yourself reaching for `.form-select--compact`, `style="width: auto;"`,
32 32 or `min-width: 0 !important` to make a row lay out right, the layout
33 33 itself wants a name. See `.email-filter-row`, `.contacts-filter-row`, and
34 34 `.work-hours-row` (all 2026-05-24) for the canonical form.
35 35
36 - ----------------------------------------------------------------------------
37 36 FILE STRUCTURE: search by BAND name (uppercase anchors) to navigate.
38 37 Per-section titles use `/* N. Title */` headers; numbered for stability,
39 38 not because order matters. Bands group related sections; cascade order is
@@ -42,8 +41,8 @@
42 41 BAND: FOUNDATIONS
43 42 1. Font Face
44 43 2. CSS Reset & Base
45 - 3. Design System Variables (Neobrute) — tokens (:root)
46 - 4. Utility Classes — shadow/hover/border,
44 + 3. Design System Variables (Neobrute) | tokens (:root)
45 + 4. Utility Classes | shadow/hover/border,
47 46 layout, form sizing,
48 47 settings, subtask
49 48
@@ -54,7 +53,7 @@
54 53 8. Main Content & Page Header
55 54
56 55 BAND: COMPONENT PRIMITIVES & WIDGETS
57 - 9. Buttons — .btn + variants/sizes
56 + 9. Buttons | .btn + variants/sizes
58 57 10. Quick Add Input
59 58 11. Cards
60 59 12. Tags & Badges
@@ -65,7 +64,7 @@
65 64 17. Email List
66 65 18. Toast Notifications
67 66 19. Modals
68 - 20. Form Elements — .form-input,
67 + 20. Form Elements | .form-input,
69 68 .form-select, +
70 69 JS-rendered layout
71 70 utilities tail
@@ -132,8 +131,7 @@
132 131 BAND: SCOPED STYLESHEETS
133 132 Compose window (compose.html standalone Tauri window)
134 133
135 - ----------------------------------------------------------------------------
136 - EXISTING VOCABULARY — grep before inventing a new class.
134 + EXISTING VOCABULARY: grep before inventing a new class.
137 135
138 136 UTILITIES
139 137 Layout .flex-1, .row, .row-flex + .row-flex-{2,3,4},
@@ -169,34 +167,32 @@
169 167 Intents: .btn-primary / -secondary / -danger
170 168 Shapes: .form-select--compact / --ghost, .form-input--ghost
171 169
172 - ----------------------------------------------------------------------------
173 170 UI MODE STRATEGY (do not casually merge)
174 171
175 172 UI mode is set ONCE at boot by index.html's inline script and exposed
176 173 as `<html class="ui-mode-desktop">` or `<html class="ui-mode-mobile">`.
177 - Viewport width does NOT switch UI mode — desktop binaries stay desktop
174 + Viewport width does NOT switch UI mode; desktop binaries stay desktop
178 175 even when the window is narrowed.
179 176
180 - §25 Mobile (base pass) — early `.ui-mode-mobile` rules; applies before
177 + §25 Mobile (base pass) | early `.ui-mode-mobile` rules; applies before
181 178 feature CSS so feature rules can override.
182 - §59 Mobile (late pass) — late `.ui-mode-mobile` rules; runs AFTER
179 + §59 Mobile (late pass) | late `.ui-mode-mobile` rules; runs AFTER
183 180 feature CSS so it wins conflicts. Add new
184 181 mobile overrides here unless you need them
185 182 early.
186 - §60 Touch hover disable — `@media (hover: none)`. INPUT-CAPABILITY axis,
183 + §60 Touch hover disable | `@media (hover: none)`. INPUT-CAPABILITY axis,
187 184 not UI mode. Reserved for hover suppression
188 185 only. Never put visibility or layout rules
189 - here — they belong under `.ui-mode-mobile`.
186 + here; they belong under `.ui-mode-mobile`.
190 187
191 188 Intra-mode responsive: `@media (max-width: ...)` queries are allowed
192 189 INSIDE a UI mode (e.g. `.ui-mode-desktop` rules at narrow desktop
193 190 widths). Do not use a media query to switch BETWEEN UI modes.
194 191
195 192 PRINT STRATEGY
196 - §48 Global print rules — early.
197 - §52 Weekly Review print — placed after §49/§50 so V2 grid resets win.
193 + §48 Global print rules | early.
194 + §52 Weekly Review print | placed after §49/§50 so V2 grid resets win.
198 195
199 - ----------------------------------------------------------------------------
200 196 WEEKLY REVIEW V1 + V2 COEXISTENCE
201 197
202 198 §49 (V1: .weekly-review-content, .weekly-review-header, ...) and
@@ -204,7 +200,7 @@
204 200 and mobile.js still render V1 classes; the V2 grid is used by newer
205 201 review surfaces. Do not delete §49 until those JS render paths migrate.
206 202
207 - ============================================================================ */
203 + */
208 204
209 205 /* 1. Font Face */
210 206 @font-face {
@@ -229,14 +225,14 @@
229 225 positioning math in day-planning-render.js / day-planning.js. */
230 226 --timeline-slot-h: 12px;
231 227
232 - /* === NEOBRUTE DESIGN SYSTEM === */
228 + /* --- NEOBRUTE DESIGN SYSTEM */
233 229 /* Clean neobrutalism: bold borders, flat by default, offset shadows on actions + floats */
234 230
235 - /* === INTENT LAYER (themeable) ===
231 + /* --- INTENT LAYER (themeable)
236 232 Resolved intent tokens, applied at runtime by js/themes.js (setProperty
237 233 on :root). Defaults below are the "goingson" titular theme for first
238 - paint; theme-common resolves every theme — including the derived states
239 - (action-hover/active, selection, row-stripe, status surfaces) — so this
234 + paint; theme-common resolves every theme, including the derived states
235 + (action-hover/active, selection, row-stripe, status surfaces), so this
240 236 is one shared mapping across audiofiles / Balanced Breakfast / MNW. */
241 237 --surface-page: #E0E4FA;
242 238 --surface-raised: #FFFFFF;
@@ -264,8 +260,8 @@
264 260 --category-six: #17A2B8;
265 261 --overlay: rgba(1, 1, 8, 0.5);
266 262
267 - /* === APP-LOCAL (not theme intents) ===
268 - GoingsOn's component CSS references the intent tokens above directly —
263 + /* --- APP-LOCAL (not theme intents)
264 + GoingsOn's component CSS references the intent tokens above directly;
269 265 no brand aliases. Only the neobrute border width is app-local now;
270 266 overlay/scrim is a theme intent (theme-common 0.7.0). */
271 267 --border-width: 2px;
@@ -348,7 +344,7 @@ html {
348 344 }
349 345
350 346 /* 4. Utility Classes (Shadow, Hover, Border) */
351 - /* === LAYOUT UTILITIES === */
347 + /* --- LAYOUT UTILITIES */
352 348 /* Compose: .row-flex + .row-flex-{2,3,4} for align-items:center rows.
353 349 .row for raw flex with no align. .flex-1 to grow a child. */
354 350 .flex-1 { flex: 1; }
@@ -359,15 +355,15 @@ html {
359 355 .mb-1 { margin-bottom: 1rem; }
360 356 .mb-2 { margin-bottom: var(--space-2); }
361 357
362 - /* === FORM SIZING UTILITIES === */
358 + /* --- FORM SIZING UTILITIES */
363 359 .form-select--compact { width: auto; min-width: 120px; }
364 360
365 - /* === SETTINGS UTILITIES === */
361 + /* --- SETTINGS UTILITIES */
366 362 .settings-divider { margin-top: 1.5rem; padding-top: 1.5rem; border-top: 2px solid var(--border); }
367 363 .settings-heading { margin-bottom: 1rem; font-family: var(--font-heading); }
368 364 .settings-desc { font-size: 0.875rem; color: var(--content-secondary); margin-bottom: 1rem; }
369 365
370 - /* === SUBTASK UTILITIES === */
366 + /* --- SUBTASK UTILITIES */
371 367 /* .subtask-item is the base row; --linked is a modifier for subtasks that
372 368 reference another task (raised bg + accent border). Use both classes
373 369 together: <div class="subtask-item subtask-item--linked">. */
@@ -403,7 +399,7 @@ body {
403 399 /* Interactive chrome is not text. Buttons, tabs, nav, badges, and labels
404 400 shouldn't show a text selection on press-and-hold (notably the mobile
405 401 bottom tab bar, where a long-press otherwise highlights the label and
406 - you have to dodge it). Content text — inputs, textareas, contenteditable,
402 + you have to dodge it). Content text: inputs, textareas, contenteditable,
407 403 and anything explicitly marked .selectable, stays selectable. */
408 404 .btn,
409 405 .tab,
@@ -1896,7 +1892,7 @@ textarea,
1896 1892 /* Pin a form's action row to the bottom of the modal's scroll area so
1897 1893 primary/cancel buttons stay visible even on short viewports. Without
1898 1894 this, on small windows or iOS with the keyboard up the button row sits
1899 - below the fold and looks cut off — there's no visible scrollbar on
1895 + below the fold and looks cut off; there's no visible scrollbar on
1900 1896 macOS by default so users don't realize they can scroll to it. */
1901 1897 .modal-content > .form-actions:last-child,
1902 1898 .modal-content form > .form-actions:last-child {
@@ -2980,7 +2976,7 @@ textarea,
2980 2976 }
2981 2977
2982 2978 /* Hide the List / Board toggle in mobile UI. Kanban drag-drop has no touch
2983 - fallback (Phase 6 #1) — showing the toggle would expose a broken feature.
2979 + fallback (Phase 6 #1): showing the toggle would expose a broken feature.
2984 2980 Keyed off UI mode rather than `hover: none` so a desktop touch laptop
2985 2981 with a working mouse still gets the toggle. Re-enable when touch-drag lands. */
2986 2982 .ui-mode-mobile #task-view-toggle { display: none; }
@@ -5213,7 +5209,7 @@ kbd {
5213 5209 }
5214 5210
5215 5211 /* Work-hours row in Settings: two small selects with a "to" label
5216 - between. Same lens as .email-filter-row — selects size to their
5212 + between. Same lens as .email-filter-row: selects size to their
5217 5213 short content; the grid columns express the intent explicitly. */
5218 5214 .work-hours-row {
5219 5215 display: grid;
@@ -5230,7 +5226,7 @@ kbd {
5230 5226 flex-shrink: 0;
5231 5227 }
5232 5228
5233 - /* 46. Saved Views Sidebar (desktop UI only — hidden on mobile) */
5229 + /* 46. Saved Views Sidebar (desktop UI only, hidden on mobile) */
5234 5230 .ui-mode-desktop .saved-views-sidebar {
5235 5231 width: 200px;
5236 5232 flex-shrink: 0;
@@ -7704,7 +7700,7 @@ html.ui-mode-mobile, .ui-mode-mobile body {
7704 7700 display: none;
7705 7701 }
7706 7702
7707 - /* === Mobile toolbar grids ===
7703 + /* --- mobile toolbar grids
7708 7704 Toolbar rows become equal-column grids on mobile so children share
7709 7705 the row predictably (no orphans, no ragged wrapping). Search inputs
7710 7706 always span the full grid row. */
@@ -7996,7 +7992,7 @@ html.ui-mode-mobile, .ui-mode-mobile body {
7996 7992 }
7997 7993
7998 7994 /* Mobile uses different modal animations (slide up/down from bottom rather
7999 - than fade-and-scale). @keyframes can't be scoped — defined globally under
7995 + than fade-and-scale). @keyframes can't be scoped; defined globally under
8000 7996 distinct names and applied via animation-name override below. */
8001 7997 @keyframes modalSlideInMobile {
8002 7998 from { transform: translateY(100%); }
@@ -220,7 +220,7 @@ if (window.__TAURI__) {
220 220
221 221 // Cloud sync: subscription required (402 from server)
222 222 listen('sync:subscription-required', () => {
223 - GoingsOn.ui.showToast('Cloud sync paused — subscription required', 'error', {
223 + GoingsOn.ui.showToast('Cloud sync paused: subscription required', 'error', {
224 224 action: { label: 'Subscribe', fn: () => GoingsOn.settings.openCloudSync() },
225 225 duration: 10000,
226 226 });
@@ -123,7 +123,7 @@
123 123 return {
124 124 ok: false,
125 125 field: 'attachments',
126 - message: `Attachments exceed ${formatBytes(ATTACHMENT_HARD_CAP_BYTES)} — remove some files or use a file-share link.`,
126 + message: `Attachments exceed ${formatBytes(ATTACHMENT_HARD_CAP_BYTES)}. Remove some files or use a file-share link.`,
127 127 };
128 128 }
129 129 return { ok: true };
@@ -415,9 +415,9 @@
415 415 : cap.warn ? 'compose-attachment-total over-warn'
416 416 : 'compose-attachment-total';
417 417 const warn = cap.over
418 - ? '<span class="compose-attachment-warn">— most mail servers will reject this. Remove some files or use a file-share link.</span>'
418 + ? '<span class="compose-attachment-warn">. Most mail servers will reject this. Remove some files or use a file-share link.</span>'
419 419 : cap.warn
420 - ? '<span class="compose-attachment-warn">— large attachment; your mail server may reject this.</span>'
420 + ? '<span class="compose-attachment-warn">. Large attachment; your mail server may reject this.</span>'
421 421 : '';
422 422 const totalLine = `<div class="${cls}"><span>Total: ${formatBytes(cap.totalBytes)} / ${formatBytes(ATTACHMENT_HARD_CAP_BYTES)}</span>${warn}</div>`;
423 423 attachmentsBar.classList.remove('hidden');
@@ -103,7 +103,7 @@ async function sendEmail() {
103 103 input,
104 104 delaySeconds: 5,
105 105 });
106 - setStatus(isReply ? 'Reply queued — undo in main window' : 'Email queued — undo in main window', 'success');
106 + setStatus(isReply ? 'Reply queued; undo in main window' : 'Email queued; undo in main window', 'success');
107 107 setTimeout(() => {
108 108 window.__TAURI__.webviewWindow.getCurrentWebviewWindow().close();
109 109 }, 250);
@@ -125,13 +125,13 @@
125 125 // App-password notes shared across providers that require them. The note is shown
126 126 // beneath the email field when the domain is detected; the link opens in the OS
127 127 // browser via the regular anchor (Tauri allows external http(s) targets).
128 - const NOTE_APPLE = 'Apple requires an <strong>app-specific password</strong> for third-party mail apps — your normal iCloud password will not work. Generate one at <a href="https://appleid.apple.com/account/manage" target="_blank" rel="noopener">appleid.apple.com</a> → Sign-In and Security → App-Specific Passwords, then paste it in the Password field below.';
128 + const NOTE_APPLE = 'Apple requires an <strong>app-specific password</strong> for third-party mail apps. Your normal iCloud password will not work. Generate one at <a href="https://appleid.apple.com/account/manage" target="_blank" rel="noopener">appleid.apple.com</a> → Sign-In and Security → App-Specific Passwords, then paste it in the Password field below.';
129 129 const NOTE_FASTMAIL = 'Fastmail requires an <strong>app password</strong> (not your normal password). Create one at <a href="https://app.fastmail.com/settings/security/integrations" target="_blank" rel="noopener">fastmail.com → Settings → Privacy &amp; Security → Integrations</a>, then paste it in the Password field below.';
130 130 const NOTE_GOOGLE = 'Gmail requires an <strong>app password</strong> (your normal Google password will not work). With 2-Step Verification enabled, create one at <a href="https://myaccount.google.com/apppasswords" target="_blank" rel="noopener">myaccount.google.com/apppasswords</a> and paste it below.';
131 131 const NOTE_YAHOO = 'Yahoo requires an <strong>app password</strong>. Create one at <a href="https://login.yahoo.com/account/security" target="_blank" rel="noopener">Yahoo Account Security → Generate app password</a> and paste it below.';
132 132 const NOTE_AOL = 'AOL requires an <strong>app password</strong>. Create one at <a href="https://login.aol.com/account/security" target="_blank" rel="noopener">AOL Account Security → Generate app password</a> and paste it below.';
133 133 const NOTE_OUTLOOK = 'If your Microsoft account has 2-Step Verification on, generate an <strong>app password</strong> at <a href="https://account.microsoft.com/security" target="_blank" rel="noopener">account.microsoft.com/security</a> → Advanced security options. Otherwise your normal password works.';
134 - const NOTE_PROTON = 'Proton Mail does not allow direct IMAP/SMTP — you must run <a href="https://proton.me/mail/bridge" target="_blank" rel="noopener">Proton Bridge</a> locally and use the bridge-generated credentials. Bridge is a paid plan feature.';
134 + const NOTE_PROTON = 'Proton Mail does not allow direct IMAP/SMTP. You must run <a href="https://proton.me/mail/bridge" target="_blank" rel="noopener">Proton Bridge</a> locally and use the bridge-generated credentials. Bridge is a paid plan feature.';
135 135
136 136 const PROVIDER_SETTINGS = {
137 137 'gmail.com': { imap: 'imap.gmail.com', imapPort: 993, smtp: 'smtp.gmail.com', smtpPort: 587, archive: '[Gmail]/All Mail', name: 'Gmail', note: NOTE_GOOGLE },
@@ -173,7 +173,7 @@
173 173
174 174 if (!settings) {
175 175 if (statusEl && domain.includes('.')) {
176 - statusEl.textContent = 'Unknown provider — fill in server details under Advanced settings';
176 + statusEl.textContent = 'Unknown provider: fill in server details under Advanced settings';
177 177 statusEl.style.color = 'var(--content-secondary)';
178 178 }
179 179 if (noteEl) { noteEl.classList.add('hidden'); noteEl.innerHTML = ''; }
@@ -198,7 +198,7 @@
198 198 if (archiveEl && (archiveEl.value === 'Archive' || !archiveEl.value)) archiveEl.value = settings.archive;
199 199
200 200 if (statusEl) {
201 - statusEl.textContent = `Detected ${settings.name || domain} — server settings auto-filled`;
201 + statusEl.textContent = `Detected ${settings.name || domain}: server settings auto-filled`;
202 202 statusEl.style.color = 'var(--success)';
203 203 }
204 204 if (noteEl) {
@@ -355,7 +355,7 @@
355 355 const hasOAuth = oauthProviders.length > 0;
356 356
357 357 const imapIntro = `
358 - <p class="settings-desc">Enter your account details below. Most providers (Gmail, Fastmail, iCloud, Yahoo, Outlook with 2-step verification) require an <strong>app password</strong> instead of your normal password — type your email address and GoingsOn shows the exact link to create one.</p>
358 + <p class="settings-desc">Enter your account details below. Most providers (Gmail, Fastmail, iCloud, Yahoo, Outlook with 2-step verification) require an <strong>app password</strong> instead of your normal password. Type your email address and GoingsOn shows the exact link to create one.</p>
359 359 `;
360 360
361 361 const oauthButtons = hasOAuth
@@ -390,7 +390,7 @@ function queueSend({ input, delaySeconds = 5 }) {
390 390 GoingsOn.api.window.openCompose(prefill).catch(() => openComposeModal(prefill));
391 391 }
392 392 if (hadAttachments) {
393 - GoingsOn.ui.showToast('Re-attach your files — attachments cleared on undo.', 'info', { duration: 8000 });
393 + GoingsOn.ui.showToast('Re-attach your files; attachments cleared on undo.', 'info', { duration: 8000 });
394 394 }
395 395 },
396 396 });
@@ -1016,7 +1016,7 @@
1016 1016 }
1017 1017 const noun = total === 1 ? 'thread' : 'threads';
1018 1018 if (shown < total) {
1019 - el.textContent = `${shown} of ${total} ${noun} — narrow with filters`;
1019 + el.textContent = `${shown} of ${total} ${noun}, narrow with filters`;
1020 1020 el.classList.add('filter-count--capped');
1021 1021 } else {
1022 1022 el.textContent = `${total} ${noun}`;
@@ -130,7 +130,7 @@
130 130 type: 'checkbox',
131 131 label: 'All day',
132 132 value: isAllDay,
133 - hint: 'Removes the time component — the event spans the whole day.',
133 + hint: 'Removes the time component. The event spans the whole day.',
134 134 },
135 135 {
136 136 name: 'title',
@@ -642,7 +642,7 @@
642 642 const past = action === 'delete' ? 'deleted' : 'edited';
643 643 return GoingsOn.ui.showConfirmDialog(
644 644 `${verb} recurring event`,
645 - `This event repeats (${pattern}). The whole series will be ${past} — per-occurrence overrides aren't supported yet.`,
645 + `This event repeats (${pattern}). The whole series will be ${past}. Per-occurrence overrides aren't supported yet.`,
646 646 { confirmText: `${verb} entire series`, cancelText: 'Cancel', danger: action === 'delete' }
647 647 );
648 648 }
@@ -417,18 +417,18 @@
417 417 if (!status.authenticated) {
418 418 dot.classList.add('warn');
419 419 labelText = 'Connect sync';
420 - titleText = 'Cloud Sync — sign in to start syncing';
420 + titleText = 'Cloud Sync: sign in to start syncing';
421 421 } else if (!status.encryptionReady) {
422 422 dot.classList.add('warn');
423 423 labelText = 'Set up encryption';
424 - titleText = 'Cloud Sync — encryption setup needed';
424 + titleText = 'Cloud Sync: encryption setup needed';
425 425 } else {
426 426 dot.classList.add('connected');
427 427 const ago = _formatSyncAgo(status.lastSyncAt);
428 428 labelText = ago ? `Synced ${ago}` : 'Sync ready';
429 429 titleText = ago
430 - ? `Cloud Sync — last sync ${ago}${status.pendingChanges ? ` · ${status.pendingChanges} pending` : ''}`
431 - : 'Cloud Sync — ready';
430 + ? `Cloud Sync: last sync ${ago}${status.pendingChanges ? ` · ${status.pendingChanges} pending` : ''}`
431 + : 'Cloud Sync: ready';
432 432 }
433 433 if (label) label.textContent = labelText;
434 434 indicator.setAttribute('title', titleText);
@@ -392,7 +392,7 @@
392 392 name: 'milestone_id',
393 393 type: 'select',
394 394 label: 'Milestone',
395 - hint: 'Group tasks into project phases — milestones are managed per project',
395 + hint: 'Group tasks into project phases; milestones are managed per project',
396 396 options: [
397 397 { value: '', label: 'No Milestone' },
398 398 ],
@@ -207,7 +207,7 @@
207 207 const date = new Date(inst.completedAt);
208 208 const dateStr = date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
209 209 const time = inst.actualMinutes > 0 ? ` (${inst.actualMinutes}m tracked)` : '';
210 - html += `<div class="task-overview-completion-item">${esc(dateStr)} — completed${esc(time)}</div>`;
210 + html += `<div class="task-overview-completion-item">${esc(dateStr)}: completed${esc(time)}</div>`;
211 211 }
212 212 html += '</div>';
213 213 }
@@ -428,7 +428,7 @@
428 428 const dateStr = start.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
429 429 const timeStr = start.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' });
430 430 const duration = s.durationMinutes != null ? `${s.durationMinutes}m` : 'active';
431 - html += `<div class="task-overview-session">${esc(dateStr)} ${esc(timeStr)} — ${esc(duration)}</div>`;
431 + html += `<div class="task-overview-session">${esc(dateStr)} ${esc(timeStr)}, ${esc(duration)}</div>`;
432 432 }
433 433 html += '</div>';
434 434 }
@@ -42,7 +42,7 @@ const ERROR_CODE_LABELS = {
42 42 const ERROR_CODE_HINTS = {
43 43 VALIDATION_ERROR: 'Check that all required fields are filled in correctly.',
44 44 AUTH_ERROR: 'Check your credentials or reconnect your account in Settings.',
45 - PARSE_ERROR: 'Try a simpler format — e.g. "tomorrow 3pm" or "2026-12-25".',
45 + PARSE_ERROR: 'Try a simpler format, e.g. "tomorrow 3pm" or "2026-12-25".',
46 46 EXTERNAL_SERVICE_ERROR: 'The remote service may be temporarily unavailable. Try again in a moment.',
47 47 CONFLICT: 'This item was modified elsewhere. Reload and try again.',
48 48 };
@@ -268,7 +268,7 @@ pub async fn open_attachment(
268 268 let blob_path = state.data_dir.join("blobs").join(&attachment.blob_hash);
269 269 if !blob_path.exists() {
270 270 return Err(ApiError::bad_request(
271 - "Blob not available locally — sync required",
271 + "Blob not available locally; sync required",
272 272 ));
273 273 }
274 274
@@ -332,7 +332,7 @@ pub async fn save_attachment(
332 332 let blob_path = state.data_dir.join("blobs").join(&attachment.blob_hash);
333 333 if !blob_path.exists() {
334 334 return Err(ApiError::bad_request(
335 - "Blob not available locally — sync required",
335 + "Blob not available locally; sync required",
336 336 ));
337 337 }
338 338
@@ -390,7 +390,7 @@ pub async fn convert_email_attachments(
390 390 tracing::warn!(
391 391 blob_hash = %meta.blob_hash,
392 392 filename = %meta.filename,
393 - "Attachment blob missing — skipping"
393 + "Attachment blob missing; skipping"
394 394 );
395 395 continue;
396 396 }
@@ -453,7 +453,7 @@ pub async fn open_email_blob(
453 453 let blob_path = state.data_dir.join("blobs").join(&blob_hash);
454 454 if !blob_path.exists() {
455 455 return Err(ApiError::bad_request(
456 - "Attachment not available locally — sync required",
456 + "Attachment not available locally; sync required",
457 457 ));
458 458 }
459 459
@@ -504,7 +504,7 @@ pub async fn save_email_blob(
504 504 let blob_path = state.data_dir.join("blobs").join(&blob_hash);
505 505 if !blob_path.exists() {
506 506 return Err(ApiError::bad_request(
507 - "Attachment not available locally — sync required",
507 + "Attachment not available locally; sync required",
508 508 ));
509 509 }
510 510