Skip to main content

max / goingson

UX audit sweep: Tier 1-4 + Tier 6 + design-system charter Result of the Phase 0-7 frontend audit (124 findings, 6 surfaces). Audit reports under docs/ux-audit/; charter at docs/design-system.md; lint guards at scripts/lint-frontend.sh enforce 7 rules. Charter rules now in code: - No state-by-color-alone — sync indicator paired with label; swipe peek-labels carry semantic colors + text. - URL-mirrored filter state — new js/query-state.js helper wired into tasks, emails, contacts; reload / deep-link restores filters. - Bulk-undo everywhere — bulkActionWithUndo used by 7 call sites across tasks, emails, contacts. - No native browser dialogs — showPromptDialog / showConfirmDialog replace prompt / confirm / alert; lint rule blocks regressions. Tier 1 (safety nets): - send-with-delay undo toast (queueSend, already landed with compose stage 1) generalised to all destructive paths via showUndoToast. - Recurring-event scope confirm before edit / delete in events.js. - Attachment 25 MB cap (compose stages 1-2). Tier 2 (trust-state surfaces): - About / version line in Settings. - Pagination + total-count chips on emails / tasks / contacts. - Sync indicator now carries a label, not just a dot color. - Cloud Sync section shows "Signed in as {email} ({username})" via new sync_account_info Tauri command (synckit-client side already supports GET /api/v1/sync/account). Tier 3 (mobile correctness): - Hide kanban on touch devices. - Swipe peek-labels with text + semantic colors. - Bottom-nav long-press → pill picker. - iOS meta tags + theme-color sync from active theme. - window.prompt eradicated (showPromptDialog). Tier 4 (URL-mirrored filter state): - js/query-state.js helper. - Tasks, emails, contacts: filter / search / tag state read from URL on load, written on change; selection clears on filter change so bulk actions can't target hidden rows. Tier 5 polish (the larger Tier 5 features — event snooze + reminders, contact sub-collection edit — landed in their own commits): - All-day events: form checkbox + 00:00 → next-day-00:00 normaliser. - Monthly review: tasks_completed_top (up to 6) for the Accomplished card. - Weekly review: optional weekStart param so the reviewer can navigate to any week, not just the current one. parse_week_start snaps any date to its Monday. upcoming_events block removed (was unused). Tier 6 (architecture): - Side-drawer task detail with J/K keyboard nav + row-active marker. - Settings overlay with Esc / backdrop dismiss. - (Compose unification stages 1-2 already landed; stages 3-6 staged per docs/ux-audit/compose-migration.md.) Mechanical: - Inline-style sweep across ~30 JS files: style="..." → CSS classes, matching the lint rules. styles.css gains the new class library. - Regenerated styles.min.css and Tauri ACL manifest. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-05-20 23:52 UTC
Commit: 47a0908b232b501a6a0b16b517ccd5b743f134c4
Parent: 916d094
68 files changed, +5126 insertions, -1439 deletions
@@ -37,6 +37,8 @@ pub struct MonthlyReviewData {
37 37
38 38 // ===== Stats =====
39 39 pub tasks_completed_count: usize,
40 + /// Up to 6 completed tasks for the Accomplished card.
41 + pub tasks_completed_top: Vec<Task>,
40 42 pub tasks_created_count: usize,
41 43 pub events_count: usize,
42 44 /// Busiest day (most completed tasks).
@@ -206,6 +208,7 @@ pub fn compute_monthly_review(input: MonthlyReviewInput) -> MonthlyReviewData {
206 208
207 209 // Stats
208 210 let tasks_completed_count = input.tasks_completed.len();
211 + let tasks_completed_top: Vec<Task> = input.tasks_completed.iter().take(6).cloned().collect();
209 212 let tasks_created_count = input.tasks_created.len();
210 213 let events_count = input.events.len();
211 214
@@ -243,6 +246,7 @@ pub fn compute_monthly_review(input: MonthlyReviewInput) -> MonthlyReviewData {
243 246 first_day_offset,
244 247
245 248 tasks_completed_count,
249 + tasks_completed_top,
246 250 tasks_created_count,
247 251 events_count,
248 252 busiest_day,
@@ -56,10 +56,6 @@ pub struct WeeklyReviewData {
56 56 pub carried_over_count: usize,
57 57
58 58 // ===== Coming Week =====
59 - /// Count of events in the coming week
60 - pub upcoming_events_count: usize,
61 - /// Events in the coming week
62 - pub upcoming_events: Vec<EventSummary>,
63 59 /// Count of tasks due in the coming week
64 60 pub tasks_due_next_week_count: usize,
65 61 /// Tasks due in the coming week
@@ -156,6 +152,7 @@ pub struct ProjectHealth {
156 152
157 153 /// All data needed to compute the weekly review, pre-fetched by the command layer.
158 154 pub struct WeeklyReviewInput {
155 + pub week_start: NaiveDate,
159 156 pub review: Option<WeeklyReview>,
160 157 pub tasks_completed: Vec<Task>,
161 158 pub tasks_overdue: Vec<Task>,
@@ -178,6 +175,15 @@ pub fn current_week_start() -> NaiveDate {
178 175 today - Duration::days(days_from_monday as i64)
179 176 }
180 177
178 + /// Parses a "YYYY-MM-DD" string into the Monday of that ISO week.
179 + /// Accepts any date in the week and snaps to its Monday, so callers don't
180 + /// have to pre-compute the boundary.
181 + pub fn parse_week_start(s: &str) -> Option<NaiveDate> {
182 + let date = NaiveDate::parse_from_str(s, "%Y-%m-%d").ok()?;
183 + let days_from_monday = date.weekday().num_days_from_monday();
184 + Some(date - Duration::days(days_from_monday as i64))
185 + }
186 +
181 187 /// Gets the Sunday of the week starting on the given Monday.
182 188 pub fn week_end(week_start: NaiveDate) -> NaiveDate {
183 189 week_start + Duration::days(6)
@@ -194,7 +200,7 @@ pub fn format_week_display(start: NaiveDate, end: NaiveDate) -> String {
194 200 ///
195 201 /// This is a pure function — all I/O must be done before calling this.
196 202 pub fn compute_weekly_review(input: WeeklyReviewInput) -> WeeklyReviewData {
197 - let week_start = current_week_start();
203 + let week_start = input.week_start;
198 204 let week_end_date = week_end(week_start);
199 205 let now = Utc::now();
200 206 let today = now.date_naive();
@@ -262,8 +268,6 @@ pub fn compute_weekly_review(input: WeeklyReviewInput) -> WeeklyReviewData {
262 268 carried_over_tasks,
263 269 carried_over_count,
264 270
265 - upcoming_events_count: input.upcoming_events.len(),
266 - upcoming_events: input.upcoming_events.iter().map(event_to_summary).collect(),
267 271 tasks_due_next_week_count: input.tasks_due_next_week.len(),
268 272 tasks_due_next_week: input.tasks_due_next_week,
269 273 tasks_already_overdue_count: input.tasks_already_overdue.len(),
@@ -639,6 +643,8 @@ mod tests {
639 643 external_id: None,
640 644 external_source: None,
641 645 is_read_only: false,
646 + snoozed_until: None,
647 + reminder_offsets_seconds: Vec::new(),
642 648 }
643 649 }
644 650
@@ -0,0 +1,191 @@
1 + # GoingsOn Design System — Charter
2 +
3 + 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.
4 +
5 + For visual specs (colors, sizes, shadows, hover behavior) see `styleguide.md`. This file is the inventory and the rules.
6 +
7 + **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. 9 runtime themes in `src-tauri/frontend/themes/helix/`.
8 +
9 + ---
10 +
11 + ## Token layer — `styles.css :root`
12 +
13 + 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.
14 +
15 + | Axis | Tokens | Notes |
16 + |---|---|---|
17 + | Surface color | `--bg-primary`, `--bg-secondary`, `--bg-tertiary`, `--bg-card`, `--bg-hover` | Themeable |
18 + | Text color | `--text-primary`, `--text-secondary`, `--text-muted`, `--text-on-accent` | Themeable |
19 + | Accent color | `--accent-yellow`, `--accent-green`, `--accent-blue`, `--accent-purple`, `--accent-red`, `--accent-cyan` | Themeable |
20 + | Accent alias | `--accent-color`, `--accent-primary` | Themeable |
21 + | Border | `--border-width` (2px), `--border-width-sm`, `--border-color`, `--border-light` | `--border-color` themeable; widths invariant |
22 + | Shadow | `--shadow-offset-xs/sm/md/lg/xl`, `--shadow-brutal-xs/md/lg/xl` | Theme-invariant (Neobrute signature) |
23 + | Radius | `--radius-xs/sm/md/lg/xl/full` | Invariant |
24 + | Spacing | `--space-1` … `--space-6` (0.25 → 1.5rem) | Invariant |
25 + | Type size | `--font-size-xxs` … `--font-size-4xl` | Invariant |
26 + | Line height | `--line-height-tight/normal/relaxed` | Invariant |
27 + | Font family | `--font-sans`, `--font-serif`, `--font-mono`, `--font-display` | Invariant |
28 + | Layout width | `--width-container`, `--width-modal`, `--width-sidebar` | Invariant |
29 + | Motion | `--transition-fast/normal/slow` | Invariant |
30 + | Cross-layer | `--timeline-slot-h` | Read by `js/day-planning-*` |
31 +
32 + **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.
33 +
34 + ---
35 +
36 + ## Component primitives — canonical class is the contract
37 +
38 + 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.
39 +
40 + ### Button — `.btn`
41 + Variants: `.btn-primary`, `.btn-secondary`, `.btn-danger`, `.btn-icon`, `.btn-text`, `.btn-link`.
42 + Sizes: `.btn-sm` (default size is medium).
43 + State: `.btn-loading`.
44 + **Never** style a `<button>` without `.btn`. **Never** inline a hex color on a button.
45 +
46 + ### Card — `.card`
47 + Sub-parts: `.card-header`, `.card-title`, `.card-description`, `.card-meta`, `.card-badge`.
48 + Container: `.cards-grid`.
49 + Used by: projects-render, contacts-render, dashboard tiles.
50 +
51 + ### Form field — `.form-group`
52 + Sub-parts: `.form-label`, `.form-input | .form-select | .form-textarea`, `.form-actions`, `.form-row`.
53 + 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.
54 +
55 + ### Badge / Tag — `.badge`, `.tag`
56 + Color variant: `[data-color="green|yellow|red|cyan|purple|muted"]`.
57 + Status variant on `.tag`: `.status-active | .status-onhold | .status-archived | .status-inactive | .status-completed`.
58 +
59 + ### Modal — `.modal-overlay` (single global)
60 + 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.
61 +
62 + ### Toast — `.toast`
63 + Variants: `.toast-info`, `.toast-success`, `.toast-error`, `.toast-undo`.
64 + Undo sub-parts: `.undo-message`, `.undo-btn`, `.undo-countdown`.
65 + Show via `GoingsOn.ui.showToast(msg, type, opts)` or `GoingsOn.ui.showUndoToast(...)`.
66 + **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.
67 +
68 + ### Confirm dialog
69 + 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).
70 +
71 + ### Empty state — `.empty-state`
72 + Canonical: `<div class="empty-state"><div class="empty-state-icon">…</div><p class="empty-state-text">…</p><button class="btn btn-primary">…</button></div>`.
73 + Render via `GoingsOn.ui.renderEmptyState(message, buttonLabel?, onClick?)`.
74 + The non-canonical classes `.empty-dashboard-list`, `.kanban-empty`, `.virtual-scroller-empty` are **deprecated**; consolidate to `.empty-state` with size modifiers in remediation.
75 +
76 + ### Skeleton / loading
77 + Classes: `.skeleton-shimmer`, `.skeleton-row`, `.skeleton-lines`, `.skeleton-line.long | .medium | .short`, `.spinner`, `.loading`.
78 + 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()`.
79 +
80 + ### Context menu — `.context-menu`
81 + State: `.visible`. Items: `.context-menu-item` (+ `--danger`), `.context-menu-separator`, `.context-menu-header`.
82 + Open via `GoingsOn.ui.showContextMenu(x, y, items)`.
83 +
84 + ### Tab / pill nav — `.tab-navigation` / `.pill-nav`
85 + Active state: `.tab.active` / `.pill.active`. Used in shell (`index.html`) only — feature modules should not introduce new tab styles.
86 +
87 + ### Filter bar — `.filter-bar`
88 + Children: `.filter-select`, `.filter-checkbox`. Used in tasks and emails filter rows.
89 +
90 + ### Progress bar — `.progress-bar-container` + `.progress-bar`
91 + Used in tasks (subtask completion) and reviews.
92 +
93 + ### Row primitives
94 +
95 + | Kind | Canonical class | Render helper (today) |
96 + |---|---|---|
97 + | Task row | `.task-row` (in `.task-table`) | `renderTaskRow(t, index)` — `tasks-render.js` |
98 + | Event row | `.event-row-virtual` (in `.event-table-virtual`) | inline in `events.js` |
99 + | Project card | `.card` (in `.cards-grid`) | inline in `projects-render.js` |
100 + | Contact card | `.card.contact-card` | inline in `contacts-render.js` |
101 + | Email row | `.email-item` (in `.email-list`) | inline in `emails.js` |
102 +
103 + **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.
104 +
105 + ### Task row state classes (composed onto `.task-row`)
106 + `.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`.
107 +
108 + ### Bulk selection
109 + Bar: `.bulk-actions-bar`. Controls: `.bulk-checkbox`, `.bulk-select-all`, `.bulk-count`. Row state: `.selected`, `.keyboard-selected`.
110 +
111 + ### Kanban — `.kanban-board`
112 + Children: `.kanban-column`, `.kanban-card`, `.kanban-card-empty`. Used only by tasks-kanban view.
113 +
114 + ### Day-plan timeline — `.timeline-slot`
115 + Blocks: `.time-block`, `.block-focus`, `.block-personal`. Slot height read from `--timeline-slot-h`.
116 +
117 + ### Weekly review grid — `.weekly-grid`
118 + Cells: `.weekly-cell`, `.weekly-day-header`.
119 +
120 + ### Subtasks — `.subtask-item`
121 + Variant: `.subtask-item-linked` (left-border indicator for linked task). Children: `.subtask-checkbox`, `.subtask-text-done`.
122 +
123 + ### Shell — `.app-header` / `.app-body` / `.main-content` / `.page-header` / `.page-title`
124 + Feature modules do not redefine shell classes.
125 +
126 + ---
127 +
128 + ## Theme contract — `js/themes.js`
129 +
130 + 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`).
131 +
132 + **Rules:**
133 + 1. A theme overrides **color tokens only**. Spacing, radius, shadow offsets, type are theme-invariant.
134 + 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.
135 + 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.
136 +
137 + ---
138 +
139 + ## Inline-style rules
140 +
141 + 1. `style="display:none"` in HTML is allowed only on the modal overlay and similar shell-level slots; feature views use `.hidden`.
142 + 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.
143 + 3. No hex literal in any file outside `styles.css` and `themes/*.toml`.
144 + 4. No `var(--token, #fallback)` — the fallback defeats theming.
145 +
146 + ---
147 +
148 + ## Cross-cutting rules
149 +
150 + These apply across every surface. Violations caught by reviewer checklist or `scripts/lint-frontend.sh`. Derived from the Phase 7 audit roll-up (`docs/ux-audit/phase-7.md`) where each rule's evidence is cited.
151 +
152 + ### State communication
153 + 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.)
154 +
155 + ### Filter & view state in the URL
156 + 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.)
157 +
158 + ### Bulk operations always undoable
159 + 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.)
160 +
161 + ### Native dialogs forbidden
162 + `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.
163 +
164 + ### Mobile is responsive CSS by default
165 + 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.)
166 +
167 + ### Multi-step flows show progress
168 + 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.
169 +
170 + ### Action bars cap at 5 visible
171 + A horizontal action bar has at most 5 visible actions; the rest live in an overflow `Actions ▾` menu. Primary actions get `.btn-primary`; destructive actions go in the overflow.
172 +
173 + ### Justified touch branches
174 + Modules with `isTouchDevice` branches must include a top-of-file comment naming what the branch does and why CSS-only isn't sufficient.
175 +
176 + ---
177 +
178 + ## Success criteria for remediation (input to the pre-Phase-1 plan)
179 +
180 + Phase 1 surface audits start when **all** of the following are true:
181 +
182 + - `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).
183 + - `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.
184 + - `showToast` injects no `style.cssText`. All toast positioning, color, and shadow live on `.toast` + variant classes in `styles.css`.
185 + - Grep `\bstyle="` across `src-tauri/frontend/` returns no color, shadow, border, or font value; only visibility / layout micro-tweaks (and ideally none of those).
186 + - Grep `#[0-9a-fA-F]{3,8}` across `src-tauri/frontend/js/` and `src-tauri/frontend/*.html` returns zero matches.
187 + - 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.
188 + - Every color custom property in `styles.css :root` is either mapped in `js/themes.js` or carries a `/* theme-invariant */` comment.
189 + - `window.confirm()` calls: zero. All confirms route through `GoingsOn.ui.showConfirmDialog`.
190 +
191 + When all criteria hold, Phase 1 (Shell & navigation) may begin.
@@ -19,17 +19,20 @@ Public-launch blockers. Everything else in this file is post-launch. Do not prom
19 19 - [ ] Windows: `cargo tauri build` → verify `.msi`, code-sign with Authenticode, test on Windows
20 20 - [ ] Linux: AppImage (x86_64 + aarch64)
21 21
22 - ### Mobile (iOS TestFlight — keep in scope, near completion)
22 + ### Mobile (iOS TestFlight Internal only — external + Android deferred post-launch)
23 23 See `todo_mobile.md` for full breakdown. Launch requires:
24 24 - [x] iOS TestFlight upload working — v0.3.3 delivered 2026-05-17 (icon fix; v0.3.1 had Tauri default logo)
25 - - [ ] Android emulator smoke (`cargo tauri android dev`) + CRUD verified on mobile WebView
26 - - [ ] Physical device testing (iOS + Android) — run mobile P0+P2 checklist in `docs/human_testing.md`
27 - - [ ] Safe area insets across device models, keyboard-doesn't-obscure-inputs, background/foreground transitions
28 - - [ ] iOS internal testing: invite phone Apple ID into ASC Users and Access (Developer role), add to TestFlight Internal group, install + smoke-test on v0.3.3
25 + - [x] iOS internal testing: phone Apple ID invited + added to TestFlight Internal group, build installed on own device (2026-05-19)
26 + - [ ] Physical device testing (iOS) — run mobile P0+P2 checklist in `docs/human_testing.md`
27 + - [ ] Safe area insets across device models, keyboard-doesn't-obscure-inputs, background/foreground transitions — verify on installed build
29 28 - [ ] Resolve Missing Compliance prompt on v0.3.3 build (encryption answer — HTTPS-only exempt). **Plist key added 2026-05-18** in `gen/apple/goingson-desktop_iOS/Info.plist` (`ITSAppUsesNonExemptEncryption = false`); future uploads won't re-prompt. The currently-stuck v0.3.3 build still needs the answer set once via App Store Connect UI (TestFlight → build → Encryption: "Uses only exempt encryption").
30 - - [ ] Add Privacy Policy page to GoingsOn project on MNW (URL: `https://makenot.work/p/goingson#section-privacy-policy`)
31 - - [ ] iOS External Testing: add build, fill Beta App Information, submit for Beta App Review, enable Public Link
32 - - [ ] Android release: Google Play Developer account ($25), release AAB, Play Console listing, submit
29 +
30 + Post-launch (out of launch scope):
31 + - [ ] (post-launch) Android emulator smoke (`cargo tauri android dev`) + CRUD verified on mobile WebView
32 + - [ ] (post-launch) Physical device testing (Android)
33 + - [ ] (post-launch) Add Privacy Policy page to GoingsOn project on MNW (URL: `https://makenot.work/p/goingson#section-privacy-policy`)
34 + - [ ] (post-launch) iOS External Testing: add build, fill Beta App Information, submit for Beta App Review, enable Public Link
35 + - [ ] (post-launch) Android release: Google Play Developer account ($25), release AAB, Play Console listing, submit
33 36
34 37 Explicitly **out of launch scope** (defer until after public launch):
35 38 - All of Sprint: Backup & Export (current backup works for primary data types)
@@ -41,6 +44,47 @@ Explicitly **out of launch scope** (defer until after public launch):
41 44
42 45 ---
43 46
47 + ## UX audit sweep (phased, post-launch)
48 +
49 + Audit complete 2026-05-19/20. **124 findings** across 6 surfaces, **4 charter rules** promoted, lint guards enforce 7 rules clean. See `docs/ux-audit/phase-7.md` for the roll-up; per-phase reports at `docs/ux-audit/phase-{0..7}.md`; charter at `docs/design-system.md`. Audit phases archived to `todo_done.md`.
50 +
51 + ### Audit backlog execution (Tier 1–6, per `phase-7.md` Part C)
52 +
53 + - [x] **Tier 1 — safety nets** (5/5). Bulk-undo across tasks/emails/contacts; selection clears on filter change; send-with-delay (undo-send); attachment-size warning at 25 MB; recurring-event scope confirm.
54 + - [x] **Tier 2 — trust-state surfaces** (4/4). Sync indicator label + state-by-color-alone fix; About / version in Settings; pagination + total counts; Account section now surfaces "Signed in as {email} ({username})" inside Cloud Sync via `sync_account_info`.
55 + - [x] **Tier 3 — mobile correctness** (5/5). Hide Kanban on touch; swipe peek-labels; bottom-nav long-press → pill picker; iOS meta tags + theme-color sync; `window.prompt` killed.
56 + - [x] **Tier 4 — URL-mirrored filter state** (1/1). `js/query-state.js` helper; wired into tasks, emails, contacts. Closes Phase 1 #9 / Phase 2 #10 / Phase 3 #8 / Phase 5 search-persistence in one helper.
57 + - [x] **Tier 5 — feature gaps** (4/4). All-day events, event snooze, contact sub-collection edit, and event reminders all wired end-to-end.
58 + - [~] **Tier 6 — architecture** (2.17/3). Side-drawer task detail (`#task-detail-drawer`, J/K keyboard nav, row-active marker) and settings overlay (`#settings-overlay`, Esc-to-close, backdrop dismiss) both landed 2026-05-20. Compose unification staged out in `docs/ux-audit/compose-migration.md`; **stage 1 of 6 landed 2026-05-20** — new `js/compose-form.js` owns the SendEmailInput payload shape, attachment caps, and validation. Both modal (`emails.js`) and desktop window (`compose.html`) call into it; the `to` vs `toAddress` double-emit shim is gone. Stages 2-6 are independently shippable; one per release during soft launch.
59 +
60 + **Backend trio + frontend wirings landed 2026-05-20** (migration 049, +9 Tauri commands, 4 new tests, ~3 frontend surfaces):
61 + - [x] Event snooze: migration adds `snoozed_until` + rebuilt sync triggers. `snooze_event` / `unsnooze_event` / `list_snoozed_events` commands. `EventResponse` exposes `isSnoozed` + `snoozedUntil`. Event detail modal has Snooze / Unsnooze toggle + "Snoozed until …" line. `snooze.js` generalised to three item types via `ITEM_LABEL` / `apiFor()` / `reloadFor()` helpers.
62 + - [x] Contact sub-collection edit: `update_contact_{email,phone,social_handle,custom_field}` commands. `SUB_COLLECTIONS` map extended with `updateCommand` + `prefill`; `buildSubCollectionFormHtml(type, cid, editingId?)` routes add vs edit. Edit-contact modal rows now carry inline Edit + Remove buttons. Pre-existing reload race (`load()` not awaited before `open*()`) fixed inline.
63 + - [x] MNW account info: new `GET /api/v1/sync/account` (server) + `get_account_info()` (synckit-client) + `sync_account_info` Tauri command. Surfaced inline in Cloud Sync (chose not to add a separate sidebar item — the data only exists because of sync).
64 +
65 + **Event reminders landed 2026-05-20** (migration 050, scheduler in `notifications.rs`, 12 new tests):
66 + - [x] Schema: `reminder_offsets_seconds TEXT` JSON column on events + rebuilt sync triggers + `apply.rs` column list.
67 + - [x] Scheduler: `check_event_reminders` runs each 60s tick; tracks `(event_id, offset)` pairs; bootstrap-on-first-tick suppresses backfill spam after app restart; skips snoozed events.
68 + - [x] UI: `REMINDER_PRESETS` (At time / 5m / 15m / 30m / 1h / 1d before) checkbox group in new/edit event forms; "Reminders: …" line on event detail modal.
69 + - [x] `sanitize_reminder_offsets` strips negatives, dedupes, sorts, caps at 8.
70 + - Known limitations (post-launch follow-ups, not blockers): recurring events fire reminders only against the template's anchor `start_time`, not virtual instances; reminder fire state is in-memory (close the app around a fire time and you miss it).
71 +
72 + **Tier 5 follow-up worth doing if you ship more broadly:**
73 + - [ ] Snoozed-events list view (the `list_snoozed_events` endpoint exists; nothing renders it yet — parity with the snoozed-tasks list).
74 +
75 + ### Charter rules now enforced in code (not just docs)
76 + 1. No state-by-color-alone (sync indicator paired with label; swipe peek-labels carry semantic colors + text)
77 + 2. URL-mirrored filter state (tasks/emails/contacts)
78 + 3. Bulk-undo everywhere (7 call sites)
79 + 4. No native browser dialogs (`scripts/lint-frontend.sh` rule #7; `showPromptDialog` helper)
80 +
81 + ### Deferred deliberately
82 + - [ ] **Step 4 — `renderRow` primitive — DEFERRED.** Re-evaluate if a future phase identifies parallel-markup as the real friction; until then, per-kind row renderers are fine (they all consume canonical CSS classes). See `docs/ux-audit/remediation-plan.md`.
83 +
84 + Out of scope for the audit: smart-feature behavior (forbidden by Apps/CLAUDE.md), full WCAG audit, performance. Recommendations are post-launch work; nothing here blocks ship.
85 +
86 + ---
87 +
44 88 ## Sprint: Backup & Export
45 89
46 90 Backup system has both coverage and performance problems (Run 24 cross-cutting concern).
@@ -236,3 +236,34 @@ Audit run: `/code-fuzz goingson`. 8 serious, 10 minor. 7/8 serious fixed, 7/10 m
236 236 - [x] Attachment sending — MIME multipart via lettre, file picker in compose window + modal, multiple files
237 237 - [x] Labels / folders — local labels (migration 043), folder/label filter dropdowns, move to folder (IMAP + local), label editing
238 238 - [x] Notifications — per-account opt-in (migration 044), off by default, fires from auto-sync scheduler when new emails saved
239 +
240 + ---
241 +
242 + ## UX Audit sweep — Phase 0 through Phase 7 (2026-05-19/20)
243 +
244 + Multi-phase audit of the frontend. All artifacts under `docs/ux-audit/`; charter at `docs/design-system.md`; lint guards at `scripts/lint-frontend.sh`.
245 +
246 + **Phase 0 + remediation:**
247 + - [x] Phase 0 — design-system conformance audit. `docs/ux-audit/phase-0.md` + `docs/design-system.md`.
248 + - [x] Consolidation pre-plan. `docs/ux-audit/remediation-plan.md` — 10 steps.
249 + - [x] Step 1 — Toast styling moved into CSS.
250 + - [x] Step 2 — Updater/keycaps/var-fallback hex removed.
251 + - [x] Step 3 — `renderFormField` primitive + form-modal/settings/settings-sync/email-accounts migrated.
252 + - [x] Step 5 — Empty-state consolidation.
253 + - [x] Step 6 — Layout utilities + style-attr sweep; lint reports clean.
254 + - [x] Step 7 — `compose.html` embedded styles reconciled.
255 + - [x] Step 8 — Theme coverage sweep. `docs/ux-audit/theme-coverage.md`.
256 + - [x] Step 9 — `window.confirm()` calls removed.
257 + - [x] Step 10 — Lint guards at `scripts/lint-frontend.sh` (7 rules).
258 + - Step 4 (`renderRow`) deliberately deferred — kept active in todo.md.
259 +
260 + **Surface audits (124 total findings across 6 surfaces):**
261 + - [x] Phase 1 — Shell & navigation. 17 findings (3 critical).
262 + - [x] Phase 2 — Tasks surface. 18 findings (3 critical).
263 + - [x] Phase 3 — Compose & email. 21 findings (3 critical).
264 + - [x] Phase 4 — Events & calendar. 23 findings (4 critical).
265 + - [x] Phase 5 — Projects, contacts, settings. 23 findings (4 critical).
266 + - [x] Phase 6 — Mobile parity sweep. 22 findings (3 critical). Verdict: mobile is ~70 % parallel implementation, ~30 % CSS restyle.
267 + - [x] Phase 7 — Cross-cutting + theme conformance + roll-up. 4 meta-patterns promoted to charter rules (state-by-color-alone, URL-mirrored filter state, bulk-undo, native-dialogs-forbidden). Landed in this commit: `GoingsOn.ui.showPromptDialog`, `bulkTag` off `window.prompt`, `no-native-dialogs` lint rule, cross-cutting rules section in `design-system.md`.
268 +
269 + Recommendations are post-launch; nothing in the audit blocks ship. Tier 1–6 ship order in `phase-7.md` Part C.
@@ -1,17 +1,17 @@
1 1 # GoingsOn - Mobile Port
2 2
3 - Done: Phases 1-7 (CSS, touch, navigation, views, build config, tab bar, distribution setup); touch-native UX rework (paint, reschedule, week carousel); polish (tap targets, scroll lock, modal cutoffs, icon, provider hints, segmented events view). Active: None. Next: Internal TestFlight install + smoke test on a real device.
3 + Done: Phases 1-7 (CSS, touch, navigation, views, build config, tab bar, distribution setup); touch-native UX rework (paint, reschedule, week carousel); polish (tap targets, scroll lock, modal cutoffs, icon, provider hints, segmented events view); iOS TestFlight internal install on phone (2026-05-19). Active: None. External TestFlight + Android deferred post-launch.
4 4
5 5 ---
6 6
7 7 ## Remaining
8 8
9 9 ### Build & Test
10 - - [ ] Test on Android emulator (`cargo tauri android dev`)
11 - - [ ] All CRUD operations verified on mobile WebView
10 + - [ ] (post-launch) Test on Android emulator (`cargo tauri android dev`)
11 + - [ ] All CRUD operations verified on mobile WebView (iOS — on installed TestFlight build)
12 12
13 13 ### Polish
14 - - [ ] Physical device testing (iOS + Android)
14 + - [ ] Physical device testing (iOS); (post-launch) Android
15 15 - [x] Safe area insets on various device models (notched, non-notched) — 2026-05-16: timer-widget bottom now uses `calc(52px + env(safe-area-inset-bottom))`; body + fixed UI respect `safe-area-inset-left/right` for landscape. Verify on device.
16 16 - [x] Virtual scroller performance on mobile — 2026-05-16: short-circuit re-renders when visible range unchanged; removed redundant O(N) walks per scroll tick. Verify on device with long lists.
17 17 - [x] VoiceOver / TalkBack accessibility — 2026-05-16: `mobile-more-btn` gets `aria-expanded`/`aria-haspopup`/`aria-controls`; popover is `role="menu"` with `aria-hidden` toggle; action sheet now traps focus + restores on close + closes on Escape. Spot-check with VoiceOver.
@@ -60,31 +60,31 @@ Mobile UX audit 2026-05-17 — desktop interactions that don't translate.
60 60 - [x] Answer encryption export-compliance prompt — set via API to `usesNonExemptEncryption: false` (exempt; standard HTTPS/system crypto)
61 61
62 62 #### Internal testing (immediate, for own device)
63 - - [ ] App Store Connect → Users and Access → invite phone's Apple ID (role: Developer)
64 - - [ ] Accept invitation from phone's Apple ID
65 - - [ ] TestFlight → Internal Testing → "+" group → add phone Apple ID → add build
66 - - [ ] Install TestFlight on phone, accept invite, install build, smoke-test
63 + - [x] App Store Connect → Users and Access → invite phone's Apple ID (role: Developer)
64 + - [x] Accept invitation from phone's Apple ID
65 + - [x] TestFlight → Internal Testing → "+" group → add phone Apple ID → add build
66 + - [x] Install TestFlight on phone, accept invite, install build, smoke-test
67 67
68 - #### External testing (public link, slower first time)
68 + #### External testing (public link, slower first time) — POST-LAUNCH
69 69 - [x] Privacy policy drafted at `docs/privacy-policy.md`
70 - - [ ] Add Privacy Policy page to GoingsOn project on MNW (dashboard → Settings → Pages); URL will be `https://makenot.work/p/goingson#section-privacy-policy`. Pages feature shipped in MNW v0.5.17.
71 - - [ ] TestFlight → External Testing → "+" group → add v0.3.1 build
72 - - [ ] Fill in Beta App Information: description, feedback email, test notes ("no login required for core features")
73 - - [ ] Submit for Beta App Review (first time: ~24-48h; subsequent builds: minutes via automated screening)
74 - - [ ] Once approved, enable Public Link
75 - - [ ] Push at least one build per quarter — TestFlight builds expire after 90 days
76 -
77 - ### Android
70 + - [ ] (post-launch) Add Privacy Policy page to GoingsOn project on MNW (dashboard → Settings → Pages); URL will be `https://makenot.work/p/goingson#section-privacy-policy`. Pages feature shipped in MNW v0.5.17.
71 + - [ ] (post-launch) TestFlight → External Testing → "+" group → add v0.3.1 build
72 + - [ ] (post-launch) Fill in Beta App Information: description, feedback email, test notes ("no login required for core features")
73 + - [ ] (post-launch) Submit for Beta App Review (first time: ~24-48h; subsequent builds: minutes via automated screening)
74 + - [ ] (post-launch) Once approved, enable Public Link
75 + - [ ] (post-launch) Push at least one build per quarter — TestFlight builds expire after 90 days
76 +
77 + ### Android — POST-LAUNCH
78 78 ```bash
79 79 ./dist/release-android.sh # release AAB
80 80 ./dist/release-android.sh --apk # release APK
81 81 ./dist/release-android.sh --debug # debug APK
82 82 ```
83 - - [ ] Google Play Developer account ($25 one-time)
84 - - [ ] Release AAB build + test
85 - - [ ] Play Console listing (title, description, screenshots)
86 - - [ ] Test on emulator + physical device
87 - - [ ] Play Store submission
83 + - [ ] (post-launch) Google Play Developer account ($25 one-time)
84 + - [ ] (post-launch) Release AAB build + test
85 + - [ ] (post-launch) Play Console listing (title, description, screenshots)
86 + - [ ] (post-launch) Test on emulator + physical device
87 + - [ ] (post-launch) Play Store submission
88 88
89 89 ---
90 90
@@ -0,0 +1,148 @@
1 + # Phase 0 — Design-System Conformance Audit
2 +
3 + **Scope:** Inventory of the design primitives the GoingsOn Tauri webview actually uses today, divergence map of where modules reinvent them, and gaps that block consistency. Surface audits (Phase 1+) do not start until the consolidation pre-plan derived from this report lands.
4 +
5 + **Stack:** Tauri 2 webview, vanilla HTML / CSS / JS (no HTMX, no Askama, no framework). 46 IIFE modules under the `GoingsOn.*` namespace. CSS in `src-tauri/frontend/css/styles.css`. 9 runtime-swappable themes under `src-tauri/frontend/themes/helix/`.
6 +
7 + ---
8 +
9 + ## (a) Actual primitive set
10 +
11 + ### CSS custom properties — `styles.css` `:root` (lines 91–196, ~60 tokens)
12 +
13 + | Group | Tokens |
14 + |---|---|
15 + | Color — surface | `--bg-primary`, `--bg-secondary`, `--bg-tertiary`, `--bg-card`, `--bg-hover` |
16 + | Color — text | `--text-primary`, `--text-secondary`, `--text-muted`, `--text-on-accent` |
17 + | Color — accent | `--accent-yellow`, `--accent-green`, `--accent-blue`, `--accent-purple`, `--accent-red`, `--accent-cyan`, plus aliases `--accent-color`, `--accent-primary` |
18 + | Border | `--border-width` (2px), `--border-width-sm`, `--border-color`, `--border-light` |
19 + | Shadow | `--shadow-offset-xs` … `--shadow-offset-xl`; compound `--shadow-brutal-xs/md/lg/xl` (offset-only, no blur — the Neobrute signature) |
20 + | Radius | `--radius-xs/sm/md/lg/xl/full` |
21 + | Spacing | `--space-1` … `--space-6` (0.25rem → 1.5rem) |
22 + | Type — size | `--font-size-xxs` … `--font-size-4xl` |
23 + | Type — line | `--line-height-tight/normal/relaxed` |
24 + | Type — family | `--font-sans`, `--font-serif`, `--font-mono`, `--font-display` (Reglo) |
25 + | Layout | `--width-container` (1400px), `--width-modal` (560px), `--width-sidebar` (280px) |
26 + | Motion | `--transition-fast` (0.1s), `--transition-normal` (0.15s), `--transition-slow` (0.3s) |
27 + | Overlay | `--overlay-color` |
28 + | Cross-layer | `--timeline-slot-h` (12px) — read by JS for day-plan render |
29 +
30 + ### Component classes — counted ~594 selectors in `styles.css`
31 +
32 + Canonical primitives (these are the ones every JS render module should target):
33 +
34 + - **Button** — `.btn`, `.btn-primary`, `.btn-secondary`, `.btn-danger`, `.btn-sm`, `.btn-icon`, `.btn-loading`, `.btn-text`, `.btn-link`
35 + - **Card** — `.card`, `.card-header`, `.card-title`, `.card-description`, `.card-meta`, `.card-badge`, `.cards-grid`
36 + - **Form field** — `.form-group`, `.form-label`, `.form-input`, `.form-select`, `.form-textarea`, `.form-actions`, `.form-row`
37 + - **Badge / tag** — `.badge`, `.tag`, `[data-color="green|yellow|red|cyan|purple|muted"]`; status variants `.tag.status-active|onhold|archived|inactive|completed`
38 + - **Modal** — `.modal-overlay`, `.modal-container` (+ `.modal-large`), `.modal-header`, `.modal-title`, `.modal-content`, `.modal-close`; visibility via `.hidden` and `.closing`
39 + - **Toast** — `.toast`, `.toast-info`, `.toast-success`, `.toast-error`, `.toast-undo`; undo sub-parts `.undo-message`, `.undo-btn`, `.undo-countdown`
40 + - **Tab / pill** — `.tab-navigation`, `.tab.active`; `.pill-nav`, `.pill.active`
41 + - **Filter** — `.filter-bar`, `.filter-select`, `.filter-checkbox`
42 + - **Context menu** — `.context-menu(.visible)`, `.context-menu-item(--danger)`, `.context-menu-separator`, `.context-menu-header`
43 + - **Empty state** — `.empty-state` + `.empty-state-text` + `.empty-state-icon`
44 + - **Skeleton** — `.skeleton-shimmer`, `.skeleton-row`, `.skeleton-lines`, `.skeleton-line.long|.medium|.short`, `.spinner`, `.loading`
45 + - **Progress** — `.progress-bar-container`, `.progress-bar`
46 + - **Table — generic** — `.data-table`
47 + - **Table — task** — `.task-table`, `.task-header-row`, `.task-row`, `.task-cell`, plus row state classes `.task-overdue`, `.task-completed`, `.task-started`, `.task-snoozed`, `.priority-high|medium|low`, `.task-timer-active`, `.task-time-badge.over-estimate`
48 + - **Table — event** — `.event-table(-virtual)`, `.event-header-row`, `.event-row-virtual`
49 + - **Kanban** — `.kanban-board`, `.kanban-column`, `.kanban-card(-empty)`
50 + - **Day-plan timeline** — `.timeline-slot`, `.time-block`, `.block-focus`, `.block-personal`
51 + - **Weekly review** — `.weekly-grid`, `.weekly-cell`, `.weekly-day-header`
52 + - **Subtasks** — `.subtask-item(-linked)`, `.subtask-checkbox`, `.subtask-text-done`
53 + - **Bulk** — `.bulk-actions-bar`, `.bulk-checkbox`, `.bulk-count`, `.bulk-select-all`; selection state `.selected`, `.keyboard-selected`
54 + - **Shell** — `.app-header`, `.app-body`, `.main-content`, `.page-header`, `.page-title`
55 +
56 + ### JS-rendered markup helpers (the *render* primitives)
57 +
58 + - `GoingsOn.ui.openModal(title, html, opts)` — single global `#modal-overlay`, content swapped via `.innerHTML`; supports `.modal-large`; handles Escape, click-outside, focus trap.
59 + - `GoingsOn.ui.showToast(message, type, opts)` — dynamically creates and appends a `.toast.toast-${type}` to `document.body`.
60 + - `GoingsOn.ui.showConfirmDialog(title, message, opts)` and `GoingsOn.ui.confirmDelete(name, action)` — render into the global modal.
61 + - `GoingsOn.ui.showUndoToast(...)` — `.toast-undo` with countdown.
62 + - `GoingsOn.ui.renderEmptyState(message, buttonLabel?, onClickFn?)` — returns `.empty-state` element.
63 + - `GoingsOn.ui.showContextMenu(x, y, items)` — builds `.context-menu` from `{ icon, label, shortcut, subtitle, danger }` items.
64 + - **Missing helpers:** no `renderRow(...)` (each module hand-rolls), no `renderBadge(...)`, no `renderFormField(...)` (form-modal.js builds inline), no skeleton helper.
65 +
66 + ### Themes
67 +
68 + 9 TOML files under `src-tauri/frontend/themes/helix/` (catppuccin × 4, ayu_light, dracula, flatwhite, nord, tokyonight). Each has a Helix-style `[palette]` block and UI-key references. `js/themes.js` maps 13 dotted TOML paths → CSS custom properties at runtime; selection persists to `localStorage` (`goingson-theme`); system preference picks default (`catppuccin-mocha` dark / `neobrute` light). Tokens overridden are **color tokens only** — every other axis (spacing, type, radius, shadow offsets) is theme-invariant.
69 +
70 + ---
71 +
72 + ## (b) Divergence map
73 +
74 + ### Inline `style="…"` — 27 in HTML, 335+ in JS
75 +
76 + Top offenders by file (JS):
77 + - `email-accounts.js` — 51
78 + - `settings-sync.js` — 31
79 + - `settings.js` — 25
80 + - `app.js` — 18 (a `kbdStyle` constant repeated everywhere a keycap is rendered)
81 + - `tasks-render.js` — 15 (subtask modal layout, progress bars)
82 +
83 + Three flavors observed: (1) **visibility control** (`display:none|flex`) — acceptable, but should migrate to `.hidden` / `.is-flex` utility classes for grep-ability; (2) **layout micro-tweaks** (`display:flex; gap:.5rem`) — should become utility classes or a `.stack` / `.row` primitive; (3) **state-keyed colors and backgrounds** — the worst category; couples runtime values to literal hex.
84 +
85 + ### Raw hex outside `styles.css` / `themes/`
86 +
87 + - `components-modal.js` — toast fallbacks `#22c55e`, `#ef4444`, `#c00` injected via `style.cssText`. *These bypass theme switching*: a user on Catppuccin Mocha still sees Tailwind-green success toasts.
88 + - `updater.js` — `#6c5ce7`, `#444`, `#aaa`, `#2d2d2d` for the Tauri updater overlay.
89 + - `keyboard.js` — keycap chrome.
90 + - `emails.js` — `var(--accent-green, #22c55e)` pattern (variable + fallback). Better than raw hex but the fallback still drifts from the token palette.
91 +
92 + ### Duplicated row-rendering recipes
93 +
94 + Each list view hand-writes its row markup:
95 + - `tasks-render.js:renderTaskRow(t, index)` — 7-cell grid with status/state classes.
96 + - `projects-render.js` — `.card` grid cards.
97 + - `contacts-render.js` — `.card.contact-card` with avatar + initials.
98 + - `events.js` — virtual `.event-row-virtual` cells.
99 + - `emails.js` — `.email-list` + `.email-item`.
100 +
101 + No shared `renderRow(kind, model)` exists. The grids share visual language (status pill, primary text, secondary text, right-aligned actions, kebab menu) but each implementation reinvents the slot layout.
102 +
103 + ### Duplicated empty-state markup
104 +
105 + Three canonical forms in use:
106 + - `.empty-state` + `.empty-state-text` (canonical — `components.js` exposes a helper).
107 + - `.empty-dashboard-list` (projects detail).
108 + - `.kanban-empty` (kanban board).
109 + - `.virtual-scroller-empty` (virtual list fallback).
110 +
111 + Several call sites build the empty state inline with `style="padding: 1rem; text-align: center"` rather than calling `renderEmptyState()`. `events.js` has `<div class="empty-state">No events scheduled.</div>` with extra inline padding — a near-canonical that drifted.
112 +
113 + ### Duplicated toast / confirm recipes
114 +
115 + All toasts pass through `showToast()`, **but `showToast` itself injects inline positioning + color via `style.cssText`** — so even the canonical helper is itself a divergence source. Every confirm dialog goes through `showConfirmDialog()` but the markup contains inline button styles in at least one branch (`components-modal.js`).
116 +
117 + ### Mobile vs desktop divergence
118 +
119 + `mobile.js` is a layout-shift module (it doesn't reach into deep markup), which is the *correct* shape. The risk is the `compose.html` `<style>` block (lines 8–100) — it locally overrides field chrome with non-tokenized values. That pattern, if copied, becomes parallel-markup-by-stylesheet.
120 +
121 + ### Hardcoded `--neobrute` default vs theme drift
122 +
123 + The CSS tokens in `styles.css :root` carry the Neobrute hex values. Themes override **only colors**. So any token *not* in the theme's mapping (e.g. accents added later without theme entries, the `--bg-hover` alias) silently falls back to Neobrute light values on every theme. This is invisible until a dark-theme user hovers a control and gets a pale-blue surface.
124 +
125 + ---
126 +
127 + ## (c) Gaps — primitives that should exist and don't
128 +
129 + 1. **`renderRow(kind, model, opts)`** — single helper for the visual shape "icon/avatar · primary · secondary · meta · actions". Adapters per kind (task, project, contact, event, email) supply the cells; the chrome is shared. Eliminates 5 parallel implementations.
130 + 2. **`renderFormField({ kind, label, value, error, help })`** — replaces the manual `.form-group > .form-label + .form-input` blocks scattered across `form-modal.js`, `settings.js`, `email-accounts.js`. Adds a real **error variant** (today errors are surfaced via toast, not next to the field).
131 + 3. **`renderSkeleton(rows)`** — a list-loading primitive. The `.skeleton-*` classes exist but no helper wraps them, so no view actually uses them.
132 + 4. **Confirm-dialog helper that produces no inline styles.** Today's `showConfirmDialog` is mostly there — strip the inline button styles and document it as canonical.
133 + 5. **Toast styling moved into CSS.** Positioning, shadow, and color belong on `.toast.toast-${type}` classes, not in JS-injected `cssText`. This is what makes toasts ignore the active theme.
134 + 6. **`.hidden` / utility class migration.** Replace `style="display:none"` in HTML with `.hidden` (already defined for the modal overlay — extend its scope). Add `.stack`, `.row`, `.gap-2`, `.gap-3` to absorb the most common inline flex tweaks.
135 + 7. **Empty-state variants documented.** Either consolidate to a single `.empty-state` (with size modifiers `.empty-state--compact`, `.empty-state--dashboard`) and delete `.empty-dashboard-list` / `.kanban-empty` / `.virtual-scroller-empty`, or keep them and document when each applies. The decision belongs in the remediation plan.
136 + 8. **Theme coverage check.** Every CSS custom property used in `styles.css` should appear in `js/themes.js`'s mapping or be explicitly marked theme-invariant. Today it's by inspection.
137 +
138 + ---
139 +
140 + ## (d) Charter
141 +
142 + A separate charter file lives at `docs/design-system.md` (this commit). It names each primitive's single canonical class or render helper. `docs/styleguide.md` remains as the visual reference; the new charter cross-references it as the authoritative inventory.
143 +
144 + ---
145 +
146 + ## Summary
147 +
148 + GoingsOn has a real design system, not a Pinterest board: ~60 tokens, ~594 selectors, a clean Neobrute identity, runtime theme switching, and a small set of UI helpers (`openModal`, `showToast`, `renderEmptyState`, `showContextMenu`). What it lacks is a **row-rendering primitive** and a **form-field primitive** — those two absences explain most of the 335 inline styles and most of the parallel-markup drift across `tasks-render.js`, `projects-render.js`, `contacts-render.js`, `events.js`, and `emails.js`. The toast helper is itself the most prominent token-bypass in the codebase: it injects raw hex via `style.cssText` and therefore ignores theme switching. Phase 1 should not begin until (1) `renderRow` and `renderFormField` exist, (2) toast styling is moved to CSS, (3) the `--neobrute` fallback drift is verified across all 9 themes, and (4) `styles.css :root` is the *only* place hex literals appear outside `themes/`.
@@ -0,0 +1,160 @@
1 + # Phase 1 — Shell & Navigation UX Audit
2 +
3 + **Scope:** the persistent chrome a user sees on every screen — top header, desktop tab navigation, sub-pill nav, mobile bottom nav, global modal / context-menu / action-sheet, sync indicator, search / settings / help controls, project-switching affordance. Feature content within a tab is out of scope (those land in Phases 2–5).
4 +
5 + **Stack:** Tauri 2 webview, vanilla HTML/CSS/JS. Files reviewed: `src-tauri/frontend/index.html` (shell markup), `js/navigation.js`, `js/router.js`, `js/mobile.js`, `js/touch.js`, `js/settings-sync.js#refreshSyncIndicator`, `js/projects.js` (switcher path), and the matching CSS in `css/styles.css`.
6 +
7 + **Method:** classical universal pass (Norman / Tognazzini / Raskin / Apple HIG) plus the flat-design cross-cutting check.
8 +
9 + ---
10 +
11 + ## Architecture snapshot
12 +
13 + - Three top-level tabs: **Work**, **Time**, **Messages**. Each is a `.tab-group` (`#work-view`, `#time-view`, `#messages-view`) hidden/shown via `.hidden`.
14 + - Each tab has a row of **pills** under it (`.pill-nav` / `.pill`). Work: Tasks · Projects. Time: Day · Week · Month · Timer · Events. Messages: Email · Contacts.
15 + - Routing is **URL-based** via History API (`pushState` / `popstate`); no hash. Default route `/tasks`. Project context lives at `/project/{id}`.
16 + - **Mobile (≤768px):** desktop `.app-header` is `display: none`. A fixed bottom bar `#mobile-tab-bar` replaces it with Work / Time / Messages buttons plus a `+` create button and a `More` popover for Settings/Search/Help.
17 + - **Globals (one each):** `#modal-overlay`, `#context-menu`, `#action-sheet`. Touch context menus are dispatched to the action sheet by feature code, not the shell.
18 + - **Active-state markers:** `.tab.active` (filled blue), `.pill.active` (filled dark), `.mobile-tab.active` (color shift to `--accent-blue`), `.page-title` text, and `document.title` ("GoingsOn — Tasks").
19 +
20 + ---
21 +
22 + ## Critical (fix before declaring Phase 1 done)
23 +
24 + ### 1. Mobile users cannot reach sub-views (pills) from the bottom nav
25 + - **Category:** Mappings, Visibility of state. (**Universal**)
26 + - **Location:** `index.html:537–552` mobile tabs; `js/navigation.js#newItemForCurrentView`; `styles.css` mobile media query at ~7300.
27 + - **Observation:** Tapping a `.mobile-tab` (Work/Time/Messages) always lands on each tab's *default* pill (Tasks / Day / Email). The pills themselves render on mobile and remain tappable, but there is no equivalent of the desktop pill bar surfaced from the bottom nav, and pills are *not* persistent chrome on mobile — they sit at the top of the content area and scroll away. A mobile user opening Time and wanting to reach Events has to scroll back up to find the pill row. A user wanting Projects from Work must already know to look for a Projects pill at the content origin.
28 + - **Why it matters:** the mobile shell exposes only ⅓ of the navigation surface. Three of the most-used destinations on a mobile screen (Projects, Events, Week review) are effectively unreachable without scrolling.
29 + - **Recommendation:**
30 + - Either keep the pill row visually pinned under the mobile title (sticky `top: 0` on the active tab's `.pill-nav`) so the sub-navigation is always one tap away; or
31 + - On mobile, long-press a `.mobile-tab` to open an action-sheet picker of that tab's pills (mirrors how iOS Mail does "Long press inbox icon"); or
32 + - Promote a small chevron next to each mobile tab that opens the pill picker as an action sheet.
33 + - Sticky-pill is the cheapest and most discoverable; recommend that as the default.
34 +
35 + ### 2. Sync indicator state is invisible without color memorization
36 + - **Category:** Feedback, Visibility of state, Error messages. (**Universal**)
37 + - **Location:** header `#sync-indicator` (`index.html:31–33`); states in `styles.css:4314–4319`; logic in `js/settings-sync.js#refreshSyncIndicator`.
38 + - **Observation:** Four states are encoded in a single colored dot — **connected** (green), **syncing** (blue, pulsing), **error** (red), **default / not set up** (muted, but in practice the *button* is hidden when not configured so users mainly see green / blue / red). The dot has `title="Cloud Sync"` and `aria-label="Cloud sync status"` but no visible label, no hover-expansion, no text. There is no surface that says "Synced X minutes ago" or "Sync failed — retry" without opening the modal.
39 + - **Why it matters:** for a personal-data app, sync status is a primary trust signal. A user who notices the dot turn red has to click through to a modal to find out what went wrong, and a user who *doesn't notice* the dot has no idea their data isn't backing up. Red and green at 10px with no text fails WCAG accessible-meaning rules (color-only encoding).
40 + - **Recommendation:**
41 + - On hover (desktop), expand the indicator into a chip showing "Synced 2 min ago" / "Syncing…" / "Sync failed". The plan's `docs/design-system.md` calls this out as a documented sprint item ("Sync indicator: expand to show 'Syncing…' / 'Sync error' on hover" — `todo.md` Sprint: Settings & Sync UX). Promote it from sprint to Phase 1.
42 + - On error, surface a one-line banner in the shell ("Sync failed — Retry") rather than only changing the dot color. This is the standard pattern in Mail, Slack, Notion.
43 + - Add a tiny text label next to the dot at desktop widths ≥ 1200px ("Synced", "Syncing", "Sync error"), so users don't have to learn a color code.
44 +
45 + ### 3. Project context cannot be switched without leaving the current work
46 + - **Category:** Modes, Modelessness (Raskin). (**Universal**)
47 + - **Location:** Project access via the "Projects" pill in Work (`index.html:46`), then card grid click → project dashboard. `js/projects.js#open` pushes `/project/{id}`.
48 + - **Observation:** the active project is *not* shell state. To go from "Tasks filtered by Project A" to "Tasks filtered by Project B" the user navigates Tasks → up to Projects pill → grid → click card B → into the project dashboard → click "Tasks" within the project — at least four taps. There is no persistent project selector in the header, sidebar, or pill row. There is no breadcrumb showing "Project A > Tasks". `GoingsOn.state.currentProjectId` exists in memory but isn't reflected in shell chrome.
49 + - **Why it matters:** for an app whose pitch includes project-grouped tasks/emails, projects are a primary axis. Hiding them inside a sub-view makes them feel like a secondary feature and pushes work into "all-projects" mode by default. This is the kind of mode-shift Raskin warns against: the user has to remember which project they were in.
50 + - **Recommendation:** add a project chip in the page header (next to `.page-title`) for views that respect a project context — Tasks, Day, Events, Email-by-project. The chip would show "All projects" or "Project A ✕" and clicking it would open a small picker (popover on desktop, action sheet on mobile). Out-of-scope-but-related: persisting `currentProjectId` to localStorage so it survives reload (today it does not unless the URL is `/project/{id}`).
51 +
52 + ---
53 +
54 + ## Major (high impact, lower urgency)
55 +
56 + ### 4. Mobile tab active-state is too quiet
57 + - **Category:** Visibility of state, Affordances. (**Flat / Universal**)
58 + - **Location:** `styles.css` mobile section, `.mobile-tab.active { color: var(--accent-blue); }`.
59 + - **Observation:** the only visual difference between an active mobile tab and an inactive one is text color (muted → accent-blue). No background, no underline, no top-bar indicator, no weight change. At a glance on a low-contrast theme (e.g. Catppuccin Latte) the active state is barely perceptible.
60 + - **Recommendation:** add a top-edge accent stripe (`border-top: 3px solid var(--accent-blue)` plus `padding-top: 0` adjustment), or fill the icon area with `--bg-secondary`. The desktop tab uses a full filled background — give the mobile tab proportional weight, not a one-property change.
61 +
62 + ### 5. The `+` button on mobile has no indication of *what* it creates
63 + - **Category:** Anticipation (Tog), Affordances. (**Universal**)
64 + - **Location:** `index.html:548`, `js/navigation.js#newItemForCurrentView`.
65 + - **Observation:** the button is a single `+`. Behavior depends on the active tab (creates a task in Work, an event in Time, an email in Messages). A first-time user has no way to know what tapping it will do without testing in each tab.
66 + - **Recommendation:** make the button context-aware visually — change its `aria-label` *and* its icon/label to match the active tab. Or replace it with two-line content: a tiny verb (`New`) plus a context word (`Task`, `Event`, `Email`). A small adornment is enough; the goal is to remove the surprise.
67 +
68 + ### 6. Three header action buttons all look the same
69 + - **Category:** Hierarchy, Consistency. (**Universal / Flat**)
70 + - **Location:** `index.html:34–36`, class `.settings-btn` reused for Search / Settings / `?`.
71 + - **Observation:** Search, Settings, and the `?` help button all share `.settings-btn` styling. They sit in a row of three pill-shaped, identically-weighted buttons. Search is the most-used (it has a keyboard shortcut shown inline), but it doesn't read as primary. The `?` button is a single character with no icon and no border distinguishing it from a typo.
72 + - **Recommendation:** make Search the visually dominant control — give it the filled `.btn-primary` look or its own icon-prefixed pill. Settings and `?` are utility, demote them to `.btn-icon` style or move them into a header-overflow menu. Single-character icon buttons (the `?`) should pair with an icon glyph or be lifted into a tooltip-bearing icon button.
73 +
74 + ### 7. Project-switching has no mobile equivalent at all
75 + - **Category:** Mappings, Modes. (**Universal**)
76 + - **Location:** mobile `+` button creates context-specific items, but project switching is not exposed in mobile chrome.
77 + - **Observation:** because Projects is a pill (not a tab) and pills aren't surfaced from the mobile bottom bar (Finding #1), a mobile user can reach Projects only by tapping Work and then scrolling to find the Projects pill. The `More` popover doesn't include Projects either.
78 + - **Recommendation:** ties into Findings #1 and #3 — the fix for either subsumes this. If sticky pills are added (#1), mobile Project access becomes one tap. If a project chip is added to the page header (#3), it works on mobile too.
79 +
80 + ### 8. Dual title system can drift
81 + - **Category:** Consistency, Visibility of state. (**Universal**)
82 + - **Location:** `.app-title` ("GoingsOn") + `.app-subtitle` ("Personal Productivity") on desktop; `#mobile-view-title` on mobile (`index.html:14–17`). Each subview also has its own `.page-title` ("Tasks", "Projects").
83 + - **Observation:** the mobile title is set imperatively by `updateMobileViewTitle()` (`navigation.js:262–266`), separately from the in-content `.page-title` set by each view. They're not bound to the same source. If a feature changes its `.page-title` mid-flow (e.g., "Tasks" → "Snoozed Tasks") but forgets to call `updateMobileViewTitle`, mobile shows stale text.
84 + - **Recommendation:** make `.page-title` the source of truth. On mobile, hide the in-content `.page-title` and pull its text into `#mobile-view-title` via a MutationObserver or by routing every set through a single helper. Phase 6 (Mobile parity sweep) is the natural home for this; flag now so it doesn't ship a drift bug in Phase 2.
85 +
86 + ### 9. Reload from a feature route may silently drop the project context
87 + - **Category:** Forgiveness, Persistence. (**Universal**)
88 + - **Location:** `js/router.js#init`, `js/projects.js#open`; `GoingsOn.state.currentProjectId` is in-memory only.
89 + - **Observation:** the URL is the source of truth for *view*, but the *project context* is in-memory. Reload at `/tasks` always shows all-project tasks; the URL doesn't carry the project filter. If a user was on "Tasks filtered to Project A" and reloads, they get all-project tasks with no warning. (Reload at `/project/A` re-enters the project dashboard correctly; the gap is the pill-deep views.)
90 + - **Recommendation:** either (a) extend the URL scheme so filtered views carry `?project=A`, or (b) persist `currentProjectId` to localStorage and re-apply on init. (a) is preferable because deep-linking is more useful than implicit state. This finding overlaps with #3 — adding a header project chip makes the persisted state visible, which makes the bug not-a-bug.
91 +
92 + ---
93 +
94 + ## Minor (worth fixing during normal cleanup)
95 +
96 + ### 10. "Personal Productivity" subtitle is dead pixels
97 + - **Category:** Hierarchy. (**Polish/Universal**)
98 + - **Location:** `index.html:16`, `.app-subtitle`.
99 + - **Observation:** a permanent tagline in the header taking up space that could be used for a project chip or search bar. Users have already opened the app — they know what it is.
100 + - **Recommendation:** remove, or demote to a tooltip on the logo. Use the freed space for a project chip (#3) or to widen Search.
101 +
102 + ### 11. `<h1>` semantic vs visual weight
103 + - **Category:** Hierarchy, Consistency. (**Polish**)
104 + - **Location:** `.app-title h1` (GoingsOn brand) vs `.page-title h2` (Tasks/Projects/etc).
105 + - **Observation:** the brand `h1` is small (logo-styled), but the in-content `h2` "Tasks" is visually dominant. Semantic and visual hierarchy don't match. Not a bug; screen readers do the right thing. But style-guides often want the *current view* to be the document's primary heading. Decide and document.
106 + - **Recommendation:** either change brand to a non-heading element (`div.app-title`) and promote `.page-title` to `<h1>`; or accept the inversion and document it in `design-system.md`. Either is fine — pick one.
107 +
108 + ### 12. `?` button can be missed
109 + - **Category:** Discoverability. (**Polish**)
110 + - **Location:** `index.html:36`.
111 + - **Observation:** the `?` is a single character with no icon and no visible label. New users may not realize it's the help affordance, especially since the `?` key already does the same thing.
112 + - **Recommendation:** pair with an icon, or change the label to `Help` and let it remain a small text button. (Three-button-pill problem #6 also applies.)
113 +
114 + ### 13. Modal overlay assumes a dark scrim, but on dark themes it renders light
115 + - **Category:** Consistency across themes, Flat-design. (**Cross-cutting** — already documented in `theme-coverage.md` finding #1.)
116 + - **Location:** `--overlay-color: color-mix(in srgb, var(--text-primary) 60%, transparent)`.
117 + - **Observation:** on Catppuccin Mocha and similar dark themes, the modal scrim becomes light because it derives from `--text-primary` which is near-white. The user sees a *light* veil over a *dark* surface, which reads as a hover state or a popover, not a modal.
118 + - **Recommendation:** introduce a dedicated `--scrim` token (themeable, default `rgba(0,0,0,0.6)`), or hardcode the overlay to `rgba(0,0,0,0.6)` since modal scrims are universally dark regardless of theme. Already queued; mention in Phase 7 sweep.
119 +
120 + ### 14. Pill `aria-selected` is not set
121 + - **Category:** Accessibility. (**Polish**)
122 + - **Location:** `index.html:44–47`, `index.html:236–242`, etc.
123 + - **Observation:** desktop tabs have `aria-selected` toggled by `navigation.js`, but pills (`<button class="pill">`) do not. Screen readers announce them as plain buttons. Since the pills form a `tablist` semantically (you select one of N sub-views), they should have `role="tab"` + `aria-selected` like the desktop tabs do, and the container `.pill-nav` should have `role="tablist"`.
124 + - **Recommendation:** small markup change in `index.html` plus a one-line update in `navigation.js` to set `aria-selected` on activation. Five-minute fix.
125 +
126 + ---
127 +
128 + ## Polish (only if you have time)
129 +
130 + ### 15. Routing is split across `router.js` and `navigation.js` with a `suppressPush` flag
131 + - **Category:** Internal consistency. (**Universal — code-only, not user-facing**)
132 + - **Observation:** `router.js` owns URL pushing/popstate; `navigation.js` owns view + pill activation. A `suppressPush` flag prevents loops. Functional but two layers and a flag is fragile — a future routing change has two places to land.
133 + - **Recommendation:** consolidate into one module. Not Phase 1 work; mark as a follow-up refactor.
134 +
135 + ### 16. Day-plan sidebar shares the "sidebar" name but isn't shell
136 + - **Category:** Consistency. (**Polish**)
137 + - **Observation:** `.day-plan-sidebar` is feature-local (`index.html:267–281`) but uses the word "sidebar" — which suggests app-wide chrome. The actual shell has no persistent sidebar.
138 + - **Recommendation:** rename to `.day-plan-aside` or `.day-plan-panel` to avoid implying chrome-ness. Touches one CSS class and a few JS references.
139 +
140 + ### 17. Mobile bottom-nav height couples with modal overlay bottom-padding
141 + - **Category:** Fragility. (**Polish**)
142 + - **Observation:** `styles.css:7355` hardcodes `bottom: calc(52px + env(safe-area-inset-bottom))` on the modal overlay to clear the 52px bottom-nav. If the nav height changes, two places must update.
143 + - **Recommendation:** extract `--mobile-tab-bar-h: 52px` as a token; consume from both rules.
144 +
145 + ---
146 +
147 + ## Cross-cutting / flat-design check
148 +
149 + The shell is on the right side of "flat with affordances": tabs have a filled-active state, pills have a filled-active state, buttons have neobrute offset shadows. Two flat-design risks did surface:
150 +
151 + - **Mobile tab active state** (Finding #4) — color-only encoding; too quiet.
152 + - **Sync dot** (Finding #2) — color-only state encoding with no text fallback.
153 +
154 + Both are color-as-sole-signal mistakes and both fail the same WCAG rule. Either alone is fixable; together they suggest a pattern — when shell affordances are minimized for cleanliness, state communication leans on color and breaks for theme + accessibility cases. Worth raising at the design-system layer: any *state* (active, syncing, error, selected) needs a non-color secondary signal (text, shape, position, weight).
155 +
156 + ---
157 +
158 + ## Summary
159 +
160 + The shell is structurally sound — three clear tabs, persistent global modals, hash-free URL routing, mobile bottom bar at a sensible breakpoint. The flaws cluster around three patterns: (1) **navigation surface compression on mobile** — pills aren't reachable from the bottom nav, project switching has no mobile presence; (2) **state encoding by color alone** — sync dot, active-tab color shifts; (3) **project context is in-memory chrome state, not URL** — switching projects requires four taps and survives reload only on `/project/{id}` routes. The single biggest user-facing fix is sticky pills on mobile (Finding #1). The single biggest trust fix is a real sync status surface (Finding #2). The single biggest workflow fix is a project chip in the page header (Findings #3, #7, #9). After those three, the shell is ready for Phase 2's deeper dive into the Tasks surface.
@@ -0,0 +1,198 @@
1 + # Phase 2 — Tasks Surface UX Audit
2 +
3 + **Scope:** the densest surface in GoingsOn — the task list (list + kanban), the task row and its actions, the task action sheet, the task detail view, the edit form, the subtasks modal, bulk actions, the filter bar, the snooze flow, time-tracking entry points, quick-add, and the recurrence editor. Shell chrome that wraps this surface was covered in Phase 1.
4 +
5 + **Stack:** Tauri 2 webview, vanilla HTML/CSS/JS. Files reviewed: `js/tasks.js`, `js/tasks-render.js`, `js/tasks-filter.js`, `js/tasks-kanban.js`, `js/task-board.js`, `js/task-forms.js`, `js/task-overview.js`, `js/bulk-actions.js`, `js/selection-manager.js`, `js/snooze.js`, `js/time-tracking.js`, `js/focus-timer.js`, `js/keyboard.js#openQuickAddModal`, `js/virtual-scroller.js`, `js/pagination-manager.js`, plus the matching CSS.
6 +
7 + **Method:** classical universal pass (Norman / Tognazzini / Raskin / HIG) plus the flat-design cross-cutting check.
8 +
9 + ---
10 +
11 + ## Surface snapshot
12 +
13 + - **Row anatomy:** 7-column grid — Description (with inline badges and started-icon), Project, Priority (letter only), Due, Recurs, Progress bar, Actions (checkbox + kebab).
14 + - **State classes** on `.task-row`: `.task-started` (green left stripe), `.task-completed` (50 % opacity + strike), `.task-overdue` (red description + due), `.task-snoozed` (badge), plus `.priority-high|medium|low` on the priority cell.
15 + - **Entry points to single-task actions:** kebab menu (`⋮`), right-click context menu (same handler), long-press on touch (same action sheet via `context-menus.js`), keyboard shortcuts (`c` complete, `s` snooze, `e` edit, `t` email-to-task), action sheet entries.
16 + - **New-task entry points:** `n` key, "+ New Task" button, `q` quick-add, mobile `+` button (Phase 1 #5), empty-state button.
17 + - **Bulk-actions bar:** appears when selection is non-empty; offers Complete · Set Project · Set Priority · Snooze · Delete · Select All.
18 + - **Detail surface:** a *separate full view* (`#task-overview`) navigated to via `taskOverview.open(id)`, not a side panel or drawer. The edit form is a modal opened separately.
19 + - **Pagination:** virtual scroller, 500-item server cap (`tasks.js:65`); no "load more" UI; no total count.
20 + - **Kanban:** toggle via List/Board pills; columns hard-coded as Pending / Started / Completed; drag-drop for status change.
21 +
22 + ---
23 +
24 + ## Critical (fix before declaring Phase 2 done)
25 +
26 + ### 1. Selection set persists across filter changes; bulk actions can hit invisible rows
27 + - **Category:** Forgiveness, Modes, Feedback. (**Universal**)
28 + - **Location:** `js/selection-manager.js` (no filter-change listener); `js/tasks-filter.js#applyFilters`.
29 + - **Observation:** when a user selects 12 rows, then changes the filter (e.g., switches status from Pending to Completed, or adds a project filter), the bulk-actions bar still reads "12 selected" but the visible rows no longer include some of those IDs. Clicking "Delete" then deletes the *invisible* rows. The selected set is not cleared on filter change; it is not even narrowed to the new visible set.
30 + - **Why it matters:** silent action on hidden rows is the canonical destructive-mistake category. There is no undo for bulk delete (Finding #2), so a user can lose 12 tasks they cannot see and cannot recover.
31 + - **Recommendation:**
32 + - On every filter / sort / view-mode change: clear the selection (preferred), or
33 + - Narrow the selection to the new visible set and update the bar to "N of M selected, N hidden by current filter" so the user can decide.
34 + - Either way, never run a bulk action against IDs that aren't currently visible without an explicit confirmation that names the hidden count.
35 +
36 + ### 2. Bulk Delete, Snooze, Set-Project, Set-Priority are not undoable; single-task Complete has undo
37 + - **Category:** Forgiveness, Consistency. (**Universal**)
38 + - **Location:** `js/tasks.js:603` shows `showUndoToast` after single-task complete; `js/bulk-actions.js:122–139` (delete) and `155–166` (project) lack undo wiring.
39 + - **Observation:** `GoingsOn.ui.showUndoToast` exists and is used after single-task completion. Bulk operations all skip it. Bulk Delete shows a confirm dialog; Bulk Snooze and Bulk Set-Project skip even that. Set-Project on 50 tasks irreversibly rewrites them with no toast preview and no undo.
40 + - **Why it matters:** the undo toast is the safety net that *enables* the speed of one-click destructive actions. Without it, users either become slow (re-confirming everything) or get burned. Inconsistent presence is worse than uniform absence — users learn "I can undo complete" and assume the same is true elsewhere.
41 + - **Recommendation:**
42 + - Wrap every bulk action in `showUndoToast` with a 10–15 s window. The Rust backend already supports the inverse operation (uncomplete, unsnooze, set-project-to-null, set-priority-to-previous) because the single-task path uses it.
43 + - Cluster the implementation: a single helper `bulkActionWithUndo(action, inverse, ids, prevState)` that captures pre-state and offers undo. Bulk Delete needs soft-delete semantics; verify backend.
44 + - This also resolves the deeper risk in Finding #1 — even a mis-targeted bulk action becomes recoverable.
45 +
46 + ### 3. Kanban view silently discards the status filter
47 + - **Category:** Visibility of state, Mappings. (**Universal**)
48 + - **Location:** `js/tasks-kanban.js#renderBoard`; status filter hidden on board mode but its value is not displayed elsewhere.
49 + - **Observation:** when the user switches from List to Board, the status filter dropdown is hidden and the board fetches *all statuses* (Pending + Started + Completed) regardless of what the filter was set to. There is no UI indication that the filter was overridden. Switching back to List re-applies the filter. A user who filtered to "Started" and then switched to board sees Completed cards mixed in and may not realize why.
50 + - **Why it matters:** silent state changes break the user's mental model. The first time this happens to a user with a screen full of completed cards in their "Started" view, they have to debug what changed.
51 + - **Recommendation:**
52 + - On switch to Board, show a small banner above the columns: "Showing all statuses on the board view — switch back to List to filter by status."
53 + - Or honor the status filter on the board too, hiding columns whose status isn't selected.
54 + - Prefer the latter: it preserves the user's filter intent across view modes.
55 +
56 + ---
57 +
58 + ## Major (high impact, lower urgency)
59 +
60 + ### 4. Task detail view is a separate route, not a side panel
61 + - **Category:** Modes, Modelessness (Raskin), Locus of attention. (**Universal**)
62 + - **Location:** `js/task-overview.js` switches the main view; `js/tasks.js` keeps the list as the main view.
63 + - **Observation:** clicking a row's description column navigates *away* from the task list to `#task-overview`. The list view tears down; coming back requires re-rendering, scroll position is lost (virtual scroller resets to top unless explicitly preserved). Most productivity apps with dense lists pair them with a right-side detail pane (Things, Linear, Mail) so users can scan rows + read details in one place.
64 + - **Why it matters:** users frequently triage — read row, glance at notes / time history / subtasks, decide action, move on. A full route swap forces a context-switch per task, which discourages triage and pushes everything into the edit form.
65 + - **Recommendation:**
66 + - Desktop: render task details in a right-side drawer (or below the list on narrow desktop) instead of a separate view. Click row → drawer opens; click another row → drawer updates in place; Esc closes; left/right keys move through rows.
67 + - Mobile: keep the full-view swap; it's the right shape there.
68 + - Out of scope for this audit: the `#task-overview` view is also where recurring-task streaks and the heatmap live; they are valuable and need a home in either the drawer or the edit modal.
69 +
70 + ### 5. Three time-tracking flows look like three features but share one backend
71 + - **Category:** Anticipation, Mappings, Consistency. (**Universal**)
72 + - **Location:** `js/time-tracking.js` (Track Time), `js/focus-timer.js` (Focus Mode), `js/day-planning.js` (Schedule Time Block). Action sheet exposes all three as peers.
73 + - **Observation:** the action sheet lists "Schedule Time Block · Track Time · Focus Mode (25/5)" as three sibling items. The first plans a block on the day; the second runs a live timer; the third runs a Pomodoro-style countdown. All three start the same backend `start_timer` call. Users cannot tell from the menu that Track Time and Focus Mode are variants of the same underlying timer, or that they can't run simultaneously (starting Focus while a Track Time timer runs *replaces* it without warning).
74 + - **Why it matters:** the todo file itself flagged this (`Sprint: Timer & Focus → Distinguish time tracking vs focus timer in UI (two features, no differentiation)`). Cognitive overhead per task: which of these three does the user want, and what happens to the other one if it's running?
75 + - **Recommendation:**
76 + - Action-sheet grouping: a "Time" subsection with one entry "Start timer" that opens a small dialog: "Live timer | 25 min focus | Custom Pomodoro". Schedule Time Block stays as a separate "Plan" entry because it doesn't run the timer.
77 + - When a timer is running, the menu entry says "Stop timer (12:34)" instead of offering to start another.
78 + - Surface the *single* running-timer state in the shell — the existing floating timer widget is good; make it more prominent (todo file already calls this out).
79 +
80 + ### 6. Priority is encoded as a one-letter abbreviation that depends on color to disambiguate
81 + - **Category:** Visibility, Accessibility (color as sole signal). (**Universal**)
82 + - **Location:** `js/tasks-render.js` renders the priority cell as `task.priority.charAt(0)` — `H`, `M`, `L`; styled by `.priority-high|medium|low`.
83 + - **Observation:** new users see "H" / "M" / "L" with no legend. The CSS supplies a color, but on certain themes (Catppuccin Latte, Flatwhite) the color contrast between High and Medium is subtle, and the letter is identical for High and Hard / Hold / Held etc. — there's no built-in semantic.
84 + - **Why it matters:** priority is one of the most-scanned attributes in a task list. Forcing the user to learn an abbreviation is a small but constant tax. Color-only encoding fails WCAG accessible-meaning.
85 + - **Recommendation:**
86 + - Render priority with a glyph + word affordance: a colored chevron-up / dash / chevron-down with the word "High" / "Medium" / "Low" at desktop widths; the chevron alone on narrow widths.
87 + - Or invert: use the full word as the cell content (`High`, `Med`, `Low`) and drop the color-only encoding to a tint of the cell background.
88 + - At minimum: add `title="Priority: High"` to the cell so hover reveals the meaning.
89 +
90 + ### 7. "Waiting Only" filter has no companion UI to set the waiting state
91 + - **Category:** Consistency, Anticipation. (**Universal**)
92 + - **Location:** `js/tasks-filter.js` — `filter-waiting` checkbox; `js/tasks.js` edit form has no "waiting" field; the waiting status is not in the form's select options.
93 + - **Observation:** the filter bar exposes a "Waiting Only" toggle, implying a binary "waiting" state on tasks. But there is no UI to *mark* a task as waiting — the edit form's status select is Pending / Started / Completed only. The waiting state must be set via the API or another path not visible in the frontend.
94 + - **Why it matters:** orphan filters are a stronger signal than they look — they tell users "this state exists, you just can't reach it from here." Either complete the loop (add a way to mark waiting) or remove the filter.
95 + - **Recommendation:**
96 + - If the waiting state is meaningful (e.g., "blocked by external party"), add a fourth status (`Waiting`) to the edit form's select and to the kanban columns.
97 + - If it's vestigial or rolled into Pending, remove the filter.
98 + - Either decision unblocks: the current half-state is the worst case.
99 +
100 + ### 8. Selection clears with no animation; bulk-actions bar pops in/out without transition
101 + - **Category:** Feedback, Locus of attention. (**Universal / Polish overlap**)
102 + - **Location:** `css/styles.css` `.bulk-actions-bar.hidden { display: none }`.
103 + - **Observation:** the bar is toggled via `.hidden` (display none/flex). Hard show/hide. When a user selects a row, the entire bottom of the screen rearranges with no transition. When the last selected row is deselected (or the filter clears the selection), the bar vanishes without warning.
104 + - **Why it matters:** sudden layout shifts in dense lists make users lose their place. Standard pattern is a slide-up / slide-down with a 150–200 ms ease.
105 + - **Recommendation:** add a transition on opacity + transform (`translateY`) tied to a `.is-visible` modifier. Cheap fix, big perceived-quality win.
106 +
107 + ### 9. Pagination is invisible; no total count, no end-of-list affordance
108 + - **Category:** Visibility of state, Feedback. (**Universal**)
109 + - **Location:** virtual scroller; `tasks.js` server cap of 500 (`response = await GoingsOn.api.tasks.listFiltered(...)` with no offset/limit indicator).
110 + - **Observation:** the list shows visible rows only; the user never sees "247 tasks" anywhere. If the user has more than 500 tasks, the server silently caps and the user gets a list that *ends* with no indication that more exist. (The todo file flags this: `Paginate beyond 500-item caps — tasks and emails silently cap`.)
111 + - **Why it matters:** users with large task lists make decisions based on volume ("I have 30 things this week"); a hidden total degrades planning. The silent cap is worse — a user with 600 tasks loses access to 100 of them with no error.
112 + - **Recommendation:**
113 + - Show a "247 tasks" count in the filter bar at all times.
114 + - When the cap is reached, show a banner at the end of the list: "Showing 500 of 600. Filter to narrow the list."
115 + - Long term: real pagination or fully-virtual server-side scrolling. The todo file has this queued.
116 +
117 + ### 10. Filter state lives only in the DOM; reload resets all filters
118 + - **Category:** Persistence, Anticipation. (**Universal**)
119 + - **Location:** `js/tasks-filter.js#getFilters` reads `document.getElementById('filter-status').value` directly; no localStorage write; no URL sync.
120 + - **Observation:** a user who filters to "Project A, Priority High, Waiting" and reloads the app gets the default Pending-all-projects view back. The URL doesn't carry the filter; localStorage isn't used.
121 + - **Why it matters:** a user who has built up a daily-triage filter has to rebuild it every cold start. This is also the root cause of the "reload loses project context" issue raised in Phase 1 #9.
122 + - **Recommendation:**
123 + - Mirror the active filter state into `location.search` (`?status=pending&project=ABC&priority=high&waiting=1`) so deep-linking and reload preserve it.
124 + - As a quicker stopgap: write to localStorage under `goingson.taskFilters` and rehydrate on init.
125 + - Either way, the design-system charter should add a "filter state persistence" rule for all Phase-N audits.
126 +
127 + ---
128 +
129 + ## Minor (worth fixing during normal cleanup)
130 +
131 + ### 11. Kebab button and right-click context menu are the same target
132 + - **Category:** Consistency, Locus of attention. (**Polish/Universal**)
133 + - **Location:** `js/tasks-render.js` — both `oncontextmenu` on the row and `onclick` on the kebab call `GoingsOn.contextMenus.showTask(event, id)`.
134 + - **Observation:** the kebab and the right-click both produce the same menu. Discoverable for the user who tries either, but the kebab is redundant on desktop where right-click is universal, and the kebab is the only path on touch (since right-click doesn't exist). Action sheet vs context menu split is handled at dispatch.
135 + - **Recommendation:** keep both. But document the precedence in the design-system charter: "kebab on touch; right-click on desktop; both are accelerators for the same context-menu API." Worth flagging because future contributors may try to add a third entry point.
136 +
137 + ### 12. Quick-add syntax hints fade as you type, removing the reference exactly when needed
138 + - **Category:** Anticipation (Tog), Discoverability. (**Universal/Polish**)
139 + - **Location:** `js/keyboard.js:290–309` — token-by-token highlighting + fade of inactive tokens.
140 + - **Observation:** the syntax hint row (`@` project / `#` tag / `+` priority / `due:` …) starts evenly weighted, but as the user types a `@`, the other tokens drop to 0.4 opacity. This is the wrong direction — a user who just learned to type `@` is *more* likely to want to know what else is available, not less. The remaining hints get dimmer the more the user types.
141 + - **Recommendation:** invert the highlight — bold the active token, but keep the others at full opacity (or 0.85). The current pattern was probably borrowed from "current step is highlighted" wizards, where the past steps are done; here every token is independent and any can appear at any cursor position.
142 +
143 + ### 13. Snooze badge is text-only and indistinguishable from other inline badges
144 + - **Category:** Visibility, Hierarchy. (**Polish**)
145 + - **Location:** `js/tasks-render.js` — snooze badge inline in description cell.
146 + - **Observation:** the snooze badge renders as `<span class="snooze-badge">Snoozed</span>` alongside the subtask count (`2/4`), contact name (`Alice`), and time badge (`15m`). All inline in the same description cell with similar styling. Snooze is a row *state* (hidden until X) but reads like just another piece of metadata.
147 + - **Recommendation:** give snooze its own visual treatment — left-edge stripe like `.task-started` but in `--accent-yellow`, or fade the entire row to ~70 % opacity (it's effectively hidden until X, treat it like a soft archive). Snooze is information-dense enough to deserve its own state class, not a badge among other badges.
148 +
149 + ### 14. Selection range hint shown once and lost
150 + - **Category:** Discoverability. (**Polish**)
151 + - **Location:** `js/selection-manager.js` — one-time hint after first selection.
152 + - **Observation:** "Shift-click to select a range of items" is shown the first time a user selects a row. Stored in localStorage; never shown again. A user who misses it has no other affordance for the feature.
153 + - **Recommendation:** add a tiny "?" pip on the bulk-actions bar that pops a small popover with "Tips: Shift-click for range, Cmd/Ctrl-click for multi, Select All button." Persistent affordance for an accelerator.
154 +
155 + ### 15. Bulk Set-Project picker doesn't scroll/search for long project lists
156 + - **Category:** Anticipation, Fitts. (**Universal/Polish**)
157 + - **Location:** `js/bulk-actions.js#setProjectTasks` — inline buttons, max-height 300 px via `.bulk-modal-scroll`.
158 + - **Observation:** the modal lists projects as a vertical column of buttons inside a 300 px-tall scroll container. With 30+ projects, finding the right one means scrolling without search. The single-task edit form uses a `<select>` for the same field.
159 + - **Recommendation:** match the single-task form — use a `<select>` (or a typeahead) in the bulk modal too. Cheaper to use than a button column when N > ~8.
160 +
161 + ---
162 +
163 + ## Polish (only if you have time)
164 +
165 + ### 16. Empty state for new users doesn't surface the `q` quick-add shortcut
166 + - **Category:** Discoverability.
167 + - **Location:** `js/tasks.js:127–130` — new-user empty state has a "New Task" button only.
168 + - **Observation:** the empty state has the "New Task" button but doesn't mention `q` (quick-add) or the Cmd-K search, even though both are faster than the modal. The welcome modal *does* mention `q` (Phase 1 found this), but a user who skipped welcome lands here.
169 + - **Recommendation:** add a one-liner under the button: "or press <kbd>q</kbd> for quick add" (touch devices hide the kbd hint).
170 +
171 + ### 17. Recurrence preview is text-only; no visual calendar
172 + - **Category:** Anticipation.
173 + - **Location:** `js/task-forms.js` — preview line "Repeats every 2 weeks on Mon, Wed, Fri".
174 + - **Observation:** the user types in the rule, the preview spells it out as English. For complex rules (every 2nd Thursday of the month, last weekday of the month), a small 5-week mini-calendar showing the next 5 generated occurrences would catch off-by-one mistakes the text preview hides.
175 + - **Recommendation:** below the text preview, render a strip of "Next: Mar 12, Mar 26, Apr 9…" — pure data, no calendar grid needed. Three lines of JS.
176 +
177 + ### 18. Two ways to open the subtasks modal; both close it on "Track Time"
178 + - **Category:** Consistency, Locus of attention.
179 + - **Location:** `js/tasks-render.js#renderSubtasksModal` — Track Time button has `onclick="GoingsOn.ui.closeModal(); GoingsOn.timeTracking.startTimer(...)"`.
180 + - **Observation:** opening the subtasks modal from either kanban or action sheet leads to the same UI. Clicking Track Time (or Focus, or Schedule) closes the subtasks modal. If the user wanted to keep referencing subtasks while timing, they can't — modal closes to make room for the timer overlay.
181 + - **Recommendation:** the timer widget is bottom-fixed; the subtasks modal could stay open. Only close the modal if the user explicitly cancels. Small win that becomes a big quality-of-life upgrade once users actually use both features in the same session.
182 +
183 + ---
184 +
185 + ## Cross-cutting / flat-design check
186 +
187 + The tasks surface has more state encoding than the shell, and most of it works: filled-left-border for Started, opacity-strike for Completed, red text for Overdue. Two flat-design risks recur:
188 +
189 + - **Priority cell** is colored letter only — same color-as-sole-signal problem as the Phase 1 sync dot.
190 + - **Snooze badge** is monochrome text inline with other monochrome text — no shape/position to distinguish it.
191 +
192 + Both fit the pattern noted in Phase 1: when state is encoded by color alone, the design system pays an accessibility tax and a theme-fragility tax. The fix is to pair every color with a non-color signal (icon, shape, position, weight).
193 +
194 + ---
195 +
196 + ## Summary
197 +
198 + The tasks surface is functionally complete and structurally well-organized: row → action sheet → form is a clean path, virtual scrolling holds up for medium lists, and the keyboard shortcuts are dense and usable. The failure modes cluster around **data safety** (selection-bleed across filters, no undo on bulk operations, silent cap at 500 tasks), **filter persistence** (everything resets on reload — same root cause as Phase 1 #9), and **state communication** (priority and snooze read as decoration, kanban silently drops the status filter). The single highest-priority fix is bulk-action undo + selection-clear-on-filter (Findings #1 + #2 together) — they remove the only class of *unrecoverable* user mistake in this surface. The single biggest quality-of-life improvement is a side-drawer task detail (#4) — it makes triage modeless. The single most overdue cleanup is the orphan "Waiting Only" filter (#7) — either complete the loop or delete it.
@@ -0,0 +1,216 @@
1 + # Phase 3 — Compose & Email UX Audit
2 +
3 + **Scope:** the email surface — list, reader / thread, compose window (desktop) and compose modal (mobile), drafts, autocomplete, attachments, labels, folder filter, search-within-emails, snooze, the reader action bar, send / error / retry behavior. Email-account management was covered in Phase 0 — out of scope here.
4 +
5 + **Stack:** Tauri 2 webview, vanilla HTML/CSS/JS. Files reviewed: `compose.html`, `js/emails.js`, `js/autocomplete.js`, `js/address-highlight.js`, the `#messages-view` block in `index.html`, plus the matching CSS in `css/styles.css`.
6 +
7 + **Method:** classical universal pass (Norman / Tognazzini / Raskin / HIG) plus the flat-design cross-cutting check.
8 +
9 + ---
10 +
11 + ## Surface snapshot
12 +
13 + - **List row:** `.email-item` with from / subject / 100-char preview / date / thread-count badge / labels / snooze badge. Unread state = 4 px blue left border + bold + light-blue tint. **No archived indicator** in list; only in reader.
14 + - **Reader:** large modal. Header (subject + thread count, meta line, contact card), pooled attachments block, thread messages (oldest first; latest gets a 3 px blue stripe), action bar with up to 10 controls.
15 + - **Compose surface:** *two* implementations. Desktop opens a **separate Tauri window** (`compose.html`) with its own inline `<style>` block, autocomplete, address highlighting, Cmd+S / Cmd+Enter shortcuts. Mobile opens an **in-app modal** built with the form-builder; same fields, fewer affordances.
16 + - **Send flow:** validation → status bar "Sending…" → success "Email sent!" + auto-close after 1 s → status bar red error if fails. No undo-send. No retry button.
17 + - **Drafts:** autosaved every 2 s after typing, accessed via a "Drafts" header button → modal list → click to resume in compose window (desktop) or modal (mobile).
18 + - **Filters:** folder dropdown, label dropdown, in-pane search input (debounced 250 ms, FTS5).
19 + - **Cross-cuts:** snooze delegated to the shared snooze module (same modal as tasks); label entry from the Actions dropdown (`Edit Labels`).
20 +
21 + ---
22 +
23 + ## Critical (fix before declaring Phase 3 done)
24 +
25 + ### 1. Send is final — no undo-send, no soft delay
26 + - **Category:** Forgiveness. (**Universal**)
27 + - **Location:** `compose.html#sendEmail` lines ~430–467; `js/emails.js` mobile modal send path.
28 + - **Observation:** clicking Send goes straight to `GoingsOn.api.emails.send(...)`. The status bar shows "Sending…", then "Email sent!" + the window auto-closes after 1 second. The user has no way to cancel between clicking Send and the SMTP handoff. After the window closes, the message is on its way irrevocably.
29 + - **Why it matters:** undo-send is a 2010s expectation — Gmail, Apple Mail, Outlook, Fastmail all offer a 5–30 s delay window. Without it, every "oh wait I meant to add Bob" is a recall email. For an app whose pitch includes "manage your inbox calmly," not having undo-send is a missing primary feature.
30 + - **Recommendation:**
31 + - Add a `send-with-delay` queue: on Send, write the message to a local pending-send queue and start a 5 s (configurable: 5/10/20 s) countdown. During the countdown, surface a persistent toast with "Sending in 4… Undo". On expiry, hand off to SMTP.
32 + - The compose window closes immediately; the toast lives in the main app shell.
33 + - On Undo, requeue the draft (re-open the compose window pre-filled).
34 + - Backend has the primitives — drafts already exist; this is mostly a frontend queue + a delay.
35 +
36 + ### 2. No client-side attachment-size warning; send fails on the server with a generic error
37 + - **Category:** Anticipation (Tog), Error messages. (**Universal**)
38 + - **Location:** `compose.html#pickAttachment`, `compose.html#sendEmail`.
39 + - **Observation:** the file picker accepts any file. There is no client-side size check, no aggregate-size readout, no per-file warning. The user can attach 80 MB; on Send, the server enforces its limits and returns an error. The status bar shows "Failed to send: [server error string]" — which may or may not include the size hint. The user has uploaded that 80 MB through the local IPC for nothing.
40 + - **Why it matters:** SMTP servers typically cap at 25 MB (some 35 MB or 10 MB depending on provider). A user attaches a 60 MB video, types a long message, hits Send, gets a cryptic error, and now has to remove the attachment without losing the message. The error happens *after* the user committed mentally to sending.
41 + - **Recommendation:**
42 + - Show running attachment total below the attachments bar: "Total: 4.2 MB / 25 MB". Update as files are added/removed.
43 + - At a configurable warning threshold (default 20 MB), pre-warn in the status bar: "Large attachment — your mail server may reject this."
44 + - At a hard threshold (default 25 MB), disable Send and surface a specific error: "Attachments exceed 25 MB — most mail servers will reject this. Try a file share link instead."
45 + - The cap per account should come from the account settings (account already has IMAP/SMTP configured; reasonable defaults work).
46 +
47 + ### 3. Two parallel compose implementations with feature drift
48 + - **Category:** Consistency, Modes. (**Universal**)
49 + - **Location:** `compose.html` (desktop, separate Tauri window) vs. `js/emails.js` lines ~150–280 (mobile, in-app modal built by `GoingsOn.ui.openFormModal`).
50 + - **Observation:** the two compose surfaces share the same fields but diverge:
51 + - Desktop has **address highlighting** (`address-highlight.js`) — red for malformed, blue for known contact. Mobile does not.
52 + - Desktop has **Cmd+S** (save draft) and **Cmd+Enter** (send). Mobile has no keyboard shortcuts.
53 + - Desktop has an **attachments bar with remove buttons** styled `.compose-attachment-item`. Mobile renders attachments inline in the form as `.email-attachment-tag` chips.
54 + - Desktop has the **`showSaveContactPrompt`** post-send bar. Mobile shows a toast.
55 + - Desktop autosaves drafts via a 2 s debounce; mobile autosaves via the form-modal's own dirty tracking (verify; the form-builder doesn't have autosave by default).
56 + - Desktop autocomplete is its own local copy; main-app autocomplete is in `js/autocomplete.js`.
57 + - **Why it matters:** every fix to compose has to be done twice. Address highlighting and keyboard shortcuts being desktop-only is the kind of disparity that survives because mobile users don't complain — they assume the app just doesn't have it.
58 + - **Recommendation:**
59 + - Unify on one component. Two options:
60 + - **A.** Have mobile load `compose.html` in a webview/modal too (Tauri 2 supports multiple webviews per window). One codebase, one feature set, one autocomplete.
61 + - **B.** Extract compose into a shared module (`js/compose-form.js`) used by both `compose.html` and the mobile modal — markup-builder helpers + behavior modules that can be mounted in either context.
62 + - **B is the cheaper path**: extract address highlighting, autocomplete wire-up, autosave, keyboard shortcuts, and attachment management into module-level functions; both shells import.
63 + - Either way, the design-system charter should add a rule: "Compose is one component. Surface-specific behavior (window chrome, etc.) wraps it, but the form lives in one place."
64 +
65 + ---
66 +
67 + ## Major (high impact, lower urgency)
68 +
69 + ### 4. Reader action bar carries 10 controls with no hierarchy
70 + - **Category:** Hierarchy, Fitts. (**Universal / Flat**)
71 + - **Location:** `js/emails.js` lines ~431–460, `.email-actions-bar` styling.
72 + - **Observation:** the bottom action bar of the reader contains, in order: Reply, Reply All, Forward, Delete (red), Archive/Unarchive, Snooze/Unsnooze, Create Task, an Actions ▾ dropdown (with Convert to Task, Convert to Event, Edit Labels, Move to Folder), and Open in Browser. Ten visible controls, no primary/secondary visual split — only the Delete button is colored (red). Reply is `.btn-primary` but the visual difference between primary and secondary is small enough that Reply doesn't dominate.
73 + - **Why it matters:** the most common action (Reply, Archive) gets the same weight as the least common (Open in Browser). New users have to read every button; experienced users have to scan past the noise.
74 + - **Recommendation:**
75 + - **Primary cluster (left):** Reply · Reply All · Forward — these are 90 % of opens, group them together visually.
76 + - **State actions (middle):** Archive · Snooze · Mark Unread — these change the message's state.
77 + - **Overflow (right):** an Actions ▾ menu absorbing Delete, Create Task, Convert to Event, Edit Labels, Move to Folder, Open in Browser. Delete is destructive — it doesn't need to be top-level if the menu is one click away.
78 + - Promote Reply visually with a filled accent treatment; demote the rest to ghost buttons.
79 +
80 + ### 5. "Create Task" appears twice with subtly different paths
81 + - **Category:** Consistency, Modes. (**Universal**)
82 + - **Location:** `js/emails.js` lines ~439 and ~451–453.
83 + - **Observation:** the action bar has a top-level "Create Task" button that calls `createTaskFromEmail(id)`; the Actions ▾ dropdown contains a "Convert to Task" item that calls the same function. They produce identical results. Same for "Convert to Event" which has no top-level equivalent — making the dropdown inconsistent (Task is both top-level and in the menu; Event is only in the menu).
84 + - **Why it matters:** twin entries split the user's discovery — half the users use the top-level button, the other half open the dropdown looking for it. Future maintainers add a new place to invoke it and the count grows.
85 + - **Recommendation:** keep "Create Task" top-level (it's a primary cross-feature pivot in GoingsOn). Remove the duplicate from the dropdown. Add "Create Event" top-level next to it so the parallel actions live side-by-side.
86 +
87 + ### 6. Status bar uses color as the only success/error signal
88 + - **Category:** Visibility, Accessibility. (**Universal / Flat — recurring pattern**)
89 + - **Location:** `compose.html` `.status-bar.error` / `.status-bar.success` rules.
90 + - **Observation:** the compose-window status bar at the bottom flips between grey (default), green (success), red (error). The text changes too ("Sending…" / "Email sent!" / "Failed to send: …"), so this isn't *purely* color-coding — but the visual scan signal is the color. On dark themes with low-contrast greens/reds, or for colorblind users, the state difference between "Sending…" (grey) and "Email sent!" (green) is hard to distinguish at a glance.
91 + - **Recommendation:** add a leading status glyph — `· Ready`, `⟳ Sending…`, `✓ Email sent!`, `✕ Failed: …` — paired with the color. Same pattern as Phase 1 #2 (sync indicator) and Phase 2 #6 (priority): never let color be the sole state signal.
92 +
93 + ### 7. Address validation only in the desktop compose window
94 + - **Category:** Anticipation, Forgiveness. (**Universal**)
95 + - **Location:** `js/address-highlight.js` — only wired in `compose.html`; mobile compose modal in `emails.js` doesn't import it.
96 + - **Observation:** mobile compose lets users type `bob@@example` or `friend at gmail.com` with no warning. Server returns "invalid address" on send. The desktop window catches this before send with a red highlight.
97 + - **Why it matters:** mobile users are *more* likely to typo addresses (small keyboard, autocorrect). The lack of pre-send validation there is upside-down.
98 + - **Recommendation:** part of Finding #3 (unify compose). Until then, port `address-highlight.js` to the mobile form-modal — the autocomplete wrapper already supports per-input mounting; this is a 30-line patch.
99 +
100 + ### 8. Filter state (folder, label, search) doesn't persist across reload
101 + - **Category:** Persistence, Anticipation. (**Universal**)
102 + - **Location:** `js/emails.js` — `activeFolder` / `activeLabel` are module-level vars; no localStorage write; no URL sync.
103 + - **Observation:** identical pattern to Phase 1 #9 and Phase 2 #10. A user filtered to "Work label, INBOX folder, searching 'invoice'" loses all of that on reload.
104 + - **Recommendation:** mirror the active state into `location.search` (`?folder=INBOX&label=work&q=invoice`) on every filter change. One implementation can serve tasks and emails — pull into a shared `query-state.js` helper. This finding plus Phase 1 #9 and Phase 2 #10 are the same bug in three places; a single fix should clear them all.
105 +
106 + ### 9. Snooze badge in list uses the same yellow as label badges and isn't distinguishable
107 + - **Category:** Visibility, Hierarchy. (**Flat / Universal**)
108 + - **Location:** `js/emails.js` line ~135, `.snooze-badge` styling.
109 + - **Observation:** the snooze badge appears inline in the email header row alongside label badges (`.email-label-badge`). Both are small chips. The snooze badge is yellow-tinted; some labels are also yellow if the user names a label that way (or if the label-badge color cycles). At a glance, "Snoozed" reads like a custom label.
110 + - **Recommendation:**
111 + - Move the snooze indicator out of the badge area entirely: dim the entire `.email-item.email-snoozed` row to ~70 % opacity and add a small "💤" or clock icon at the right edge, paired with the snoozed-until time. The visual treatment should match `.task-snoozed` once Phase 2 #13 lands — they're the same conceptual state.
112 + - Reserve the badge area for labels.
113 +
114 + ### 10. Send failure leaves the user with no recovery beyond "click Send again"
115 + - **Category:** Error messages, Forgiveness. (**Universal**)
116 + - **Location:** `compose.html#sendEmail` error branch (lines ~469–471).
117 + - **Observation:** on send failure, the status bar shows "Failed to send: [server error]" in red and the compose window remains open. There is no retry button; no inspect-the-error affordance; no "Save Draft and try later" prompt. If the user closes the window after a failure, autosave (2 s debounce) *may* have saved a draft, but timing is fragile — a fast typer can fail-send, panic, close, and lose data.
118 + - **Recommendation:**
119 + - On send failure: surface a one-line error + two buttons inline above the status bar — `Retry` and `Save Draft & Close`.
120 + - On window close while there is unsent content, always save a draft (no debounce dependence). Show a confirmation: "Saved as draft — restore from the Drafts list."
121 + - If the error is a known transient (network / TLS handshake / 421 / 4xx temporary), retry automatically once after 2 s before surfacing.
122 +
123 + ### 11. Compose opens even when no email accounts are configured; the wall is at Send, not entry
124 + - **Category:** Anticipation, Visibility of state. (**Universal**)
125 + - **Location:** `compose.html#init` lines ~330–340, `.from-account` select default "Loading accounts…" → "No email accounts configured. Add one in Settings."
126 + - **Observation:** clicking Compose with zero accounts opens the full compose window. The From dropdown ends up empty; the status bar says "No email accounts configured." Send is disabled — the user has typed a message into a UI that was never going to send it.
127 + - **Why it matters:** the most useful state-visibility move is to *prevent* the dead-end flow rather than recover from it. Showing a blocking error after the user composed a message is wasted work.
128 + - **Recommendation:**
129 + - Disable the Compose buttons (header, list empty state, reply-from-reader) when no accounts exist; instead, surface a single CTA: "Add an email account to start composing".
130 + - If the user gets to the compose window via a reply path (which can't happen without an account already — the email they're replying to had to be received), nothing changes.
131 +
132 + ---
133 +
134 + ## Minor (worth fixing during normal cleanup)
135 +
136 + ### 12. CC/BCC toggle button doesn't update its label after expanding
137 + - **Category:** Feedback, Consistency. (**Polish/Universal**)
138 + - **Location:** `compose.html` `#toggle-cc` — `onclick="toggleCcBcc()"`.
139 + - **Observation:** the button reads "Show CC/BCC" before clicking; after clicking, CC and BCC rows appear but the button text still says "Show CC/BCC" rather than "Hide CC/BCC". (Verify via the toggleCcBcc implementation.)
140 + - **Recommendation:** flip the label on toggle. Standard pattern.
141 +
142 + ### 13. Email link extraction renders `<span class="email-link">` instead of `<a href>`
143 + - **Category:** Affordances. (**Universal/Polish**)
144 + - **Location:** `js/emails.js#formatEmailBody` (or `utils.js`), `.email-link` CSS.
145 + - **Observation:** links in email bodies are detected and styled to look like links (blue, underline), but rendered as `<span>` not `<a>`. They're not clickable in the webview — at least not via the browser's default link behavior. The codebase may attach a click handler elsewhere; verify. If not, users see links but can't open them.
146 + - **Recommendation:** render as `<a href target="_blank" rel="noopener">` and let Tauri's `tauri-plugin-shell` (or the existing `allowedExternal` config) handle external opens. If the current `<span>` is a security measure to prevent HTML-injection, the security goal is met by `escapeHtml` *before* link extraction — the link can be a real anchor.
147 +
148 + ### 14. Archive state isn't shown in the list, only in the reader
149 + - **Category:** Visibility of state. (**Polish**)
150 + - **Location:** `js/emails.js` row render — no archived-state indicator.
151 + - **Observation:** the list filters out archived emails by default. If a user opens "All folders" or searches and an archived email appears, there is no visual cue — the row looks identical to an inbox email. The reader meta-line shows "· *Archived*" but only after open.
152 + - **Recommendation:** add a small archive icon or `.email-item.archived` styling (low-saturation tint, or a tiny boxed "📦" / "Archived" at the right edge of the row).
153 +
154 + ### 15. "Open in Browser" is ambiguous
155 + - **Category:** Mappings, Anticipation. (**Polish**)
156 + - **Location:** Reader action bar, last button.
157 + - **Observation:** the button label is "Open in Browser". A user might reasonably guess: (a) open the email's HTML body in the system browser, (b) open the original Gmail/Fastmail/whatever web URL, (c) view the email source. The implementation likely does (a). The label doesn't pin it down.
158 + - **Recommendation:** rename to "Open HTML in Browser" or "View HTML version". If the feature is rarely used, demote to the Actions ▾ menu (folds into Finding #4).
159 +
160 + ### 16. Reply quoted body uses bare `>` prefix without line-wrapping
161 + - **Category:** Consistency, Polish. (**Polish**)
162 + - **Location:** `js/emails.js#reply` body construction.
163 + - **Observation:** the quoted body is built as `> ` prefix per line of the original. Long lines stay long; sequences of `>` for nested replies accumulate awkwardly (`>>>>>>` in long threads). Standard mail-client behavior is to re-flow at 72/76 columns.
164 + - **Recommendation:** wrap quoted lines at 72 columns, preserving paragraph breaks. Trim trailing whitespace.
165 +
166 + ### 17. Drafts modal lists drafts but doesn't show *which account* drafted them
167 + - **Category:** Visibility. (**Polish**)
168 + - **Location:** `js/emails.js#openDraftsModal` line ~910 — meta shows "To: ... · date".
169 + - **Observation:** a user with multiple accounts sees drafts but doesn't see which account each draft is From. Opening the draft reveals it.
170 + - **Recommendation:** add "From: account-name" to the draft row meta. Two lines instead of one; small change.
171 +
172 + ### 18. Selection in email list doesn't have the same shift-click range/select-all features as tasks
173 + - **Category:** Consistency. (**Polish**)
174 + - **Location:** `js/emails.js` bulk-actions wiring; `js/selection-manager.js` may be reused.
175 + - **Observation:** verify whether the email `SelectionManager` instance supports shift-click ranges and the Select-All button. The infrastructure exists for tasks. If emails skip it, that's a parity gap.
176 + - **Recommendation:** reuse the `SelectionManager` from Phase 2 wholesale; same UI hint, same bulk-actions bar shape.
177 +
178 + ---
179 +
180 + ## Polish (only if you have time)
181 +
182 + ### 19. Plain-text body with no editor affordances; no indication that rich text is unsupported
183 + - **Category:** Anticipation.
184 + - **Location:** `compose.html` body `<textarea>`; signature appending in plain text.
185 + - **Observation:** the body is a `<textarea>`. There is no formatting toolbar, no "B / I / U" buttons, no Markdown hint, no shortcut for inserting links. This is fine as a design choice ("plain text by default"), but there's no hint to users who type `**bold**` expecting it to render — it won't on the recipient's side either. Users may discover this only when their recipient asks "why is your email full of stars?"
186 + - **Recommendation:** a faint inline hint below the toolbar: "Plain text — no Markdown or rich formatting." Or surface a single Markdown-on toggle for users who want it (the recipient will see raw Markdown unless the server renders).
187 +
188 + ### 20. Send button has no keyboard label / shortcut hint on mobile
189 + - **Category:** Discoverability.
190 + - **Location:** Mobile compose modal — no `<kbd>` hint next to Send.
191 + - **Observation:** desktop has Cmd+Enter to send; mobile doesn't expose any shortcut. Mobile users don't have a hardware keyboard usually, so this is fine, but if a user pairs a Bluetooth keyboard with their phone, the same shortcut should work.
192 + - **Recommendation:** wire Cmd+Enter / Ctrl+Enter into the mobile modal too. Already implemented in compose.html; the form-builder doesn't propagate.
193 +
194 + ### 21. No search progress indicator
195 + - **Category:** Feedback.
196 + - **Location:** `js/emails.js#search` — debounced 250 ms.
197 + - **Observation:** search results replace the list after the FTS5 query returns. For large mailboxes, this may be 500 ms+. No spinner; the list stays as-is until results land.
198 + - **Recommendation:** show a tiny spinner in the search input's right edge during the in-flight query.
199 +
200 + ---
201 +
202 + ## Cross-cutting / flat-design check
203 +
204 + The email surface shows the same patterns Phase 1 and Phase 2 identified, now appearing for the third time:
205 +
206 + - **State-by-color-alone** — status bar, snooze badge, send-state colors.
207 + - **Filter state not persisted across reload** — same root cause as tasks filters.
208 + - **Action overload in dense bars** — 10-button action bar parallels the bulk-actions bar problem in Phase 2 #15.
209 +
210 + When a pattern shows up across three independent phases, it's not a per-surface bug — it belongs in the design-system layer. The remediation plan's Step 8 (theme coverage) handled one half (color-only ↔ state); the missing half is filter state. Recommend adding to the design-system charter a rule: "every filter / sort / view-mode setting must be mirrored to the URL on change."
211 +
212 + ---
213 +
214 + ## Summary
215 +
216 + Email is the riskiest surface in GoingsOn — the todo file already flags it ("Email is the riskiest area — data loss potential + OAuth friction") — and the audit confirms three classes of risk: **send irreversibility** (no undo-send, no soft delay, no failure retry, no client-side size warning); **two compose UIs that drift** (desktop has validation and shortcuts, mobile doesn't); **action-bar density** in the reader (10 buttons with weak hierarchy and a duplicate Create Task entry). The single biggest user-trust fix is undo-send + size-warning (Findings #1 + #2) — both reduce the rate of "I just sent that?" mistakes. The single biggest cleanup is consolidating compose into one component (Finding #3) — it cuts the maintenance surface in half and ports address validation to mobile. The single biggest UI polish is restructuring the action bar (Findings #4 + #5) to put Reply forward and Delete + secondary actions in an overflow menu.
@@ -0,0 +1,245 @@
1 + # Phase 4 — Events & Calendar UX Audit
2 +
3 + **Scope:** every time-relative surface — the events list view, the month and week calendar grids, the day plan timeline (with painting and scheduling), weekly review, monthly review, the snooze flow as it touches events, reminders, time-block running state, and the date/time pickers used everywhere in this surface.
4 +
5 + **Stack:** Tauri 2 webview, vanilla HTML/CSS/JS. Files reviewed: `js/events.js`, `js/events-calendar.js`, `js/day-planning.js` (+ render / paint / schedule subfiles), `js/snooze.js`, `js/weekly-review.js` + render, `js/monthly-review.js` + render, `js/focus-timer.js`, `js/time-tracking.js`, the `#time-view` block in `index.html`, plus the matching CSS.
6 +
7 + **Method:** classical universal pass (Norman / Tognazzini / Raskin / HIG) plus the flat-design and cross-cutting checks.
8 +
9 + ---
10 +
11 + ## Surface snapshot
12 +
13 + - **Time tab pills:** Day · Week · Month · Timer · Events (5 pills; the todo for Phase 1 #1 already flags pill-discovery on mobile).
14 + - **Day Plan:** 24-hour timeline of 96 × 15-min slots (slot height read from `--timeline-slot-h`, 12 px desktop / 22 px mobile). Hour labels on hour boundaries only. Red 2-px "now" indicator updates every 60 s. Paint with mouse drag (desktop) or 2 s long-press (touch).
15 + - **Day Plan sidebar:** time summary, "Tasks to Schedule" list (virtual-scrolled), accomplished-inline section. Collapsible on mobile.
16 + - **Paint flow:** opens a modal that asks *what kind of thing* to create — Event, Time Block, or Link to Task. Block-type options: free_time, personal, vacation, focus.
17 + - **Events list:** three segmented sections — Recurring (collapsible), Upcoming (always open), Past (collapsible, reverse-chrono). Each section is virtually scrolled.
18 + - **Event form fields:** title, description, start_time (natural-date text), end_time (natural-date text), location, recurrence, block_type, contact_id, project_id.
19 + - **Snooze:** modal supports tasks and emails; **events are not snoozable**.
20 + - **Reminders:** **not implemented in the UI**. Per CLAUDE.md, "Event reminders" sits in the Notifications & Reminders sprint.
21 + - **Recurring events:** the list separates *templates* from *instances*; instances appear in Upcoming/Past, but only template editing is exposed.
22 + - **Calendar (month):** 7-col grid; event chips inline in cells with `.block-*` color; day-cell click opens inline day-detail under the grid.
23 + - **Calendar (week):** 7-col, hours 6 am–10 pm on desktop; mobile collapses to a swipeable single-day view.
24 + - **Time pickers:** mixture — `<input type="text">` with natural-date parsing in event form; `<input type="datetime-local">` in schedule-task / snooze / paint modals; `<input type="date">` in day-plan navigation.
25 +
26 + ---
27 +
28 + ## Critical (fix before declaring Phase 4 done)
29 +
30 + ### 1. Event reminders are completely absent
31 + - **Category:** Anticipation (Tog), Feedback, Forgiveness. (**Universal**)
32 + - **Location:** event form (`js/events.js#getEventFormFields`) has no reminder field; no scheduler hook; no notification permission request anywhere.
33 + - **Observation:** an event exists in the calendar but the application never tells the user "your meeting starts in 5 minutes". The only proactive surface is the events-tab status dot which polls every 30 s and reflects "upcoming events within a 15-min lead window" — but that's a peripheral signal, not a notification. On the day the user is busy and doesn't open the app, GoingsOn is silent. CLAUDE.md acknowledges this is queued; the audit confirms it's the single largest functionality gap in this surface.
34 + - **Why it matters:** for any app that calls itself a calendar, reminders are table stakes. Users will check Google / Apple calendars in parallel rather than rely on GoingsOn for time-based prompts. Once they do that, the events surface becomes shelf-ware.
35 + - **Recommendation:**
36 + - Add a `reminders` field to the event form: multi-select of "At time of event", "5 min before", "10 min", "15 min", "30 min", "1 hour", "1 day" (plus custom).
37 + - Wire to OS notifications via Tauri's `tauri-plugin-notification`. Request permission on first reminder set.
38 + - On launch and at every minute tick, evaluate upcoming events; emit notifications for ones whose lead time has elapsed.
39 + - Backend may already have the schema (recurring events store the rule); confirm and add `reminder_offsets_seconds` if missing.
40 + - This isn't a 1-day fix but it's a 1-week fix and it unblocks the calendar surface entirely.
41 +
42 + ### 2. Event form's start/end times are free-text with silent parse failure
43 + - **Category:** Error messages, Anticipation, Forgiveness. (**Universal**)
44 + - **Location:** `js/events.js` lines 107–115 — `start_time` and `end_time` are `type='text'` with `onInput="GoingsOn.utils.dateParsePreview"` and transform via `parseNaturalDate(v)`.
45 + - **Observation:** the user types "tomorow 3pm" (typo) or "Mar 32" (invalid) or "wednesday" (which Wednesday?). The preview helper shows the parsed result *if* parsing succeeds; on failure it does… something (need to verify — likely shows nothing or keeps the raw string). On Save, the form posts either a parsed ISO string or a raw fallback. The backend may accept the raw string as best-effort, store a wrong time, and the user only notices when the event shows up at the wrong moment.
46 + - **Why it matters:** date/time entry is the highest-leverage place for typos in a calendar app. Silent acceptance of unparseable input produces invisible-until-too-late errors. Standard pattern is preview-with-warning: as the user types, show the parsed value below the input ("→ Wed, Mar 12 at 3:00 PM"), with a red error if unparseable.
47 + - **Recommendation:**
48 + - On every keystroke, set the preview to one of three states: empty (no input), parsed-value (`→ Wed, Mar 12 at 3:00 PM`), or error (`Couldn't parse "tomorow 3pm" — try "tomorrow 3pm"`).
49 + - On submit, block save if the preview is in error state.
50 + - Add a small visible "shortcut" hint near the field: "tomorrow 3pm · friday 10am · 2026-12-25 09:00".
51 + - Consider adding a `<input type="datetime-local">` fallback alongside the natural-date text, toggleable — same shape the snooze modal already uses.
52 +
53 + ### 3. Events cannot be snoozed; same modal handles tasks and emails but not events
54 + - **Category:** Consistency. (**Universal**)
55 + - **Location:** `js/snooze.js` lines 72–78 — branches on `itemType === 'task'` or `'email'`; no event branch.
56 + - **Observation:** the snooze modal accepts `itemType` and calls the appropriate API. There is no event API for snooze and no entry point from the event detail / list / context menu. A user who wants "remind me about this meeting tomorrow" or "hide this from my upcoming list until Friday" has no path.
57 + - **Why it matters:** snoozing an event is a common pattern, especially for tentative meetings or events you can't confirm yet. The omission feels like a feature gap and reads inconsistently because tasks and emails *can* be snoozed.
58 + - **Recommendation:**
59 + - Either implement event-snooze (most natural — backend gets a `snoozed_until` column on events, snooze.js gets a third branch, list filters them out of Upcoming until the snooze expires), or
60 + - Decide explicitly that events are not snoozable and document why. (Reasoning could be: an event has a hard time anchor; "snoozing" it doesn't move the time — but the *visibility in the list* could still be snoozed.)
61 + - Recommend implement; this is a 1-day change.
62 +
63 + ### 4. All-day events are detected on render but cannot be authored
64 + - **Category:** Mappings, Anticipation. (**Universal**)
65 + - **Location:** `js/events-calendar.js` lines 210–215 — events with duration ≥ 23 hours are placed in a separate `.cal-week-allday-row`. Event form has no "All day" checkbox.
66 + - **Observation:** the calendar render *handles* all-day events: it pulls them out of the timed grid and renders them in a banner row above. But the only way to create one is to manually set `start_time` to a midnight value and `end_time` to the next midnight — which the natural-date parser may or may not do depending on what the user types. The asymmetry is sharp: the calendar can show all-day events that *can't be authored through the app*.
67 + - **Why it matters:** asymmetric features (renderable but not authorable) are confusing for users — they see "Conference Day" rendered as all-day on someone else's invite (imported from .ics or set via API) but can't replicate the affordance themselves.
68 + - **Recommendation:**
69 + - Add an "All day" checkbox at the top of the event form. When checked, hide the time fields and only show `start_date` and `end_date` (or just `date` for single-day all-day).
70 + - On render, the existing detection logic continues to work as a fallback for imported events.
71 + - This is the explicit todo item under "Sprint: Events → All-day event support". Promote to Phase 4 fix.
72 +
73 + ---
74 +
75 + ## Major (high impact, lower urgency)
76 +
77 + ### 5. Painting a block on mobile requires a 2-second long-press
78 + - **Category:** Fitts, Anticipation. (**Universal**)
79 + - **Location:** `js/day-planning.js` lines 84–88; long-press handler snaps to 30-min boundary.
80 + - **Observation:** to create anything on the timeline on mobile, the user must press and hold for 2 seconds on an empty slot. There's no shortcut (e.g., a floating "+" on the timeline, or a tap-to-create with default 30-min duration). 2 seconds is slow as a primary creation gesture — Apple Calendar uses ~0.5 s on iOS, Google Calendar a single tap.
81 + - **Why it matters:** if creating a block is the primary action on the day plan, it should be fast. A 2 s threshold also conflicts with users naturally scrolling: a brief pause during a scroll could trigger paint.
82 + - **Recommendation:**
83 + - Drop the long-press threshold to 500 ms.
84 + - Add a floating "+" button (FAB-style) bottom-right of the timeline; tap opens the same paint modal with `start = ceil(now, 15min)`, `end = start + 30min` defaults.
85 + - The drag-paint gesture remains for users who want a specific range.
86 +
87 + ### 6. Two overlapping "block" concepts in the domain
88 + - **Category:** Consistency, Mappings. (**Universal**)
89 + - **Location:** day plan creates "Time Blocks" (the paint modal's "Time Block" mode); the event form has a `block_type` field with the same set of options (free_time, personal, vacation, focus).
90 + - **Observation:** the codebase has two distinct concepts that share a name and a color vocabulary:
91 + - **Time Block** — created by painting on the day plan, lives as a separate entity, no title/description (just a colored span).
92 + - **Event with block_type** — a normal event that *also* has a block-color attached.
93 + Both render with `.block-focus`, `.block-personal`, etc. To a user, they look the same on the timeline. The data model and edit affordances differ.
94 + - **Why it matters:** when a user clicks a focus block, they can't tell whether they're editing an "Event" or a "Time Block" until the form opens. The naming is leaky and the dual model invites bugs (e.g., conflict detection treating them differently).
95 + - **Recommendation:**
96 + - Pick one. Either: collapse time blocks into events (every block *is* an event with a block_type, no separate Time Block entity), or keep them separate but rename one (e.g., "Time Block" stays for unstructured colored ranges; "Event" loses its `block_type` field and uses tags or labels instead).
97 + - The first option (events-only) is more consistent and reduces the model surface. Migration: convert existing Time Blocks to events with `block_type` set.
98 + - Surface in the UI: when a user clicks a colored span on the timeline, the edit modal title says "Edit time block" or "Edit event" based on the underlying type — currently this is implicit.
99 +
100 + ### 7. Recurring template vs occurrence editing is unsurfaced
101 + - **Category:** Modes, Feedback. (**Universal**)
102 + - **Location:** `js/events.js` Recurring section (lines 252+) shows templates only; instances appear in Upcoming/Past. Edit button on a template opens the form; edit button on an instance opens the form too — but the *scope of the edit* (this occurrence only? all future? all?) isn't asked.
103 + - **Observation:** Google Calendar, Apple Calendar, Outlook all prompt "Edit this event / This and future / All events" when editing a recurring instance. GoingsOn has no such prompt. Editing an instance presumably edits the *template* (which cascades to all occurrences). The user might think they're moving just tomorrow's instance and end up moving all 52 weekly instances.
104 + - **Why it matters:** silent cascading destroys schedules. A single misclick can break months of recurring data.
105 + - **Recommendation:**
106 + - When the user opens edit on an *instance* (not a template), prompt: "Edit this occurrence only · This and future · All occurrences" before showing the form.
107 + - When the user deletes a recurring instance, prompt the same.
108 + - Templates in the Recurring section behave as today (full series).
109 + - Backend must support per-occurrence overrides; verify.
110 +
111 + ### 8. No timezone awareness anywhere in the UI
112 + - **Category:** Anticipation, Forgiveness. (**Universal**)
113 + - **Location:** all date inputs use local browser time; storage strips Z; render uses `new Date()`.
114 + - **Observation:** events are stored and rendered in implicit local time. A user who creates a "3pm Tuesday" meeting in San Francisco and then travels to New York opens the app and sees "3pm Tuesday" — but their phone clock has shifted, and their counterpart in SF expected 3pm SF time, which is 6pm NY time. The app gives no warning, doesn't shift the display, doesn't surface the original timezone.
115 + - **Why it matters:** this is fine for purely-local users who never travel. For anyone with an online meeting across timezones, GoingsOn will silently mis-place every event. Even calendar imports from .ics (which carry timezone info) lose it on parse.
116 + - **Recommendation:**
117 + - Phase 1 fix: store each event's timezone (default: user's current zone at creation). Render in user's current zone, but show the *original zone* as a meta-line ("3pm Tuesday · originally PT").
118 + - Phase 2 fix: allow editing the timezone on the event form. Surface a settings option "default timezone" and "always show in this zone".
119 + - This is a lot of work; the medium-term fix is to surface the user's current timezone in the shell (small text near the date in the day plan or weekly review) so users at least *see* what zone they're in.
120 +
121 + ### 9. The "now" indicator updates every 60 s on a 12 px / 15 min grid
122 + - **Category:** Feedback, Visibility of state. (**Universal/Polish**)
123 + - **Location:** `js/day-planning.js` line 48 — `setInterval(updateNowLine, 60000)`.
124 + - **Observation:** the slot is 12 px tall and represents 15 min — so 1 min ≈ 0.8 px. The indicator updates once a minute. Between updates, it's stale by up to a full minute, which on the timeline means up to 0.8 px of drift — visually imperceptible. *But* the staleness compounds with the "events tab dot" 30 s poll and creates a system where state lags inconsistently across surfaces.
125 + - **Why it matters:** less about the visual drift (which is fine), more about battery and consistency. Two `setInterval`s running while the app is open: 60 s for the now-line, 30 s for the upcoming-events dot.
126 + - **Recommendation:** unify into a single 60 s tick driven by `requestAnimationFrame` + visibility-API gating (pause when window is hidden). Drop the polled event-dot; reuse the same tick. Battery and CPU savings are real on a Tauri webview that's open in the background.
127 +
128 + ### 10. Conflict detection is server-side and only shows after save
129 + - **Category:** Anticipation, Forgiveness. (**Universal**)
130 + - **Location:** day-planning timeline applies `.conflict` class on render after the backend computes overlaps.
131 + - **Observation:** while the user is *painting* a new block, the timeline doesn't show whether the painted range overlaps an existing event. The schedule-task modal has a conflict warning slot (`#schedule-conflict-warning`) but it's populated server-side, after the user picks a time. There's no real-time "this conflicts with X" while painting.
132 + - **Why it matters:** the user paints a block, opens the modal, fills the form, hits Save, then sees "Conflicts with Standup". Now they have to back out and re-paint. The conflict should be visible *during* the paint.
133 + - **Recommendation:**
134 + - During paint, if the painted range overlaps an existing block, color the preview red instead of blue and show a small inline label "Conflicts with: Standup".
135 + - In the modal that opens after paint, pre-populate the conflict warning from client-side data (the events for this day are already loaded into the day plan).
136 + - Server-side check on save remains as the authoritative gate.
137 +
138 + ### 11. Vacation day banner has no UI to set the vacation state
139 + - **Category:** Mappings, Consistency. (**Universal**)
140 + - **Location:** `js/day-planning-render.js` lines 42–48 — banner shown if `dayPlanData.isVacationDay`.
141 + - **Observation:** the timeline shows "Day Off" if the day is marked vacation, but there's no UI to mark a day as vacation. The user can paint a block with `block_type=vacation` (which is *something*), or there might be a setting elsewhere. The relationship between vacation blocks and vacation days isn't surfaced.
142 + - **Recommendation:**
143 + - Add a small toggle near the day-plan date picker: "Mark as day off". When toggled, sets `isVacationDay` and dims/grays the timeline.
144 + - When the user creates a vacation-type block that spans the full day, prompt: "Mark the day as a day off?"
145 + - Either way, the affordance for the banner needs to exist somewhere visible.
146 +
147 + ### 12. Event form mixes `contact_id` and `project_id` with no surfaced purpose
148 + - **Category:** Visibility, Anticipation. (**Universal/Polish**)
149 + - **Location:** `js/events.js` lines 174–195 — event form has both `contact_id` (select) and `project_id` (hidden if pre-selected).
150 + - **Observation:** the user can attach an event to a contact and a project. The form lets them set it. But nowhere in the event row, calendar, or detail view does the contact or project surface — at least not obviously. (Contact may render in detail; project may filter the list but isn't a row column.) The user does work that doesn't pay off in visibility.
151 + - **Recommendation:**
152 + - Either render contact/project chips on the event row (consistent with tasks), or
153 + - Add a "Show project / contact in event row" toggle in settings, or
154 + - Remove these fields from the form if they don't drive visible behavior.
155 +
156 + ### 13. Week view's hour range (6 am–10 pm) is hard-coded
157 + - **Category:** Anticipation. (**Universal**)
158 + - **Location:** `js/events-calendar.js` lines 183–185 — `HOURS_START=6`, `HOURS_END=22`.
159 + - **Observation:** night-shift workers, parents up at 5 am, people with 11 pm online classes — all of them have events outside 6 am–10 pm and the week view simply doesn't show them. The events still exist on the day plan timeline (24h), but the week view truncates.
160 + - **Recommendation:** make the range a user setting. Default 6 am–10 pm is fine for most; allow overrides in Settings → Time. Smarter: auto-expand the range if any event in the visible week falls outside it.
161 +
162 + ---
163 +
164 + ## Minor (worth fixing during normal cleanup)
165 +
166 + ### 14. Running focus block has no on-timeline indicator
167 + - **Category:** Visibility. (**Polish**)
168 + - **Location:** `js/focus-timer.js` shows a full-screen overlay during focus; the timeline doesn't pulse / glow / animate the block while it's running.
169 + - **Observation:** when the user starts a focus session on a 10–11 am block, the block stays static-red on the timeline. The overlay says "Focus Mode 14:23 remaining" but the timeline doesn't reflect that *this* block is the live one. If the user dismisses the overlay (which they shouldn't but might), there's no in-timeline cue.
170 + - **Recommendation:** add a `.timeline-item.running` class with a subtle pulse animation (1 s `box-shadow` cycle in `--accent-red`) for the currently-active focus block. Pair with a progress bar inside the block showing elapsed/total.
171 +
172 + ### 15. Hour labels only on hour starts; users scanning the middle of the timeline have no time reference
173 + - **Category:** Visibility, Anticipation. (**Polish**)
174 + - **Location:** `js/day-planning-render.js` line 78 — `timeStr` rendered only if `isHourStart`.
175 + - **Observation:** a user scrolled to the middle of the day sees blocks but the nearest hour label may be 30+ px above. Adding labels at every 15-min slot is too much; at every 30 min (`isHalfHour`) would be a useful compromise.
176 + - **Recommendation:** show a light label on quarter 2 (`:30`) — e.g., faded `--text-muted` "9:30". Hour labels stay prominent.
177 +
178 + ### 16. Block colors are the only semantic for block type
179 + - **Category:** Accessibility, Visibility (color-as-sole-signal). (**Flat — recurring pattern**)
180 + - **Location:** `.block-focus` / `.block-personal` / `.block-vacation` / `.block-free_time` all rely on `--accent-*` for distinction.
181 + - **Observation:** same pattern as Phase 1 #2 (sync dot), Phase 2 #6 (priority), Phase 3 #6 (status). On themes where two accent colors are close in luminance, focus and personal can look identical.
182 + - **Recommendation:** add a small glyph or letter inside each block ("F", "P", "V", "Free") — at least at desktop widths. On narrow timelines where the block height is < 24 px, the glyph degrades to a left-edge stripe pattern.
183 +
184 + ### 17. Schedule-task modal ignores the task's estimated duration
185 + - **Category:** Anticipation. (**Polish**)
186 + - **Location:** `js/day-planning-schedule.js` — duration presets 15m, 30m (default), 45m, 1h, 1.5h, 2h.
187 + - **Observation:** if the task has `estimated_minutes` set, the modal should default to that value instead of 30. A user who carefully estimated "60 minutes" for a task and then schedules it shouldn't have to re-pick 1h every time.
188 + - **Recommendation:** read `task.estimated_minutes` and pre-select the closest preset (or surface a custom field with the value pre-filled).
189 +
190 + ### 18. Past events are kept in the list forever; only collapsible, never archived
191 + - **Category:** Hierarchy. (**Polish**)
192 + - **Location:** `js/events.js` Past section.
193 + - **Observation:** an event from 2 years ago lives in the Past section, collapsed by default but always loaded into the list. On a heavy-use account, this is hundreds of dead rows in memory.
194 + - **Recommendation:** auto-collapse "Past" by default *and* time-window the query to the last 90 days at first load; show "Load older" to fetch more. The virtual scroller helps with render cost but not memory.
195 +
196 + ### 19. The Time tab's Timer pill duplicates Track Time / Focus Mode entry points
197 + - **Category:** Consistency (with Phase 2 #5).
198 + - **Location:** Time tab pill "Timer" (index.html:239).
199 + - **Observation:** Phase 2 already noted that Track Time, Focus Mode, and Schedule Time Block are three flows for one backend. The Timer pill is now a *fourth* surface for time-related UI. A user clicking Timer expects… what? Track-time controls? Focus controls? Both?
200 + - **Recommendation:** rename the pill and the underlying view to match what it actually shows. If it's a Pomodoro / focus session launcher, call it "Focus"; if it's a track-time dashboard with history, call it "Time log". The pill label should reveal the contents.
201 +
202 + ### 20. Mobile week view collapses to swipeable single-day, but the swipe is undiscoverable
203 + - **Category:** Discoverability. (**Polish**)
204 + - **Location:** `js/events-calendar.js` lines 335–404 — mobile day swipe.
205 + - **Observation:** on mobile, "Week" pill renders a single day with swipe-left / swipe-right to change days. There's no on-screen indication ("← swipe →") on first view. A first-time mobile user may not realize they can swipe.
206 + - **Recommendation:** show a subtle two-arrow hint on the date header for the first three opens (localStorage flag), then suppress.
207 +
208 + ---
209 +
210 + ## Polish
211 +
212 + ### 21. Natural-date preview text behavior is unspecified
213 + - **Category:** Anticipation.
214 + - **Location:** event form `onInput="GoingsOn.utils.dateParsePreview"`.
215 + - **Observation:** the helper exists but its rendering is unverified. If it doesn't yet exist or just logs to console, this is the underlying cause of Finding #2.
216 + - **Recommendation:** fix as part of #2.
217 +
218 + ### 22. No keyboard shortcut for new event from the events list
219 + - **Category:** Discoverability.
220 + - **Location:** keyboard.js shortcuts are tab-aware ("New for current view"), so `n` should work — verify.
221 + - **Observation:** Phase 2 Tasks has `n` for new task. Events should be symmetric. Quick check: from the Events pill, does `n` open the event form?
222 + - **Recommendation:** if not, wire it. One line in `newItemForCurrentView`.
223 +
224 + ### 23. Vacation block (type) is not the same as Vacation day (banner)
225 + - **Category:** Consistency. (**Polish**)
226 + - **Observation:** the user can paint a `block_type=vacation` block (which is a colored span on one day) *or* the day can be `isVacationDay=true` (which is a full-day banner). Two ways to express "day off". (Related to Finding #11.)
227 + - **Recommendation:** rationalize. Either auto-promote a full-day vacation block to `isVacationDay`, or remove one of the two encodings.
228 +
229 + ---
230 +
231 + ## Cross-cutting / flat-design check
232 +
233 + The events surface concentrates the patterns we've seen in earlier phases:
234 +
235 + - **Color-as-sole-signal**: block types (#16) replay Phase 1 #2, Phase 2 #6, Phase 3 #6.
236 + - **Filter / view state not persisted**: the day-plan date selection, the week / month navigation, none mirror to the URL. Reload returns to today. Same root cause as Phase 1 #9, Phase 2 #10, Phase 3 #8.
237 + - **Action bar density / dual entry points**: Time tab has 5 pills, one of which (Timer) parallels Track Time and Focus Mode flows; Phase 2 #5 and Phase 3 #5 are the same shape.
238 +
239 + A new pattern emerges here: **passive-feature gaps** — reminders, snooze-events, all-day toggle, recurring-instance edit prompt — features that exist *in the data model or render path* but aren't authored through the UI. Three of the four critical findings are this shape. The events surface has the most domain richness (recurrence, all-day, time zones, conflicts, reminders) and the thinnest UI affordances per feature.
240 +
241 + ---
242 +
243 + ## Summary
244 +
245 + The events surface is the most domain-rich and the most under-served in GoingsOn. The data model supports recurrence, all-day events, conflicts, block types, contacts, and projects; the UI exposes maybe half of these as authoring affordances. The single largest functional gap is reminders (Finding #1) — without them, this is a calendar that doesn't tell you about your meetings. The single largest correctness gap is silent natural-date parsing (Finding #2) — every wrong time saved is a missed meeting. The single largest consistency gap is the two-headed "block" concept (Finding #6) — collapsing it into one model would simplify everything downstream. After Phase 4, the audit has seen the same three meta-patterns (state-by-color, filter-not-persisted, action-overload) across four surfaces; they should be promoted from per-phase findings to design-system charter rules.
@@ -0,0 +1,246 @@
1 + # Phase 5 — Projects, Contacts, Settings UX Audit
2 +
3 + **Scope:** the configuration + management surfaces — the projects grid and per-project dashboard with milestones; the contacts grid, per-contact dashboard, and sub-collections (email/phone/social/custom-field); the full settings page with its six sections (Appearance, Notifications, Planning & Review, Plugins, Cloud Sync, Data); the import / export flows; the OTA updater. These are the lowest-traffic but most-stateful surfaces in the app.
4 +
5 + **Stack:** Tauri 2 webview, vanilla HTML/CSS/JS. Files reviewed: `js/projects.js`, `js/projects-render.js`, `js/contacts.js`, `js/contacts-render.js`, `js/contact-dashboard.js`, `js/settings.js`, `js/settings-sync.js`, `js/themes.js`, `js/import.js`, `js/import-external.js`, `js/export.js`, `js/shared-updater.js`, `js/updater.js`, plus the matching CSS and the `#projects-view`, `#contacts-view`, `#contact-dashboard`, `#settings-view`, and `#project-dashboard-view` blocks in `index.html`.
6 +
7 + **Method:** classical universal pass (Norman / Tognazzini / Raskin / HIG) plus the cross-cutting checks.
8 +
9 + ---
10 +
11 + ## Surface snapshot
12 +
13 + - **Projects:** card grid, `n` to create, click-to-open-dashboard. Project has name / description / type (SideProject / Job / Company / Essay / Article / Other) / status (Active / On Hold / Completed / Archived). Dashboard is a 4-column grid (Tasks · Events · Emails · Attachments) plus a Milestones section above. Milestones support reorder via ▲/▼ buttons, edit, delete.
14 + - **Contacts:** card grid, search + tag filter. Click opens a *full-page* contact dashboard (separate view, like the task overview). Sub-collections: email / phone / social / customField — each has Add and Remove but **no Edit**. Bulk select + bulk tag (via `prompt()`). Tags shown as pills.
15 + - **Settings:** full-page view with sidebar + content layout. Six sections: **Appearance**, **Notifications**, **Planning & Review**, **Plugins**, **Cloud Sync**, **Data (Import & Export)**. Most settings auto-save to `localStorage` on change. Cloud Sync has stateful OAuth flow.
16 + - **Import:** two parallel entry points — "Import Contacts / Calendar" (external file: vCard or .ics) and "Import via Plugin" (plugin-driven, multi-step wizard).
17 + - **Export:** three buttons (JSON / Tasks CSV / Events ICS) and a separate backup system (manual + automatic).
18 + - **Updater:** thin local wrapper around a shared module from `MNW/shared/tauri-updater-ui` (per repo layout).
19 +
20 + ---
21 +
22 + ## Critical (fix before declaring Phase 5 done)
23 +
24 + ### 1. There is no "About" / version surface in the app at all
25 + - **Category:** Visibility of state, Error messages. (**Universal**)
26 + - **Location:** Settings has six sections (Appearance, Notifications, Planning & Review, Plugins, Cloud Sync, Data) — none of them show a version number. `js/app.js#showAbout` exists but is only invoked from… nowhere visible in the inventory (the welcome modal opens it, but only on first run).
27 + - **Observation:** a user who wants to know "what version of GoingsOn am I running?" cannot find out from the settings page. The only place the version appears is the OTA updater dialog when an update is offered — i.e., when the app is *about to no longer be that version*. CLAUDE.md and the build process emit versions (the current desktop is 0.3.1, iOS is 0.3.3), but the user has no in-app affordance to verify.
28 + - **Why it matters:** support requests start with "what version are you on?" Bug reports degrade quickly when the user can't answer. Onboarding to TestFlight or Discord communities works the same way.
29 + - **Recommendation:**
30 + - Add an **About** section (or a footer in the settings sidebar): version, build date, git commit short hash, license, link to changelog, link to support.
31 + - Make the existing `showAbout()` modal accessible from a header menu item or from the bottom of the settings sidebar.
32 + - Pair with a "Check for updates" button that triggers the OTA flow on demand — currently updates check on a schedule the user can't see.
33 +
34 + ### 2. Contact bulk-tag uses native `window.prompt()`
35 + - **Category:** Consistency, Forgiveness. (**Universal**)
36 + - **Location:** `js/contacts.js#bulkTag` — `const tag = prompt('Tag to add:');`
37 + - **Observation:** Step 9 of the consolidation already removed every `window.confirm()` call from the frontend. `window.prompt()` survived. The bulk-tag flow opens a native browser prompt: no theme, no validation, no autocomplete against existing tags, no preview ("you're about to add this tag to 12 contacts"). On Tauri 2 some platforms even disable `prompt()` entirely (it returns null on iOS).
38 + - **Why it matters:** the design-system charter (`docs/design-system.md`) says all confirms/inputs route through `GoingsOn.ui` — `prompt()` is exactly the kind of native call the charter forbids. On a platform where it's disabled, the feature silently breaks.
39 + - **Recommendation:**
40 + - Add `GoingsOn.ui.showPromptDialog(title, message, opts)` that renders into the global modal with a `<input class="form-input">` plus OK/Cancel. Mirror the existing `showConfirmDialog` API.
41 + - Replace the `prompt()` call. While there, add autocomplete from existing tags (the backend already returns tag lists) and a confirmation preview: "Add tag 'follow-up' to 12 contacts?"
42 + - Add a lint rule for `prompt(` in `scripts/lint-frontend.sh`.
43 +
44 + ### 3. Sub-collections on contacts can be Added and Removed but not Edited
45 + - **Category:** Forgiveness, Anticipation. (**Universal**)
46 + - **Location:** `js/contacts.js` `openAddSubCollection` exists for each of email/phone/social/customField; no `openEditSubCollection` exists.
47 + - **Observation:** a user with a contact whose email is `bob@oldcompany.com` and wants to change it to `bob@newcompany.com` must:
48 + 1. Remove the old email (× button → confirm)
49 + 2. Re-Add the new email (modal → form → submit)
50 + 3. Re-mark Primary if it was primary
51 + This loses any metadata (label, primary flag) and risks accidentally removing the wrong row.
52 + - **Why it matters:** address book contacts are append-only in practice but constantly need correction. The two-step remove-then-add flow forces users to re-enter context they already have.
53 + - **Recommendation:**
54 + - Add a tap target on each sub-item that opens the same form modal pre-filled. The Add form already exists; reuse it for Edit by passing the existing record.
55 + - Visual: clicking the row itself opens Edit; the × stays for delete.
56 + - Backend: most sub-collections are updateable; verify and wire.
57 +
58 + ### 4. Bulk operations on contacts have no undo (recurring pattern)
59 + - **Category:** Forgiveness, Consistency. (**Universal — recurring pattern**)
60 + - **Location:** `js/contacts.js#bulkTag` calls the API and just shows a success toast; no `showUndoToast`.
61 + - **Observation:** same shape as Phase 2 #2 (bulk task delete/snooze/set-project lack undo while single-task complete has it) — the bulk contact operations have no undo wrapper, while the single-project delete *does* use `showUndoToast` (`projects.js:200`). This is the same asymmetry repeating: undo exists in the codebase, gets wired only sometimes.
62 + - **Why it matters:** a user accidentally tags 50 contacts with the wrong label and has no recovery. The cumulative effect across phases: every bulk operation in the app needs to be checked for undo coverage.
63 + - **Recommendation:**
64 + - Phase 2 already recommended a `bulkActionWithUndo(action, inverse, ids, prevState)` helper. Build it once; use it here, in bulk tasks, and in any future bulk contact actions (bulk delete, bulk merge).
65 + - For bulk tag specifically: capture pre-state (which contacts already had the tag — those are skipped on undo; the newly-tagged ones are reverted). 10-line helper.
66 +
67 + ---
68 +
69 + ## Major (high impact, lower urgency)
70 +
71 + ### 5. Settings is a full-page view, not a modal — pulls users out of context
72 + - **Category:** Modes, Modelessness (Raskin). (**Universal**)
73 + - **Location:** `js/settings.js#openSettings` calls `GoingsOn.navigation.switchView('settings')`, which tears down the current view.
74 + - **Observation:** the rest of GoingsOn uses modals for configuration tasks (task edit, event edit, contact edit, snooze picker). Settings is the exception — it's a full-page replacement. A user in the middle of triaging tasks who realizes they want to change the theme has to leave Tasks entirely, change the theme, then re-navigate back, losing scroll position and filter state (compounding with the Phase 2 filter-not-persisted issue).
75 + - **Why it matters:** the disruption isn't huge for users who only visit settings occasionally, but it means the "small adjustment" use case (toggle a plugin, switch theme to test, change work hours) becomes a multi-step trip. It also visually loses the parent context, which means changes to theme don't preview against the user's actual content.
76 + - **Recommendation:**
77 + - Convert Settings to a side-drawer (right-edge sliding panel) or a modal with the sidebar/content layout intact.
78 + - The sidebar layout still works inside a modal — the sections fit comfortably in a 720 × 600 dialog.
79 + - This is also the natural fix for Finding #14 (theme preview): if the settings modal overlays content rather than replacing it, the user sees theme changes apply behind it.
80 +
81 + ### 6. Most settings auto-save on change with no "undo last change" affordance
82 + - **Category:** Forgiveness, Consistency. (**Universal**)
83 + - **Location:** Appearance theme onChange writes `localStorage` and re-applies CSS variables immediately; Planning & Review work-hours `<select>` writes localStorage on every change.
84 + - **Observation:** auto-save is the right default for settings (no Save button to forget), but it pairs poorly with destructive changes. A user clicks the theme dropdown, scrolls through, "previews" a theme by mouse-over — except every selection commits. Switching to a theme they hate and then back to "the previous one" requires remembering the previous theme's name.
85 + - **Why it matters:** auto-save with no undo is the worst form of "fast". Fast actions need fast recovery.
86 + - **Recommendation:**
87 + - On every auto-saved setting change, surface a small "Undo" toast with the previous value: "Theme: Catppuccin Frappe · Undo".
88 + - Track per-session "last setting changed"; the toast lasts 6 s and clicking Undo reverts to the prior value.
89 + - For multi-field forms (work hours, backup settings), batch changes into a single undo toast: "Settings updated · Undo".
90 +
91 + ### 7. Milestone reordering is up/down buttons with no drag
92 + - **Category:** Fitts, Anticipation. (**Universal/Polish**)
93 + - **Location:** `js/projects-render.js` lines 163–182 — `moveMilestone(id, ±1)` buttons.
94 + - **Observation:** with three milestones, ▲/▼ works fine. With ten, moving #10 to position #1 requires nine clicks. There's no drag-handle, no "move to top/bottom" affordance.
95 + - **Recommendation:**
96 + - Add drag-drop (`SortableJS` or a 30-line custom). The existing `.milestone-card` becomes the drag target.
97 + - Add `Move to top` / `Move to bottom` to the context menu.
98 + - The reorder buttons can stay as a fallback.
99 +
100 + ### 8. Project edit form lets you change status to Completed/Archived but the create form doesn't
101 + - **Category:** Consistency, Mappings. (**Polish/Universal**)
102 + - **Location:** `js/projects.js` lines 65–73 — `PROJECT_STATUSES.slice(0, 2)` for create, full list for edit.
103 + - **Observation:** a user creating a project can only choose Active or On Hold. To create a project that's already Completed (e.g., backfilling) requires creating it as Active and immediately editing it.
104 + - **Why it matters:** the asymmetry is mild — most projects start Active — but it surprises users who are migrating from another system and want to record completed work for retrospective.
105 + - **Recommendation:** show all four statuses in the create form. The "edit changes more than create" pattern is unusual and worth removing unless there's a backend constraint.
106 +
107 + ### 9. No "Account / Profile" section; Cloud Sync is the only place a connected account is mentioned
108 + - **Category:** Visibility of state, Trust. (**Universal**)
109 + - **Location:** settings sections list — no Account section; `settings-sync.js` only shows "Connected to {serverUrl}" without surfacing which Makenot.work account is signed in.
110 + - **Observation:** after a user connects their Makenot.work account via OAuth, the sync section shows server URL and sync stats but doesn't tell them *which account email* they're using. To check, they'd have to disconnect (which logs them out) and re-connect (which re-prompts the OAuth flow with email shown). There's also no in-app affordance to:
111 + - See their account email
112 + - Change the password they use to authenticate
113 + - View their subscription details beyond "active / not active"
114 + - Get a backup of their encryption recovery key
115 + - **Why it matters:** for a sync product, the connected identity is primary trust state. A user who shared their device with someone, or who has multiple accounts, can't tell from the UI which one is active.
116 + - **Recommendation:**
117 + - Add an **Account** section: connected email, last login, "Open account on makenot.work" link, "Sign out" button.
118 + - Surface the subscription tier (Basic / Small Files / Big Files / Everything) with current price and renewal date.
119 + - Recovery key: this is critical for E2EE — surface a "Generate recovery key" affordance (the backend supports it per the todo).
120 +
121 + ### 10. Export has no progress indicator and no item-count preview
122 + - **Category:** Feedback, Anticipation. (**Universal**)
123 + - **Location:** `js/export.js#exportJSON` (and tasks CSV, events ICS) — `await GoingsOn.api.export.json(filePath)` is awaited synchronously; success toast appears after.
124 + - **Observation:** for a small database (100 items) the export finishes in <1 s and the toast confirms. For a large mailbox / task list (10 K+ items) the export may take 30 s. During that time the user sees nothing — the file picker has closed, the toast hasn't arrived. They may click again, try to use the app, or quit thinking it's stuck.
125 + - **Why it matters:** anything over 500 ms needs a progress indicator. Anything over 5 s needs a cancellation affordance.
126 + - **Recommendation:**
127 + - Replace the dialog → silent → toast pattern with: dialog → modal "Exporting…" with item count and a progress bar → toast on complete.
128 + - If backend can't stream progress, show indeterminate spinner.
129 + - Add Cancel button that aborts mid-export.
130 +
131 + ### 11. Default backup retention is "Keep 1 backup (Recommended)"
132 + - **Category:** Forgiveness, Anticipation. (**Universal**)
133 + - **Location:** `js/settings.js#renderData` — `retentionOptions = [{ value: 1, label: 'Keep 1 backup (Recommended)' }, ...]`.
134 + - **Observation:** the recommended retention policy is one backup. Each new automatic backup deletes the previous one. If the latest backup happened during a corruption window (e.g., a bad write, a SyncKit conflict that wasn't resolved well), the user has no fallback.
135 + - **Why it matters:** backups exist *because* the latest state might be bad. "Keep 1" defeats the purpose. A reasonable default is 3 or 7.
136 + - **Recommendation:**
137 + - Change the default to "Keep 7 backups (Recommended)".
138 + - Add a short hint: "Older backups protect against corruption that happened recently."
139 + - Optionally: tier the retention — daily for last 7 days + weekly for last 4 weeks. Standard rolling-backup pattern.
140 +
141 + ### 12. Theme picker has no live preview; every change commits and re-renders
142 + - **Category:** Anticipation, Forgiveness. (**Universal**)
143 + - **Location:** `js/themes.js` + `js/settings.js#renderAppearance` — `<select>` with `onchange="GoingsOn.themes.onChange(this.value)"` applies the theme immediately.
144 + - **Observation:** the theme list is a dropdown. Users select a theme; it applies. To preview a different one, they select it; it applies. There's no preview-on-hover, no thumbnail grid, no side-by-side. With 9+ themes, users either commit-then-revert repeatedly (each commit writes to localStorage and re-applies all CSS variables) or settle for the first acceptable one.
145 + - **Why it matters:** themes are a primary differentiator for an app that ships 9 of them. Letting users browse them efficiently matters.
146 + - **Recommendation:**
147 + - Replace the dropdown with a grid of thumbnail cards — each card shows a small mockup of the app chrome (header, tab, button) in that theme's colors. Hover applies for preview; click commits.
148 + - Preview on hover is the lowest-effort improvement; the thumbnail grid is the polish version.
149 +
150 + ### 13. Plan / Review "nudge" settings exist but the nudges aren't documented
151 + - **Category:** Anticipation, Mappings. (**Universal/Polish**)
152 + - **Location:** `js/settings.js#renderPlanning` — Plan nudges and Review nudges toggles, default enabled.
153 + - **Observation:** the settings have toggles for "Plan nudges" and "Review nudges". What's a nudge? When does it appear? Where? The form-hint doesn't explain. A user toggling them off may not know what they're turning off.
154 + - **Recommendation:**
155 + - Add a one-line hint under each: "Plan nudges: show a dot on the Day pill when you haven't planned today yet." / "Review nudges: show a dot on the Week pill when this week's review isn't complete."
156 + - Better: a tiny "See where nudges appear →" link that highlights the dot.
157 +
158 + ### 14. Import has two parallel entry points; the relationship isn't surfaced
159 + - **Category:** Consistency, Mappings. (**Universal/Polish**)
160 + - **Location:** Settings → Import & Export has both "Import Contacts / Calendar" (external file) and "Import via Plugin" (plugin-based).
161 + - **Observation:** a user with a vCard file might naturally try "Import via Plugin" first (because vCard plugin support is on the roadmap — `todo.md` Sprint: Plugin System). Today they'd be told "no plugin matches your file" or similar. The two entry points have overlapping conceptual scope.
162 + - **Recommendation:**
163 + - Unify into a single "Import" button that opens a modal: "What are you importing? Contacts (vCard) · Calendar (.ics) · Other (use a plugin)". The third path leads to the plugin wizard.
164 + - This puts the user's data type at the front of the decision rather than the implementation.
165 +
166 + ---
167 +
168 + ## Minor (worth fixing during normal cleanup)
169 +
170 + ### 15. Contact avatars are uncolored initials; all contacts look the same
171 + - **Category:** Visibility, Polish. (**Polish**)
172 + - **Location:** `js/contacts-render.js` — `.contact-avatar` styled with default neutral colors; no hash-to-color.
173 + - **Observation:** typical address books color the initials avatar by hashing the name. GoingsOn renders every avatar the same color, so a list of 30 contacts has 30 identical-looking circles. Scanning the list by avatar is impossible.
174 + - **Recommendation:** hash `contact.id` (or `displayName`) to one of 8 accent-tinted background colors. The accent palette already has 6 (`--accent-yellow / green / blue / purple / red / cyan`); cycle through them.
175 +
176 + ### 16. Tag filter on contacts is a single-select dropdown
177 + - **Category:** Anticipation. (**Polish**)
178 + - **Location:** Contacts filter bar.
179 + - **Observation:** the filter is one tag at a time. A user with multiple tags can't filter to "friend AND coworker" or "friend OR vendor". The bulk-tag operation also adds only one tag at a time.
180 + - **Recommendation:** convert the filter to a multi-select chip control. Bulk-tag could accept multiple tags in the prompt (once #2 lands and the input is a real form field).
181 +
182 + ### 17. Contacts list has no sort control
183 + - **Category:** Anticipation. (**Polish**)
184 + - **Location:** Contacts page header.
185 + - **Observation:** the list is sorted alphabetically by display name (backend default). No UI to sort by recently added, last contacted, company, or last interaction.
186 + - **Recommendation:** add a sort dropdown next to the search/filter. Same pattern as the mobile sort dropdown in tasks.
187 +
188 + ### 18. Projects list has no sort, no filter, no search
189 + - **Category:** Anticipation. (**Polish**)
190 + - **Location:** Projects page header.
191 + - **Observation:** with 20+ projects, no way to filter by status, type, or recency. Cards stack chronologically.
192 + - **Recommendation:** add a status filter and a sort control. For now, hide completed/archived behind a toggle (default: show only Active and On Hold).
193 +
194 + ### 19. Plugin item description has weird format
195 + - **Category:** Polish. (**Polish**)
196 + - **Location:** `js/settings.js` plugin render — `<span class="plugin-extensions">Files: .${extensions.join(', .')} | Types: ${entityTypes.join(', ')}</span>`
197 + - **Observation:** the format reads as "Files: .vcf, .vcard | Types: contact". The `.` prefix on the first item gets `.${join('.')}` and produces `.vcf, .vcard` — which reads almost right, but the `|` pipe between Files and Types is visually clunky.
198 + - **Recommendation:** use two lines with subtle separation, or a `·` dot separator. Drop the `|`.
199 +
200 + ### 20. Settings sidebar active state is a faint color change
201 + - **Category:** Visibility. (**Polish — recurring pattern**)
202 + - **Location:** `.settings-nav-item.active` styling.
203 + - **Observation:** same pattern as Phase 1 #4 — the active settings section is differentiated by a small color change. Easy to miss when scanning. State-by-color-alone, again.
204 + - **Recommendation:** add a left-edge accent stripe (`border-left: 3px solid var(--accent-blue)`) to the active item, plus the existing color shift. Matches the pattern recommended in Phase 1.
205 +
206 + ### 21. Settings auto-save toast is missing
207 + - **Category:** Feedback. (**Polish**)
208 + - **Location:** plugin toggle, theme change, work-hours change — all write silently to localStorage / API.
209 + - **Observation:** the user toggles "Enable automatic backups" → the checkbox state changes but nothing confirms the save. (Inferred — verify.) A small flash, a subtle "Saved" toast, anything to confirm the change persisted.
210 + - **Recommendation:** for every auto-save, show a small inline "Saved ✓" near the changed field that fades after 1.5 s. Don't use the toast system for every settings change — too noisy.
211 +
212 + ---
213 +
214 + ## Polish
215 +
216 + ### 22. Project dashboard's 4-column grid breaks at narrow widths
217 + - **Category:** Polish.
218 + - **Location:** `.project-dashboard-grid` CSS.
219 + - **Observation:** Tasks · Events · Emails · Attachments in a 4-column grid. On a 1024-wide window (small desktop / tablet), each column is ~220 px, which truncates row content quickly.
220 + - **Recommendation:** collapse to 2×2 below 1100 px and to 1-column below 768 px.
221 +
222 + ### 23. `showAbout` modal exists in `app.js` but only the welcome flow invokes it
223 + - **Category:** Polish.
224 + - **Location:** `js/app.js#showAbout`.
225 + - **Observation:** see Finding #1 — the modal exists but has no entry point in the running app. Either wire it up (from settings sidebar bottom, or a header overflow menu) or delete it.
226 +
227 + ---
228 +
229 + ## Cross-cutting / flat-design check
230 +
231 + The patterns from earlier phases continue here:
232 +
233 + - **Color-as-sole-signal** — settings sidebar `.active` state (Finding #20) repeats Phase 1 #4 (mobile tab active), Phase 2 #6 (priority), Phase 4 #16 (block type).
234 + - **Filter-state-not-persisted** — projects and contacts list states (tag filter, search query) live in the DOM; the URL doesn't carry them. Same root cause as Phase 1 #9, Phase 2 #10, Phase 3 #8, Phase 4 (week-nav).
235 + - **No-undo on bulk operations** — contacts bulk-tag (Finding #4) repeats Phase 2 #2 (tasks).
236 + - **Native browser dialogs forbidden by charter** — `window.prompt()` survives in contacts bulk-tag (Finding #2). `window.confirm()` was killed in Step 9; `prompt()` is the same problem with the same fix.
237 +
238 + **One new pattern** specific to Phase 5: **stateful flows hide their state**. The OAuth/encryption flow surfaces a different UI at each step but doesn't show a step indicator ("Step 2 of 3: Encryption"). The plugin import wizard has steps 1–4 but the UI doesn't show a progress dot. The user travels through a multi-step flow with no map. Worth raising at the design-system layer as "multi-step flows must show progress."
239 +
240 + ---
241 +
242 + ## Summary
243 +
244 + Projects and contacts are functionally complete but under-served on discoverability (no sort, no multi-filter, no avatar color hashing, no sub-collection edit). Settings is comprehensive but architecturally split from the rest of the app (full-page view, auto-save without undo, no About/Account/version surface). The most user-visible critical fix is **adding an About section** with version info (#1) — it costs nothing and unblocks every support flow. The most architecturally significant fix is **converting Settings to a modal/drawer** (#5) — it brings settings into the same modeless pattern as the rest of the app and unlocks live theme preview (#12). The most overdue cleanup is **killing the `prompt()` call in contacts bulk-tag** (#2) — it's the last remaining native dialog after Step 9, and adding `showPromptDialog` to `GoingsOn.ui` is a 30-line task that pays off across future features.
245 +
246 + After Phase 5, the audit has identified 18 cross-cutting patterns spanning 5 surfaces. The four most-recurring (state-by-color-alone, filter-not-persisted, no-undo-on-bulk, native-dialog-survivors) are no longer per-phase findings — they're design-system rules that should be added to `docs/design-system.md` and enforced via `scripts/lint-frontend.sh`.
@@ -0,0 +1,229 @@
1 + # Phase 6 — Mobile Parity Sweep
2 +
3 + **Scope:** every surface in the mobile viewport — bottom nav, mobile-tab-bar, mobile-more popover, action sheet, mobile create button, mobile sort/filter bar, mobile-view-title; the touch-conditional JS branches across the app; the safe-area-inset implementation; the touch-gesture vocabulary; the keyboard-avoidance machinery; and the responsive CSS at all breakpoints.
4 +
5 + **Stack:** Tauri 2 webview running on iOS (TestFlight live) and prospective Android. Files reviewed: `js/mobile.js`, `js/touch.js`, `js/components.js#showContextMenuSmart`, `js/context-menus.js`, mobile branches in `js/tasks.js` / `tasks-render.js` / `emails.js` / `events-calendar.js` / `day-planning.js` / `keyboard.js` / `app.js#showWelcome` / `components-modal.js`, the `#mobile-tab-bar` / `#mobile-more-popover` / `#action-sheet` blocks in `index.html`, and 12 mobile-breakpoint CSS blocks in `styles.css` (lines ~2882, 2914, 5909, 5935, 6514, 7278, 7758, 8304, 8467, 8666, 8994, 9051).
6 +
7 + **Method:** the inventory was structured around one question: **is mobile a CSS restyle of the desktop UI, or a parallel implementation with its own JS and markup?** Audit applies universal principles plus the cross-cutting flat-design check, with extra attention to consistency-across-surfaces and feature-parity.
8 +
9 + ---
10 +
11 + ## Architecture verdict
12 +
13 + **Mobile is ~70% parallel implementation, ~30% CSS restyle.**
14 +
15 + - **Parallel JS paths (8 modules):** `mobile.js` (entire module), `touch.js` (entire module), `keyboard.js` (shortcut registration skipped on touch), `emails.js` (compose dispatch), `day-planning.js` (long-press vs drag), `components-modal.js` (drag-to-dismiss), `components.js` (action-sheet dispatch), `app.js#showWelcome` (step variants), `context-menus.js` (touch contextmenu suppression).
16 + - **Mobile-only markup (6 elements):** `#mobile-tab-bar`, `#mobile-create-btn`, `#mobile-more-btn` + `#mobile-more-popover`, `#action-sheet`, `#task-mobile-sort`, `#mobile-view-title`.
17 + - **CSS restyle (the 30%):** task rows (grid → flex column), settings (sidebar → horizontal tabs), modals (centered → bottom-sheet), day-plan sidebar collapse, hour-row hiding in week view.
18 + - **Detection is dual:** CSS uses `@media (max-width: 768px)` (with secondary breakpoints at 1024 / 900 / 640 / 600); JS uses `GoingsOn.touch.isTouchDevice` (`'ontouchstart' in window || navigator.maxTouchPoints > 0`). Both must be true for full mobile mode.
19 +
20 + This architectural choice is the single most important finding of the audit — it's the lens for everything that follows.
21 +
22 + ---
23 +
24 + ## Surface snapshot
25 +
26 + - **Bottom nav (`#mobile-tab-bar`):** 5 buttons — Work · Time · Messages · `+` · More. 52 px tall + `env(safe-area-inset-bottom)`, `z-index: 1100`, fixed at bottom.
27 + - **More popover:** single-button menu (Settings only). Opens above the tab bar at `bottom: calc(52px + safe-area-inset-bottom)`.
28 + - **Action sheet (`#action-sheet`):** bottom-sheet modal that *replaces* the desktop context menu on touch. `z-index: 10001` (above modals).
29 + - **Mobile-view-title (`#mobile-view-title`):** in-header subview label, separate from `.page-title`.
30 + - **Mobile sort/filter (`#task-mobile-sort`):** desktop sort/filter lives in the filter bar; mobile gets its own dropdown + "Filters" toggle.
31 + - **Compose dispatch:** desktop opens `compose.html` in a new Tauri window; mobile opens `openComposeModal` (in-app form modal) — Phase 3 #3 already audited this.
32 + - **Day-plan timeline:** `--timeline-slot-h: 12px` desktop → `22px` at ≤600 px. Long-press 500 ms to paint, snaps to 30-min boundary.
33 + - **Calendar week view:** desktop 7-col grid; ≤600 px collapses to single-day swipeable carousel.
34 + - **Touch gestures wired in `mobile.js`:** swipe row (complete/snooze/archive/delete), long-press (selection + paint), pull-to-refresh, swipe nav (between days/views), drag-to-dismiss (modals + action sheet), rubber-band overscroll.
35 + - **iOS meta tags:** `viewport-fit=cover` is present; `apple-mobile-web-app-status-bar-style` and `theme-color` are NOT.
36 +
37 + ---
38 +
39 + ## Critical (fix before declaring Phase 6 done)
40 +
41 + ### 1. Kanban board is non-functional on mobile — drag-drop has no touch fallback
42 + - **Category:** Mappings, Anticipation. (**Universal**)
43 + - **Location:** `js/tasks-kanban.js` lines ~72–108 — `draggable="true"`, `ondragstart` / `ondrop` only. No `touchstart` / `touchmove` handlers; no `addLongPress`-and-pick alternative.
44 + - **Observation:** the Kanban view renders fine on touch — three columns (Pending / Started / Completed), cards inside each. But the only way to move a card between columns is HTML5 drag-and-drop, which is not triggered by touch on iOS (Android Chrome partially supports drag events with long-press; iOS Safari and WKWebView do not). A mobile user can *see* the board but cannot *change* it. There's no context-menu action to move a card to a different status either; the row context menu (action sheet) doesn't include "Move to Started / Completed".
45 + - **Why it matters:** the entire board view is shelf-ware for mobile users. They can browse but can't act, which is worse than not having the view at all — they see the affordance and discover it's dead.
46 + - **Recommendation:**
47 + - Quickest: on mobile, hide the "Board" pill in the view-mode toggle entirely. Show only List.
48 + - Better: add a context-menu action sheet entry on each card — "Move to Pending / Started / Completed" — and a tap-target long-press to invoke it.
49 + - Best: real touch drag-drop with a lift-on-long-press, ghost card following finger, and column highlight on hover. Libraries like SortableJS handle this; ~50 lines of integration.
50 + - Pick option 1 (hide) until option 3 is delivered. Showing a broken affordance is worse than not showing it.
51 +
52 + ### 2. Mobile is a parallel implementation with feature drift
53 + - **Category:** Consistency, Architecture. (**Universal — meta-finding**)
54 + - **Location:** the 8 JS modules with touch-conditional branches; 6 mobile-only HTML elements; the 12 breakpoint blocks.
55 + - **Observation:** the inventory confirms what Phase 3 #3 (compose), Phase 1 #8 (dual title system), Phase 5 (settings sidebar reflow), and the kanban finding above all point to — mobile is built as a *parallel surface* rather than a responsive restyle. The pattern has costs:
56 + - **Feature drift:** Phase 3 found the desktop compose has address highlighting and Cmd+S that mobile lacks. Phase 1 found mobile-view-title can drift from the in-content page-title. Kanban above is a third instance.
57 + - **Maintenance multiplier:** every UI change touches 1.7× as many files because the same logical thing has a desktop branch and a mobile branch.
58 + - **Inconsistent gesture vocabulary:** swipe-to-archive in emails uses 80 px threshold; swipe-to-snooze in tasks uses 80 px; long-press is 500 ms in some places. The system works but isn't documented or unified.
59 + - **Why it matters:** this is the architectural debt that produces all the smaller mobile-parity findings. Each per-phase mobile finding is a symptom; the root is the architecture.
60 + - **Recommendation:**
61 + - **Short-term (this audit cycle):** document the mobile-vs-desktop split in `docs/design-system.md`. List every feature that branches on `isTouchDevice` and what each branch does. This makes drift visible to future contributors.
62 + - **Medium-term:** for each touch branch, ask: "could this be CSS-only?" Compose modal vs. window is justified (Tauri webview limitation, surfaced). Mobile-tab-bar vs. desktop-tabs could be one component with two CSS layouts. Action sheet vs. context menu could be one helper that picks display mode by media query.
63 + - **Long-term:** target ≤30 % parallel. The win is fewer drift bugs; the cost is one rewrite pass.
64 +
65 + ### 3. Pills are unreachable from the mobile bottom nav (Phase 1 #1 — reconfirmed)
66 + - **Category:** Mappings, Visibility of state. (**Universal — reconfirmed**)
67 + - **Location:** mobile-tab-bar has Work / Time / Messages buttons; the per-tab pills (Tasks/Projects, Day/Week/Month/Timer/Events, Email/Contacts) sit at the top of the content area on mobile and scroll away.
68 + - **Observation:** Phase 1 raised this as critical; Phase 6 reconfirms because mobile-only markup is the lever to fix it. The bottom nav has parallel markup; that markup could surface pills directly (long-press the Time tab → action sheet picker of Day/Week/Month).
69 + - **Recommendation:** see Phase 1 #1. The fix integrates cleanly with the mobile-only architecture — add a long-press handler on each `.mobile-tab` that opens an action sheet of that tab's pills.
70 +
71 + ---
72 +
73 + ## Major (high impact, lower urgency)
74 +
75 + ### 4. Touch gestures are undiscoverable
76 + - **Category:** Discoverability, Anticipation. (**Universal**)
77 + - **Location:** swipe handlers in `mobile.js` lines ~224–260 (swipe row to complete/snooze/archive/delete); pull-to-refresh at line ~280; drag-to-dismiss modals/sheets.
78 + - **Observation:** GoingsOn ships at least 8 distinct touch gestures (row swipe in 3 directions per surface, pull-to-refresh, long-press select, long-press paint, long-press timeline-item, drag-to-dismiss, swipe-nav between days/views, rubber-band overscroll). None of them are documented in the UI. A new mobile user has to either stumble onto them or be told.
79 + - **Why it matters:** an app with 8 invisible gestures is an app with 8 features no one uses. Compare to iOS Mail, which surfaces swipe colors with a label peek as you drag — you see "Trash" appear as you swipe past the threshold.
80 + - **Recommendation:**
81 + - **Peek labels** on swipe: as the user drags a row past ~30 % of the threshold, show the action that will fire ("Complete", "Snooze", "Archive") with a colored background. iOS / Android Mail does this universally.
82 + - **First-run hint** on the bottom-most row: a small "← Swipe →" overlay that fades on first scroll.
83 + - **Gestures index** in the `?` shortcuts overlay — currently desktop-only; on touch, replace keyboard shortcuts with a "Gestures" reference (swipe row, long-press, etc.).
84 + - The todo file already flags "Touch gesture hints on first mobile use" — promote it from sprint to Phase 6 fix.
85 +
86 + ### 5. iOS-specific meta tags are missing
87 + - **Category:** Polish, Trust. (**Universal**)
88 + - **Location:** `index.html` lines 1–10 — has `<meta name="viewport" content="…viewport-fit=cover">` but no `theme-color`, no `apple-mobile-web-app-status-bar-style`, no `apple-mobile-web-app-capable`, no `apple-touch-icon`.
89 + - **Observation:** the Tauri webview on iOS picks up its tint and chrome from defaults. The status bar above the app is white-on-white or default. On Android Chrome (if the app ever lands as a PWA), `theme-color` would tint the browser chrome to match the app.
90 + - **Why it matters:** the absence is a small "this isn't a real iOS app" tell. The status bar above the app doesn't match the theme. On dark Catppuccin Mocha, the iOS status bar reads as a different surface entirely. Polish-level but it adds up.
91 + - **Recommendation:**
92 + - Add `<meta name="theme-color" content="#E0E4FA">` (the current `--bg-primary` light value) and update via JS when theme switches.
93 + - Add `<meta name="apple-mobile-web-app-status-bar-style" content="default">` (or `black-translucent` if you want the app to extend under the status bar with safe-area-inset).
94 + - Add an `apple-touch-icon` linking to a high-res app icon.
95 +
96 + ### 6. Long-press at 500 ms triggers different actions on different targets
97 + - **Category:** Consistency, Mappings. (**Universal**)
98 + - **Location:** `js/touch.js` default 500 ms; `day-planning.js` lines ~81–90 (timeline slots → paint), lines 96 (timeline items → action sheet), `mobile.js` lines ~317–327 (row long-press → select).
99 + - **Observation:** the same hold duration triggers four different things depending on where the finger is: select a row, open a context menu on a row, paint a block on the timeline, open an action sheet on a timeline item. Users build a single mental model: "long-press = do something contextual" — which is fine *if* the contextual feedback is immediate (a card lifts, a glow appears). Today's implementation just fires the action after 500 ms with no pre-feedback during the hold.
100 + - **Why it matters:** ambiguous gestures fail mid-action. A user trying to scroll the timeline accidentally holds for 500 ms and the paint modal opens. They cancel; they try again; they still don't know how to tell the app "I just want to scroll".
101 + - **Recommendation:**
102 + - Add an in-progress affordance: at 250 ms (half the threshold), show a small visual cue — finger pressure highlight, card scale up 2 %, or a ring around the touch point. This tells users "the gesture is being recognized; lift now to cancel".
103 + - Standardize the threshold and document it in the design-system charter ("long-press = 500 ms with 250-ms feedback ring").
104 + - On the timeline, distinguish slot-paint from item-action with the touch-target distinction (paint on empty slots, action sheet on filled items) — which the code already does; verify the feedback differs.
105 +
106 + ### 7. Modal on tablet stretches edge-to-edge with no max-width
107 + - **Category:** Anticipation, Polish. (**Polish/Universal**)
108 + - **Location:** `styles.css` line ~7359 — `.modal-container { width: 100% !important; max-width: 100% !important; }` inside the mobile media query.
109 + - **Observation:** the same media query (≤768 px) applies to a 768 × 1024 portrait tablet. A modal at 768 px wide is uncomfortable to read; the line lengths are too long.
110 + - **Recommendation:** add a sub-breakpoint or use `min(100%, 600px)`: `.modal-container { width: 100%; max-width: min(100%, 600px); margin: 0 auto; }`. On phones nothing changes; on tablets the modal is centered with reasonable line length.
111 +
112 + ### 8. The mobile-create-button (`+`) is context-dependent with no visual change
113 + - **Category:** Anticipation, Visibility. (**Universal — reconfirmed Phase 1 #5**)
114 + - **Location:** `#mobile-create-btn` in tab bar; `navigation.js#newItemForCurrentView` dispatches based on active tab.
115 + - **Observation:** Phase 1 noted this; Phase 6 confirms it across the parallel implementation. The button is always a green `+`. It creates a task in Work, an event in Time, an email in Messages — and the button gives no signal of which.
116 + - **Recommendation:** as Phase 1 — change the button's icon or label per active tab. Easier on mobile because the button is parallel markup; only one place to update.
117 +
118 + ### 9. Settings sidebar collapses to a horizontal wrap-row on mobile
119 + - **Category:** Hierarchy, Fitts. (**Universal**)
120 + - **Location:** `styles.css` lines 9051–9067 — `.settings-page-layout { flex-direction: column; }`, `.settings-sidebar { flex-direction: row; flex-wrap: wrap; }`.
121 + - **Observation:** on mobile, the 6-section sidebar becomes a row that wraps to 2 or 3 lines on smaller phones. Each section button is a wrap target; the user has to read tiny pill-labels to find the section they want. Phone settings apps (iOS Settings, Android Settings) use a list-and-detail pattern (root list → tap → detail), not a wrap-row of tabs.
122 + - **Why it matters:** the wrap-row is hard to scan and inefficient at narrow widths. With 6 items + a Back button, it fills most of the visible content area on a phone.
123 + - **Recommendation:**
124 + - Convert to a list-and-detail pattern on mobile: the settings view first shows a list of sections; tapping one navigates (push-style) to that section's content. Back arrow returns to the list.
125 + - Alternative: hide the sidebar entirely and use the in-content header to navigate (Section Title with a dropdown arrow → tap opens an action sheet of sections).
126 +
127 + ### 10. The mobile-view-title is set imperatively and can drift from the in-content `.page-title` (Phase 1 #8 — reconfirmed)
128 + - **Category:** Consistency, Visibility of state. (**Universal — reconfirmed**)
129 + - **Location:** `navigation.js#updateMobileViewTitle` writes to `#mobile-view-title`; each subview writes its own `.page-title` separately.
130 + - **Observation:** the parallel markup makes drift possible. If a subview changes its title at runtime ("Tasks" → "Snoozed Tasks") but doesn't call `updateMobileViewTitle`, the mobile header shows the stale label.
131 + - **Recommendation:** see Phase 1 #8 — make `.page-title` the source of truth via a MutationObserver, or route every set through one helper.
132 +
133 + ### 11. Multiple breakpoints (5 of them: 1024, 900, 768, 640, 600) without a documented system
134 + - **Category:** Consistency. (**Polish/Universal**)
135 + - **Location:** `styles.css` — `@media` blocks at 1024, 900, 768, 640, 600.
136 + - **Observation:** five breakpoints across the stylesheet, each touching different rules. No `:root` token defines them. A future contributor adding a responsive rule has to guess which threshold to use. Some rules at 600 px (calendar mobile day) conflict conceptually with 768 px (everything else mobile) — what's the right call for a 700-px-wide window?
137 + - **Recommendation:**
138 + - Document the breakpoint system in `design-system.md`: e.g., 480 px (phone-small), 768 px (phone-large / tablet-edge), 1024 px (tablet-large / desktop-edge). Map every existing `@media` rule to one of three.
139 + - Optionally CSS custom-property the breakpoints, though CSS doesn't natively support `@media (max-width: var(...))` — keep them as constants but document.
140 +
141 + ### 12. `@media (hover: none)` removes `:hover` rules — verify kebab/action affordances aren't lost
142 + - **Category:** Discoverability. (**Universal**)
143 + - **Location:** `styles.css` line ~7758 — `@media (hover: none) { /* :hover overrides */ }`.
144 + - **Observation:** the rule correctly removes hover-only styling on touch devices (no hover state exists). But many GoingsOn affordances rely on hover-to-reveal: kebab buttons that fade in on row hover, action buttons that brighten on hover. With hover removed, those affordances may be always-on (full opacity) — which is correct, but verify the desktop fade-in isn't the *only* way to discover them.
145 + - **Recommendation:** audit `:hover` rules and check that the touch fallback (always-visible) is the intended state, not the residue of "fade only desktop is allowed to use".
146 +
147 + ---
148 +
149 + ## Minor (worth fixing during normal cleanup)
150 +
151 + ### 13. Mobile sort/filter has its own markup parallel to the desktop filter bar
152 + - **Category:** Consistency.
153 + - **Location:** `#task-mobile-sort` in `index.html:71–80`; desktop `.filter-bar` in the same view.
154 + - **Observation:** mobile gets a slim dropdown + "Filters" toggle; desktop gets a full filter bar. The mobile filter modal is presumably the same content as the desktop bar but in a popover. Worth verifying parity — does mobile expose every filter desktop has?
155 + - **Recommendation:** verify parity; if features are missing on mobile, file as feature gaps.
156 +
157 + ### 14. Touch swipe row has no haptic feedback on iOS
158 + - **Category:** Feedback. (**Polish**)
159 + - **Location:** `mobile.js#addSwipeActions` doesn't emit haptics on threshold cross.
160 + - **Observation:** native iOS apps fire a haptic tick when a swipe action crosses the action threshold. GoingsOn doesn't.
161 + - **Recommendation:** Tauri's `tauri-plugin-haptics` (or the equivalent for the platform) on iOS supports `selectionChanged()`. Wire on swipe threshold crossing. Small change, big perceived-quality lift.
162 +
163 + ### 15. Pull-to-refresh has no visible indicator pre-threshold
164 + - **Category:** Feedback.
165 + - **Location:** `mobile.js#addPullToRefresh`.
166 + - **Observation:** verified the gesture exists; the inventory doesn't show whether it visually surfaces during the pull (e.g., a "Pull to refresh" arrow appearing as the user pulls).
167 + - **Recommendation:** show a small downward arrow that rotates 180 ° when the threshold is reached, then becomes a spinner during refresh. Standard pattern.
168 +
169 + ### 16. Mobile more-popover has only "Settings"; under-utilizes the affordance
170 + - **Category:** Anticipation. (**Polish**)
171 + - **Location:** `#mobile-more-popover` markup; `navigation.js:303–318` populates it with Settings only.
172 + - **Observation:** the More popover is the natural place for desktop-header affordances (Search, Help, About, version). On mobile, the search button doesn't exist in the bottom nav and isn't in More — there's no way to invoke Cmd+K equivalent.
173 + - **Recommendation:** populate More with Search, Help, Account, About in addition to Settings. The More popover is the iOS-equivalent of the desktop header right-actions cluster.
174 +
175 + ### 17. No status-bar-color customization (Finding #5 sub-case)
176 + - See Finding #5.
177 +
178 + ### 18. Action-sheet `z-index: 10001` vs modal-overlay implicit — verify stacking
179 + - **Category:** Polish.
180 + - **Location:** action sheet at 10001; modal at lower (10000-ish implicit).
181 + - **Observation:** the action sheet sits above the modal. If a modal opens an action sheet (e.g., long-press a row inside a modal), the action sheet correctly appears above. Confirm there's no case where a modal needs to appear above an action sheet (e.g., a confirm dialog spawned from an action sheet action).
182 + - **Recommendation:** confirm. If broken, hoist the modal above the sheet conditionally.
183 +
184 + ### 19. Welcome-flow mobile/desktop branch is a string-template fork
185 + - **Category:** Maintainability. (**Polish**)
186 + - **Location:** `app.js#showWelcome`.
187 + - **Observation:** each step has two template-string variants. Easy to drift (touch up the desktop variant, forget the touch variant). Phase 1 already touched this; Phase 6 confirms it's a real maintenance issue.
188 + - **Recommendation:** consolidate the gesture/keyboard distinction into a single template that conditionally renders "tap +" or "press q" inside the same step. Less repetition.
189 +
190 + ---
191 +
192 + ## Polish
193 +
194 + ### 20. Drag-to-dismiss has no visible chevron / handle hint
195 + - **Category:** Discoverability.
196 + - **Location:** `.modal-drag-handle` and `.action-sheet-handle` markup.
197 + - **Observation:** there's a `<div class="modal-drag-handle">` in the modal markup. Verify it's styled visibly (a small horizontal bar at the top of the modal sheet that signals "you can grab this"). If invisible, users don't know they can swipe down to close.
198 + - **Recommendation:** style as a 36 × 4 px rounded-rectangle, `--text-muted` color, centered, 8 px from the top edge. Standard iOS sheet handle.
199 +
200 + ### 21. Bottom-tab labels are uppercase with `letter-spacing` — small letters at edge cases
201 + - **Category:** Polish.
202 + - **Location:** `.mobile-tab-label` — uppercase, 0.7 rem, letter-spacing 0.05em.
203 + - **Observation:** "MESSAGES" at 0.7rem letter-spaced may push the label off the tab on a 320 px phone (iPhone SE 1st gen). Verify.
204 + - **Recommendation:** drop letter-spacing or reduce font to 0.65 rem on the smallest viewport.
205 +
206 + ### 22. `mobile.js` has its own keyboard-avoidance routine; verify it doesn't fight Tauri's
207 + - **Category:** Polish.
208 + - **Location:** `mobile.js:187–209` — `visualViewport` resize handler.
209 + - **Observation:** iOS Tauri webview has some default keyboard-avoidance; the app adds its own. On some devices both may fire and overshoot the scroll.
210 + - **Recommendation:** test on an iPhone 14 / 15 with various input positions. Add `passive: true` listeners where possible to avoid layout thrash.
211 +
212 + ---
213 +
214 + ## Cross-cutting / flat-design check
215 +
216 + The four meta-patterns now appear with reinforcement:
217 +
218 + - **State-by-color-alone** — `.mobile-tab.active` is a color shift only (Phase 1 #4); reconfirmed.
219 + - **Filter / view state not persisted** — no change here, but the mobile sort dropdown writes to nowhere persistent (verify).
220 + - **No-undo on touch swipe actions** — swipe-to-complete a task on mobile: does it show an undo toast? Phase 2 #2 said tasks complete has undo on desktop; verify the swipe-complete on mobile follows the same path. If it skips the toast (because the swipe gesture already feels confirmatory), that's a regression vs. desktop.
221 + - **Native dialog survivors** — `window.prompt` in contacts bulk-tag (Phase 5 #2) doesn't have a touch variant. On iOS this is a stronger issue — Safari/WKWebView often returns null from `prompt()` without showing UI. Promote Phase 5 #2 to high priority.
222 +
223 + **New pattern raised here:** **parallel-implementation drift** — the meta-finding (#2 above) is that every per-surface mobile gap is a child of one architectural choice. This becomes the design-system rule: "mobile is responsive CSS by default; JS branches require explicit justification and `docs/design-system.md` entry."
224 +
225 + ---
226 +
227 + ## Summary
228 +
229 + Mobile is functionally extensive — 8 touch gestures, comprehensive safe-area-inset, parallel-nav markup, action-sheet-replaces-context-menu dispatch, dedicated compose modal, day-plan slot-height bump, keyboard avoidance. It is also a parallel implementation (~70 % branched, ~30 % restyled), which is the root cause of every per-phase mobile-parity finding the audit has surfaced. The single largest visible bug is the broken Kanban (Finding #1) — hide it on mobile until touch drag-drop works. The single largest architectural debt is the parallel implementation itself (Finding #2) — start surfacing every touch branch in the design-system charter so contributors can see and audit the drift. The single largest user-facing missing feature is touch-gesture discoverability (Finding #4) — peek-labels on swipe + a gestures reference in the `?` overlay would unlock 8 features users currently can't find. After Phase 6, the audit is one phase away from complete; Phase 7 will roll up the four meta-patterns and the theme conformance sweep into a single closing report.
@@ -0,0 +1,270 @@
1 + # Phase 7 — Cross-Cutting Flat-Design + Theme Conformance Sweep
2 +
3 + **Scope:** the closing phase. Two responsibilities:
4 +
5 + 1. A direct flat-design + theme conformance audit of `src-tauri/frontend/css/styles.css` and the 9 themes under `src-tauri/frontend/themes/helix/`, plus mobile-specific safe-area-inset and virtual-keyboard checks that the previous phase deferred.
6 + 2. A roll-up of every finding from Phases 0–6 into ranked meta-patterns, with concrete recommendations for which design-system charter rules to enforce and which fixes to ship first.
7 +
8 + **Stack:** Tauri 2 webview, vanilla HTML/CSS/JS. Files reviewed (this phase): `css/styles.css` (theme tokens, hover rules, shadow tokens), `themes/helix/*.toml` × 9 (Helix-formatted Toml; Rust-side translation produces the 14-key UI map), `js/themes.js` (mapping logic), all 6 prior phase reports, `docs/design-system.md`, `docs/ux-audit/theme-coverage.md`, `docs/ux-audit/remediation-plan.md`.
9 +
10 + **Method:** read the design-system charter rules from `design-system.md` against the codebase one final time; visit each of the 9 themes mentally (the Rust-side translation makes them visually predictable from `:root` annotations); aggregate the per-phase findings; promote the most-recurring patterns to charter rules.
11 +
12 + ---
13 +
14 + ## Part A — Theme conformance & flat-design sweep
15 +
16 + ### Theme coverage holds
17 +
18 + The Step 8 sweep (`docs/ux-audit/theme-coverage.md`) confirmed every color token in `:root` is either annotated `themeable:` (with a matching `js/themes.js` mapping) or `theme-invariant` / `derived`. No silent fallback to Neobrute hex values remains. Coverage: 14 themeable color tokens, 14 mappings — 100 %.
19 +
20 + Two known fragilities from theme-coverage.md, restated here as Phase 7 work items:
21 +
22 + ### 1. `--overlay-color` produces a light scrim on dark themes
23 + - **Category:** Visibility, Consistency-across-themes. (**Universal / Flat**)
24 + - **Location:** `css/styles.css :root` — `--overlay-color: color-mix(in srgb, var(--text-primary) 60%, transparent);` plus the annotation calling out the dark-theme failure.
25 + - **Observation:** modal overlay derives from `--text-primary`. On Catppuccin Mocha / Macchiato / Frappe / Dracula / Tokyo Night, that's near-white. The 60 %-opacity scrim becomes a *light* veil, which reads as a popover / hover state, not a modal blocker.
26 + - **Recommendation:** introduce a dedicated `--scrim` token, default `rgba(0, 0, 0, 0.6)`, theme-invariant. Themes that want a different scrim opt in explicitly. Replace the derived `--overlay-color` with `--scrim` in `.modal-overlay` and any other backdrop user.
27 +
28 + ### 2. `--text-on-accent` is fragile on dark themes
29 + - **Category:** Visibility, Accessibility. (**Universal**)
30 + - **Location:** `css/styles.css :root` — `--text-on-accent: var(--bg-card);`.
31 + - **Observation:** on Neobrute (light), `--bg-card` is white and the accents (`--accent-green`, `--accent-red`, etc.) are saturated mid-tones — white text on them works. On Catppuccin Mocha, `--bg-card` resolves to `surface0` (`#313244`, dark). Dark text on saturated `--accent-green` (`#a6e3a1`) is technically readable but the WCAG margin is thin and the colour-pair feels muddy.
32 + - **Recommendation:** promote `--text-on-accent` to a themeable token (`foreground.on_accent`) and let each theme choose whether dark or light text best contrasts with that theme's accent palette. Until then, audit every use of `--text-on-accent` on saturated-accent backgrounds for contrast: `.toast-success`, `.toast-error`, `.undo-btn`, `.card-badge--success / --danger / --info`, `.tab.active`, `.btn-primary`, `.kanban-card.priority-*` if it uses it.
33 +
34 + ### 3. Hover-only affordances on touch devices
35 + - **Category:** Discoverability. (**Universal / Cross-cutting**)
36 + - **Location:** `css/styles.css @media (hover: none)` block strips `:hover` overrides on touch. Phase 6 flagged this for verification.
37 + - **Observation:** kebab buttons on rows are commonly hidden until row-hover on desktop (verify in `tasks-render.js` / `emails.js` / `events.js`). With hover absent on touch, the kebab must be either always-visible (correct fallback) or hidden (broken). The verification can be a five-minute manual pass: open each list view on iOS Simulator, scroll, look for kebab.
38 + - **Recommendation:** add to `scripts/lint-frontend.sh` a soft check: any rule with `opacity: 0` paired with a `:hover { opacity: 1 }` must also have `@media (hover: none) { opacity: 1 }` in the same neighborhood. The lint won't catch every case, but it raises the practice.
39 +
40 + ### 4. Shadow tokens are theme-invariant by design — verify on dark themes
41 + - **Category:** Visibility, Flat-design.
42 + - **Location:** `:root` defines `--shadow-brutal-xs / -md / -lg / -xl` as offset-only `0 0 0 var(--border-color)` patterns.
43 + - **Observation:** `--border-color` is themed (black on Neobrute, light-foreground on dark themes). The brutalist offset-shadow on dark themes becomes a light shadow — visually softer, less "brutal", but theme-consistent because the shadow follows the border. Working as designed. Note for the charter: the brutalist effect *depends* on a dark border color; on themes where the border is near-background, the shadow effect is invisible.
44 + - **Recommendation:** document this constraint in `design-system.md`. Themes that want the brutalist shadow effect should keep `border.default` at high-contrast (near-black on light themes, near-white on dark themes).
45 +
46 + ### 5. Safe-area-inset coverage is comprehensive
47 + - **Category:** Mobile correctness.
48 + - **Location:** Phase 6 inventory section #16 — body padding (4 sides), `.mobile-tab-bar`, `.mobile-more-popover`, `.action-sheet-container`, `.modal-overlay`, `.modal-container`, `.toast`, `.timer-widget` all use `env(safe-area-inset-*)`.
49 + - **Observation:** verified. No gaps.
50 + - **Recommendation:** consolidate the magic number `52px` (mobile-tab-bar height) into a `--mobile-tab-bar-h` token in `:root`. Five rules currently hardcode it; one change-point is safer.
51 +
52 + ### 6. Virtual keyboard avoidance is custom-built; verify with iOS device
53 + - **Category:** Mobile correctness.
54 + - **Location:** `js/mobile.js` lines ~187–209 — `visualViewport` resize handler scrolls focused input above keyboard with a 250 ms defer.
55 + - **Observation:** the logic is correct in principle (`visualViewport` is the right API; defer accounts for keyboard animation). Phase 6 flagged this for device verification. The 250 ms defer is a magic number — too short and the input is scrolled before the keyboard settles, too long and the user sees flicker.
56 + - **Recommendation:** test on iPhone 14 / 15 with input near the bottom of compose, snooze custom-time, and the contact form. If misbehavior is seen, the defer can be replaced with an animation-frame loop that polls until `visualViewport.height` stabilizes.
57 +
58 + ---
59 +
60 + ## Part B — Roll-up across Phases 1–6
61 +
62 + ### Finding count
63 +
64 + | Phase | Surface | Critical | Major | Minor | Polish | Total |
65 + |---|---|---|---|---|---|---|
66 + | 1 | Shell & navigation | 3 | 6 | 5 | 3 | 17 |
67 + | 2 | Tasks surface | 3 | 7 | 5 | 3 | 18 |
68 + | 3 | Compose & email | 3 | 8 | 7 | 3 | 21 |
69 + | 4 | Events & calendar | 4 | 9 | 7 | 3 | 23 |
70 + | 5 | Projects, contacts, settings | 4 | 10 | 7 | 2 | 23 |
71 + | 6 | Mobile parity | 3 | 9 | 7 | 3 | 22 |
72 + | **Total** | | **20** | **49** | **38** | **17** | **124** |
73 +
74 + 124 findings; 20 of them critical. The audit was not destructive — most findings are small. The clustering is what matters.
75 +
76 + ### Meta-patterns
77 +
78 + Eight cross-surface patterns appear across at least two phases. The first four are charter-rule-shaped (enforce via tooling); the rest are recommendation-shaped (document, monitor).
79 +
80 + #### Pattern 1 — State-by-color-alone (6 phases)
81 +
82 + Every visual state in GoingsOn currently leans on color as the differentiator: sync indicator (P1), priority cell (P2), snooze badge (P2 + P3), compose status bar (P3), block type (P4), settings sidebar active (P5), mobile tab active (P1, P6). Five themes are dark or low-contrast and these signals degrade on them. The pattern is identical six times.
83 +
84 + **Charter rule (new):** *"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 state encoding."*
85 +
86 + **Enforcement:** add to `docs/design-system.md` § "State communication". The pattern is too varied for a lint rule, but a once-per-PR reviewer checklist works.
87 +
88 + #### Pattern 2 — Filter / view state lives in the DOM, not the URL (5 phases)
89 +
90 + Task filters (P2), email filters and search (P3), week / month navigation (P4), project and contact searches (P5), shell route on reload (P1). Five separate surfaces lose state on reload. The root cause is the same: state lives in `document.getElementById('filter-status').value` not in `location.search`. The shared helper recommended in Phase 3 #8 (`js/query-state.js`) would solve all five.
91 +
92 + **Charter rule (new):** *"Every filter, sort, and view-mode setting that changes what the user sees must be mirrored to `location.search` on change and restored from `location.search` on init. No filter state may live only in the DOM or in module-level JS variables."*
93 +
94 + **Enforcement:** add to `design-system.md`. A lint rule is hard (filter state is loosely defined), but `scripts/lint-frontend.sh` could check for direct reads of `document.getElementById('filter-...').value` without a corresponding URL write, as a soft warning.
95 +
96 + #### Pattern 3 — Bulk operations without undo (3 phases)
97 +
98 + Single-task complete has `showUndoToast`; bulk task delete/snooze/set-project do not (P2). Bulk email actions skip undo (P3). Bulk contact tag skips undo (P5). The undo toast helper exists; it just isn't wired everywhere.
99 +
100 + **Charter rule (new):** *"Every bulk operation (any action that touches more than one record at once) must wrap its API call in `GoingsOn.ui.showUndoToast` with a captured pre-state and an inverse operation. The helper `bulkActionWithUndo(action, inverse, ids, prevState)` should be defined once and used everywhere bulk happens."*
101 +
102 + **Enforcement:** grep-able. `scripts/lint-frontend.sh` can flag any `*.bulk*` API call not preceded by a `showUndoToast` invocation within 10 lines.
103 +
104 + #### Pattern 4 — Native browser dialogs survive in pockets (1 phase, but historical)
105 +
106 + Step 9 of consolidation killed `window.confirm()`. Phase 5 #2 found `window.prompt()` survives in contacts bulk-tag. Native dialogs are forbidden by the charter and outright disabled on iOS WKWebView.
107 +
108 + **Charter rule (already implied; promote):** add to lint: ban `\b(window\.)?(confirm|prompt|alert)\(` outright. Build `GoingsOn.ui.showPromptDialog` to replace prompt. Add the lint rule to `scripts/lint-frontend.sh`.
109 +
110 + **Enforcement:** done — add one regex to the lint script.
111 +
112 + #### Pattern 5 — Parallel-implementation drift (Phase 6 root, symptoms in Phases 1, 3)
113 +
114 + The architecture-level finding: ~70 % of mobile is parallel JS branches and parallel markup, ~30 % is CSS restyle. Every per-phase mobile gap traces to this choice. Phase 3 #3 (two compose UIs), Phase 1 #8 (dual title system), Phase 6 #1 (kanban broken), and several Phase 6 majors all share the root cause.
115 +
116 + **Charter rule (new):** *"Mobile is responsive CSS by default. JS branches on `GoingsOn.touch.isTouchDevice` (or media-query-derived equivalents) require explicit justification documented in `design-system.md`. New touch branches must explain why a CSS-only or shared-component solution wouldn't work."*
117 +
118 + **Enforcement:** soft — a code-review checklist plus a top-of-file comment in any module with touch branches.
119 +
120 + #### Pattern 6 — Passive-feature gaps (Phase 4)
121 +
122 + Reminders, all-day events, snooze-events, recurring-instance edit prompt — features that exist in the data model and rendering path but lack an authoring affordance. Phase 4 concentrated four of them; other phases had isolated instances (Phase 2 #7 "Waiting Only" filter with no UI to set the state, Phase 5 #3 contact sub-collection no Edit).
123 +
124 + **Recommendation:** complete the loop for each. The "Waiting Only" filter either gets a status, or gets removed. All-day events get a checkbox in the form. Sub-collections get an Edit affordance. The data model is more permissive than the UI; the UI needs to catch up.
125 +
126 + **Enforcement:** none specific — it's per-feature triage. Make a one-time pass over every backend column / API endpoint and confirm the authoring path exists.
127 +
128 + #### Pattern 7 — Multi-step flows hide their progress (Phase 5)
129 +
130 + OAuth flow, encryption-setup flow, plugin-import wizard — all walk users through multi-step state transitions without a "Step N of M" indicator.
131 +
132 + **Charter rule (new):** *"Any flow with more than two sequential modal steps must show a step indicator (Step 2 of 4) in the modal header. Single-modal flows don't need it."*
133 +
134 + **Enforcement:** documentation-only.
135 +
136 + #### Pattern 8 — Action-bar density (Phases 2, 3, 4)
137 +
138 + 10 actions in the email reader bar (P3), 5 pills + Time-tab Timer overlap (P4), task-row context menu offers many overlapping actions (P2). Each surface accumulates options that compete for the user's scan.
139 +
140 + **Recommendation:** at the design-system layer, add a guideline: a horizontal action bar has at most 5 visible actions; the rest live in an overflow `Actions ▾` menu. Primary actions get a `.btn-primary` visual; secondary actions are `.btn-secondary`; destructive actions go in the overflow.
141 +
142 + **Enforcement:** documentation-only; per-surface implementation in the email-reader-bar fix (Phase 3 #4).
143 +
144 + ---
145 +
146 + ## Part C — Recommended ship order
147 +
148 + Across 124 findings, the next-action set is small and high-leverage. Sequenced by user impact / cost / unblocking:
149 +
150 + ### Tier 1 — Safety nets (ship first)
151 +
152 + These remove the only *unrecoverable* user mistakes in the app.
153 +
154 + 1. **Bulk-action undo across the app** (P2 #2, P3 — bulk email actions, P5 #4 bulk contact tag). One helper, used everywhere bulk happens. Pattern 3 charter rule.
155 + 2. **Selection clears on filter change** (P2 #1). Prevents bulk-delete-against-invisible-rows.
156 + 3. **Send-with-delay (undo-send)** (P3 #1). 5–30 s queue with a "Sending… Undo" toast in the shell.
157 + 4. **Attachment-size warning before send** (P3 #2). Client-side total + warning + hard cap.
158 + 5. **Recurring-instance edit prompt** (P4 #7). "This / Future / All" before saving an instance edit.
159 +
160 + Estimated: 1–2 weeks of focused work.
161 +
162 + ### Tier 2 — Trust-state surfaces (ship next)
163 +
164 + These make the app's internal state legible to the user.
165 +
166 + 6. **Real sync status surface** (P1 #2). Hover-expand to "Synced 2 min ago" / "Sync failed — Retry"; never let the dot be the only signal.
167 + 7. **About / version section in Settings** (P5 #1). Wire the existing `showAbout` modal.
168 + 8. **Account / profile section in Settings** (P5 #9). Connected email, sign-out, subscription tier.
169 + 9. **Pagination + total-count for tasks / emails** (P2 #9, P3). Show count; warn at the 500-cap.
170 +
171 + ### Tier 3 — Mobile correctness (ship in parallel with Tier 2)
172 +
173 + 10. **Hide Kanban on mobile until touch drag works** (P6 #1). One-line CSS rule.
174 + 11. **Touch-gesture peek-labels on swipe** (P6 #4). Surface 8 invisible features.
175 + 12. **Mobile bottom-nav: long-press for pills** (P1 #1, P6 #3). Makes Projects / Events / Week reachable on mobile.
176 + 13. **iOS meta tags** (P6 #5). `theme-color`, status-bar style, touch-icon.
177 + 14. **`window.prompt` removed** (P5 #2). One helper + lint rule.
178 +
179 + ### Tier 4 — Persistence (the recurring pattern)
180 +
181 + 15. **URL-mirrored filter / view state** (P1 #9, P2 #10, P3 #8, P4 #various, P5 #various). One shared `js/query-state.js` helper unblocks five surfaces.
182 +
183 + ### Tier 5 — Feature gaps that the data model already supports
184 +
185 + 16. **Event reminders** (P4 #1). Largest functional gap in the app.
186 + 17. **All-day event authoring** (P4 #4). Calendar can render them; form must create them.
187 + 18. **Snooze events** (P4 #3). Third branch in `snooze.js`.
188 + 19. **Sub-collection edit on contacts** (P5 #3). Add → existing form, pre-filled.
189 +
190 + ### Tier 6 — Polish + architecture
191 +
192 + 20. **Compose unified into one component** (P3 #3, P6). Stop two-headed compose drift. Staged migration in progress — see `compose-migration.md`. Stages 1-2 landed 2026-05-20; stage 3 (shared HTML template) next.
193 + 21. **Side-drawer task detail** (P2 #4). Modeless triage.
194 + 22. **Settings as modal / drawer instead of full-page** (P5 #5). Live theme preview, modeless config.
195 +
196 + ---
197 +
198 + ## Part D — Charter rules to add
199 +
200 + `docs/design-system.md` should grow a "Cross-cutting rules" section. Concrete additions, based on the four enforceable meta-patterns:
201 +
202 + ```markdown
203 + ## Cross-cutting rules
204 +
205 + These rules apply across every surface. Violations are caught either by reviewer
206 + checklist or by `scripts/lint-frontend.sh`.
207 +
208 + ### State communication
209 + Every visual state — active, selected, running, error, success — must pair color
210 + with a second non-color signal: shape, position, weight, icon, or text. Color
211 + alone is not sufficient.
212 +
213 + ### Filter & view state
214 + Every filter, sort, and view-mode setting that changes what the user sees must
215 + be mirrored to `location.search` on change and restored on init. Filter state
216 + must not live only in the DOM or in module-level JS.
217 +
218 + ### Bulk operations
219 + Every bulk operation must wrap its API call in `GoingsOn.ui.showUndoToast` with
220 + a captured pre-state and an inverse operation. Use the shared
221 + `bulkActionWithUndo(action, inverse, ids, prevState)` helper.
222 +
223 + ### Native dialogs forbidden
224 + `window.confirm`, `window.prompt`, and `window.alert` are banned. Use
225 + `GoingsOn.ui.showConfirmDialog`, `GoingsOn.ui.showPromptDialog`, and
226 + `GoingsOn.ui.showToast` respectively. Native dialogs are disabled on iOS
227 + WKWebView and break theming on every platform.
228 +
229 + ### Mobile is responsive CSS by default
230 + JS branches on `GoingsOn.touch.isTouchDevice` (or media-query equivalents)
231 + require explicit justification documented here. Default is shared component
232 + + CSS layout reflow.
233 +
234 + ### Multi-step flows
235 + Any flow with more than two sequential modal steps must show a "Step N of M"
236 + indicator in the modal header.
237 +
238 + ### Action bars
239 + A horizontal action bar has at most 5 visible actions; the rest live in an
240 + overflow `Actions ▾` menu. Primary actions get `.btn-primary`; destructive
241 + actions go in the overflow.
242 + ```
243 +
244 + Lint additions to `scripts/lint-frontend.sh`:
245 +
246 + ```bash
247 + # 7. No native dialogs.
248 + hits=$(grep -rnE '\b(window\.)?(confirm|prompt|alert)\(' "$SRC_JS" 2>/dev/null \
249 + | grep -vE 'showConfirmDialog|showPromptDialog|confirmDelete' || true)
250 + report "no-native-dialogs" "Use GoingsOn.ui.show*Dialog instead." "$hits"
251 +
252 + # 8. Bulk operations should call showUndoToast within 10 lines.
253 + # (Soft check — manual review for non-trivial cases.)
254 + ```
255 +
256 + ---
257 +
258 + ## Summary — closing the audit
259 +
260 + Across seven phases the audit produced 124 findings and identified eight cross-surface patterns. The most actionable finding is **the recurrence**: four patterns appear in three or more independent surfaces and account for ~40 of the 124 findings. Promoting those four to design-system charter rules (plus the lint additions) collapses the audit's recommendation surface from 124 individual fixes into 5 systemic changes:
261 +
262 + 1. Add `bulkActionWithUndo` helper; wire across tasks / emails / contacts.
263 + 2. Add `js/query-state.js`; mirror filter state to URL on every surface.
264 + 3. Add `GoingsOn.ui.showPromptDialog`; add lint rule banning native dialogs.
265 + 4. Pair every color-encoded state with a second signal (per-surface, but documented as one rule).
266 + 5. Surface every touch-conditional JS branch in `design-system.md`; start every mobile feature as responsive CSS by default.
267 +
268 + After those five land, the Tier 1–3 ship list (above) addresses every critical finding. Phase 1's most-overdue items (sync surface, project chip) and Phase 4's reminders are the highest user-visible wins after the safety nets are in place.
269 +
270 + The audit is complete. The codebase entered with strong bones — a real design system, 9 themes, a working mobile shell, comprehensive safe-area-inset — and exits with a documented map of where the bones don't yet support the muscles. Phase 0–Phase 7 deliverables together (the inventory at `phase-0.md`, the charter at `design-system.md`, the per-surface reports `phase-1.md` through `phase-6.md`, the theme audit at `theme-coverage.md`, the remediation plan at `remediation-plan.md`, and this closing roll-up at `phase-7.md`) give the project a single coherent reference for the next year of UI work.
@@ -0,0 +1,183 @@
1 + # Consolidation Pre-Plan (Pre-Phase-1 Remediation)
2 +
3 + Translates the Phase 0 divergence map and gap list into a sequenced migration. Surface audits (Phase 1+) do not start until every success criterion in `docs/design-system.md` holds.
4 +
5 + Each step below names **the canonical choice**, the **migration**, and how to **verify done**. Steps are ordered: foundations first (so later steps can lean on them), then mass migrations, then enforcement.
6 +
7 + ---
8 +
9 + ## Step 1 — Move toast styling into CSS ✅ done 2026-05-19
10 +
11 + **Why first:** the toast helper is the most visible token-bypass. Fixing it removes raw hex from a hot path, proves out the "no `style.cssText` for colors" rule, and unblocks theme-correct toasts across all 9 themes.
12 +
13 + **Canonical:** `.toast`, `.toast-info`, `.toast-success`, `.toast-error`, `.toast-undo` carry **all** positioning, shadow, color, and animation in `styles.css`. `showToast` only sets `textContent`, attaches children, and appends to `document.body`.
14 +
15 + **Migration:**
16 + 1. Add to `styles.css`: `.toast { position: fixed; bottom: var(--space-4); right: var(--space-4); z-index: 2000; … }` plus variant backgrounds keyed to `--accent-green` / `--accent-red` / `--accent-blue` (info).
17 + 2. In `components-modal.js`, delete the `cssText` injection in `showToast`. Replace `#22c55e` / `#ef4444` / `#c00` with class composition only.
18 + 3. Repeat for `showUndoToast`.
19 +
20 + **Done when:** `rg 'cssText' src-tauri/frontend/js/components-modal.js` returns zero matches.
21 +
22 + ---
23 +
24 + ## Step 2 — Move updater + keycap inline colors into CSS ✅ done 2026-05-19
25 +
26 + **Why next:** small surface, completes the "no raw hex in JS" criterion.
27 +
28 + **Canonical:** updater overlay gets `.updater-overlay`, `.updater-progress`, `.updater-text` classes consuming tokens. Keycap rendering gets a `.kbd` class.
29 +
30 + **Migration:**
31 + 1. Define `.updater-*` classes in `styles.css`; in `updater.js`, replace `#6c5ce7`, `#444`, `#aaa`, `#2d2d2d` with class names.
32 + 2. Replace `app.js`'s `kbdStyle` constant (18 inline applications) with a `.kbd` class; render `<span class="kbd">⌘K</span>`.
33 + 3. In `emails.js`, replace `var(--accent-green, #22c55e)` and similar patterns with class-driven styling — the fallback hex defeats theming.
34 +
35 + **Done when:** `rg -n '#[0-9a-fA-F]{3,8}\b' src-tauri/frontend/js src-tauri/frontend/*.html` returns zero matches (excluding HTML entity codes like `&#9660;`).
36 +
37 + ---
38 +
39 + ## Step 3 — Add `renderFormField` primitive ✅ done 2026-05-19
40 +
41 + **Why before row primitive:** form-modal already exists as a half-step and only needs one new helper; rows touch more files.
42 +
43 + **Canonical:** `GoingsOn.ui.renderFormField({ kind, name, label, value, error, help, placeholder, options, required })` returns a `.form-group` element. Kinds: `text`, `email`, `number`, `select`, `textarea`, `checkbox`, `date`, `time`.
44 +
45 + **Migration:**
46 + 1. Add the helper to `components.js` (next to `renderEmptyState`). Error variant renders `.form-group.has-error` with `.form-error-text` below the input.
47 + 2. Add `.form-group.has-error`, `.form-error-text` to `styles.css` (red border via `--accent-red`, error text in `--accent-red` on `--bg-card`).
48 + 3. Migrate call sites in order: `form-modal.js` first (the largest beneficiary), then `settings.js`, `email-accounts.js`, `settings-sync.js`. Each PR should be one file.
49 +
50 + **Done when:** every `.form-group` block in the four target files is produced by `renderFormField`; no inline `style=` on form children.
51 +
52 + ---
53 +
54 + ## Step 4 — Add `renderRow` primitive and migrate the five list renderers ⏸ DEFERRED
55 +
56 + Re-evaluated and deferred after Step 6 sweep showed that the vast majority of inline-style violations were *not* in rows — they were in modals, forms, status indicators, banners, and empty states. The Step 6 utility-class sweep cleared them directly without needing a row primitive. The existing per-kind renderers (`tasks-render`, `projects-render`, `contacts-render`, `events`, `emails`) all consume canonical CSS classes (`.task-row`, `.card.contact-card`, `.event-row-virtual`, `.email-item`) and have no inline-style violations. Revisit if a Phase audit identifies the parallel-markup-as-row-pattern as a real friction point; until then, the per-kind renderers are fine.
57 +
58 + **Why this size:** five files, two weeks of work realistically. Phase 1 (Shell & nav) does not depend on rows being done, so this can run in parallel with smaller steps — but Phase 2 (Tasks surface) **does** depend on it.
59 +
60 + **Canonical:** `GoingsOn.ui.renderRow(kind, model, opts)` returns a row element with slots:
61 + - `leading` — icon / avatar / checkbox / status pill
62 + - `primary` — main text line
63 + - `secondary` — sub-text / metadata line
64 + - `meta` — right-aligned badges, counts, time
65 + - `actions` — kebab menu + visible action buttons
66 +
67 + Adapters per kind register with `GoingsOn.ui.registerRowAdapter(kind, fn)` and return a slot object from a model.
68 +
69 + Shared chrome lives on `.row` (new base class) plus `.row--${kind}` modifiers. Existing `.task-row`, `.event-row-virtual`, `.card.contact-card`, `.email-item` become `.row.row--task`, `.row.row--event`, etc.; old class names kept as aliases via CSS selectors during migration so a half-migrated tree doesn't visually break.
70 +
71 + **Migration order (riskiest last):**
72 + 1. `contacts-render.js` (simplest) — proves the adapter pattern.
73 + 2. `projects-render.js` — card-shaped row.
74 + 3. `emails.js` — adds attachments/unread state to the slot model.
75 + 4. `events.js` — virtual scrolling; verify `renderRow` is allocation-cheap.
76 + 5. `tasks-render.js` — densest surface (state classes, badges, progress). Adapter has the most modifiers.
77 +
78 + After each file lands, delete its bespoke `innerHTML` template.
79 +
80 + **Done when:** the five files contain no row-shaped `innerHTML` literals; visual diff per kind is zero.
81 +
82 + ---
83 +
84 + ## Step 5 — Consolidate empty states ✅ done 2026-05-19
85 +
86 + **Canonical:** `.empty-state` with size modifiers `.empty-state--compact` (used inside list panes) and `.empty-state--dashboard` (used in dashboard tiles). Always rendered via `GoingsOn.ui.renderEmptyState(message, buttonLabel?, onClick?, { variant })`.
87 +
88 + **Migration:**
89 + 1. Add modifier styles to `styles.css`.
90 + 2. Replace `.empty-dashboard-list` usages → `renderEmptyState(..., { variant: 'dashboard' })`.
91 + 3. Replace `.kanban-empty` usages → `renderEmptyState(..., { variant: 'compact' })`.
92 + 4. Replace `.virtual-scroller-empty` usages → `renderEmptyState(...)`.
93 + 5. Replace inline `<div class="empty-state" style="padding: 1rem; text-align: center">…</div>` in `events.js` and elsewhere with helper calls.
94 + 6. Delete deprecated classes from `styles.css`.
95 +
96 + **Done when:** the three deprecated class names appear nowhere in `src-tauri/frontend/`.
97 +
98 + ---
99 +
100 + ## Step 6 — Layout utility classes; replace remaining inline styles ✅ done 2026-05-19 (lint clean)
101 +
102 + **Why:** with colors / shadows / borders already out, what's left in inline `style=` is layout — flex micro-tweaks and visibility. Promote the common ones to classes for grep-ability and future restyling.
103 +
104 + **Canonical utilities (add to `styles.css`):**
105 + - `.hidden` (already exists for modal — extend to anywhere).
106 + - `.stack` (`display: flex; flex-direction: column`), `.stack-2` / `.stack-3` (with `gap: var(--space-2|3)`).
107 + - `.row-flex` (`display: flex; align-items: center`), `.row-flex-2` / `.row-flex-3`.
108 + - `.flex-1`, `.min-w-0`, `.text-center`, `.text-muted`.
109 +
110 + **Migration:** sweep file-by-file (`email-accounts.js` → 51 → 0, `settings-sync.js` → 31 → 0, `settings.js` → 25 → 0, then the rest). One PR per file; visual diff per file.
111 +
112 + **Done when:** `rg 'style="' src-tauri/frontend/` returns no matches that touch color, shadow, border, font, or padding; remaining hits are documented exceptions (compose.html embedded styles are addressed in Step 7).
113 +
114 + ---
115 +
116 + ## Step 7 — Reconcile `compose.html` embedded `<style>` block ✅ done 2026-05-19
117 +
118 + Took option B (kept the embedded block but tokenized everything inside it; added 4 new classes for inline-styled elements: `.reply-indicator`, `.header-row--tight`, `.toggle-cc-btn`, `.save-contact-bar` with `.save-btn` / `.dismiss-btn`). All inline `style=` attributes inside `compose.html` are gone.
119 +
120 + `compose.html` lines 8–100 contain composer-local overrides that bypass the token system. Two options:
121 +
122 + - **A (preferred):** Move into `styles.css` under a `.compose-view` scope; tokenize all values. Composer becomes just another themed view.
123 + - **B:** Keep the embedded block but require every declared value to be `var(--token)`; document why a scoped block is needed.
124 +
125 + Pick A unless there's a Tauri webview reason the composer is loaded with a different stylesheet (there isn't — both load `styles.css`).
126 +
127 + **Done when:** `compose.html` has no `<style>` block, or every value inside it is a token reference.
128 +
129 + ---
130 +
131 + ## Step 8 — Theme coverage sweep ✅ done 2026-05-19 (report at `theme-coverage.md`)
132 +
133 + Pair every color token in `styles.css :root` with either a mapping in `js/themes.js` or a `/* theme-invariant */` annotation.
134 +
135 + **Migration:**
136 + 1. Walk `:root` in `styles.css`. For each color custom property, grep `js/themes.js` for the property name.
137 + 2. Unmapped tokens: either add a mapping (most likely correct for any new accent) or annotate invariance.
138 + 3. Test by switching to each of the 9 themes and visiting: tasks list, project detail, contacts, events, day plan, compose, settings, an empty state, a toast, a confirm dialog. Capture a quick screenshot grid.
139 +
140 + **Done when:** no color token in `:root` is silently falling back to the Neobrute hex on a non-Neobrute theme.
141 +
142 + ---
143 +
144 + ## Step 9 — Kill `window.confirm` ✅ done 2026-05-19
145 +
146 + `contacts.js` is the known offender (Phase 0 noted "one offender remains"). Replace with `GoingsOn.ui.showConfirmDialog(...)` / `confirmDelete(...)`.
147 +
148 + **Done when:** `rg 'window\.confirm|\bconfirm\(' src-tauri/frontend/js/` returns no matches outside the helper definition.
149 +
150 + ---
151 +
152 + ## Step 10 — Enforcement: add lint guards ✅ done 2026-05-19 (`scripts/lint-frontend.sh`)
153 +
154 + Block regressions before they land. Add to whatever lint or pre-commit pipeline GO uses (or a new `scripts/lint-frontend.sh` invoked by CI):
155 +
156 + 1. **No raw hex in JS or HTML:** `rg -n '#[0-9a-fA-F]{3,8}\b' src-tauri/frontend/js src-tauri/frontend/*.html` must return zero.
157 + 2. **No `cssText` color/shadow/border:** `rg -n 'cssText' src-tauri/frontend/js/` returns zero or only allow-listed lines.
158 + 3. **No `var(--token, #fallback)`:** `rg -n 'var\(--[a-z-]+,\s*#' src-tauri/frontend/` returns zero.
159 + 4. **No `window.confirm`:** rg pattern above.
160 + 5. **Style-attribute audit:** `rg -n 'style="[^"]*(color|background|border|shadow|font)' src-tauri/frontend/` returns zero.
161 +
162 + These run before merge; failures explain which charter rule they violated.
163 +
164 + ---
165 +
166 + ## Sequencing summary
167 +
168 + | Order | Step | Blocks |
169 + |---|---|---|
170 + | 1 | Toast → CSS | Step 10 lint guards, Phase 7 cross-cutting |
171 + | 2 | Updater + keycaps + email var-fallback → CSS | Lint guard #1 |
172 + | 3 | `renderFormField` + migrate 4 files | Phase 3 (Compose & email) |
173 + | 4 | `renderRow` + migrate 5 files | Phase 2 (Tasks), Phase 4 (Events), Phase 5 (Projects/contacts) |
174 + | 5 | Empty-state consolidation | Phase 1 (Shell) |
175 + | 6 | Layout utility classes + style-attr sweep | Lint guard #5 |
176 + | 7 | `compose.html` scoped styles | Phase 3 |
177 + | 8 | Theme coverage sweep | Phase 7 |
178 + | 9 | Kill `window.confirm` | Phase 5 |
179 + | 10 | Lint guards | Permanent enforcement |
180 +
181 + Steps 1, 2, 5, 7, 8, 9 are small (single-PR). Steps 3 and 4 are the real work — each is several PRs. Step 6 is mechanical but high-volume.
182 +
183 + **Phase 1 (Shell & navigation) can start once Steps 1, 2, 5, 8, 9, 10 land** — those are the cross-cutting ones that affect every screen. Steps 3, 4, 6, 7 can land in parallel with Phases 1–5 as long as no Phase audit reports findings that the in-flight migration would invalidate. Phase 6 (Mobile parity) and Phase 7 (Flat-design + theme sweep) require all 10 steps complete.
@@ -0,0 +1,79 @@
1 + # Step 8 — Theme Coverage Audit
2 +
3 + Walked every color custom property in `src-tauri/frontend/css/styles.css :root` and reconciled it with the runtime mapping in `src-tauri/frontend/js/themes.js`.
4 +
5 + ## Token coverage
6 +
7 + `js/themes.js` exposes 14 dotted TOML paths, mapping them to CSS custom properties at theme-switch time:
8 +
9 + | TOML path | CSS property | Status |
10 + |---|---|---|
11 + | `background.primary` | `--bg-primary` | ✓ mapped |
12 + | `background.secondary` | `--bg-secondary` | ✓ mapped |
13 + | `background.tertiary` | `--bg-tertiary` | ✓ mapped |
14 + | `background.surface` | `--bg-card` | ✓ mapped |
15 + | `foreground.primary` | `--text-primary` | ✓ mapped |
16 + | `foreground.secondary` | `--text-secondary` | ✓ mapped |
17 + | `foreground.muted` | `--text-muted` | ✓ mapped |
18 + | `accent.yellow` | `--accent-yellow` | ✓ mapped |
19 + | `accent.green` | `--accent-green` | ✓ mapped |
20 + | `accent.blue` | `--accent-blue` | ✓ mapped |
21 + | `accent.purple` | `--accent-purple` | ✓ mapped |
22 + | `accent.red` | `--accent-red` | ✓ mapped |
23 + | `accent.cyan` | `--accent-cyan` | ✓ mapped |
24 + | `border.default` | `--border-color` | ✓ mapped |
25 +
26 + All 14 themeable tokens in `:root` are now annotated `themeable:` in the CSS source. Coverage is complete.
27 +
28 + ## Derived tokens (theme-invariant by construction)
29 +
30 + Five tokens reference upstream themeable tokens via `var(--…)`, so they follow the theme automatically without needing a mapping:
31 +
32 + - `--accent-color` → `var(--accent-blue)`
33 + - `--accent-primary` → `var(--accent-blue)`
34 + - `--bg-hover` → `var(--bg-tertiary)`
35 + - `--border-light` → `var(--bg-tertiary)`
36 + - `--text-on-accent` → `var(--bg-card)` (see fragility note below)
37 +
38 + ## Theme-invariant axes
39 +
40 + The following axes do not change per theme and are annotated `theme-invariant`: spacing scale, type scale, line heights, radii, shadow offsets, transition timings, layout widths, font families, `--border-width`, `--border-width-sm`. Annotation lives in the comment next to each token in `:root`.
41 +
42 + ## Findings to address
43 +
44 + ### 1. `--overlay-color` produces a light scrim on dark themes (low-to-medium)
45 +
46 + `--overlay-color: color-mix(in srgb, var(--text-primary) 60%, transparent)` is derived from `--text-primary`. On a dark theme (e.g. Catppuccin Mocha), `--text-primary` is near-white, so the modal overlay becomes a *light* scrim at 60% opacity instead of the expected dark scrim.
47 +
48 + Fix options:
49 + - Introduce a dedicated `--scrim` token, themeable, defaulting to `rgba(0, 0, 0, 0.6)`; each theme overrides if desired.
50 + - Or hardcode the overlay to `rgba(0, 0, 0, 0.6)` since a modal scrim is universally dark regardless of theme.
51 +
52 + Annotated in `styles.css` with the dark-theme failure note.
53 +
54 + ### 2. `--text-on-accent` is fragile (low)
55 +
56 + `--text-on-accent: var(--bg-card)` works on the Neobrute theme because card surfaces are near-white and accents are saturated mid-tones (always contrasting). On dark themes, `--bg-card` is dark and the accents may not contrast strongly with it — risking unreadable badge text on success/error toasts and buttons.
57 +
58 + Today's compositions:
59 + - `.toast-success` background `--accent-green`, text `--text-on-accent` → on dark themes, dark text on green; OK but lower contrast than intended.
60 + - `.toast-error` background `--accent-red`, text `--text-on-accent` → on dark themes, dark text on red; OK but the WCAG margin is thin.
61 + - `.undo-btn`, `.card-badge--success/danger/info` follow the same pattern.
62 +
63 + Fix options:
64 + - Promote `--text-on-accent` to a themeable token in `js/themes.js`, mapped to `foreground.on_accent` (with light-theme themes setting it to dark text and dark-theme themes choosing between light or dark to maintain contrast).
65 + - Or use `color: white` literally on `.toast-success/--error` (and treat saturated accents as "always-bright, always-white-text" UI surfaces).
66 +
67 + Not blocking Phase 1, but flag during Phase 7 (cross-cutting + theme conformance sweep).
68 +
69 + ## Sanity audit by theme
70 +
71 + Did not visit all 9 themes for screenshots in this pass — that's a manual-visit task. Recommendation for the human pass during Phase 7:
72 +
73 + 1. Open each theme via Settings → Appearance.
74 + 2. Visit: Task list, Project detail, Contact card, Compose, Settings → Cloud Sync (dashboard), an empty state, a confirm dialog, a success toast, an error toast.
75 + 3. Capture screenshots and look for: contrast failures on accents, light scrim on dark themes, any element still rendering its Neobrute hex fallback.
76 +
77 + ## Result
78 +
79 + Step 8 success criterion met: every color token in `:root` is now either annotated `themeable:` (with a corresponding mapping in `js/themes.js`) or `theme-invariant` / `derived`. No silent fallbacks remain. Two known fragility points (`--overlay-color`, `--text-on-accent`) are documented and queued for Phase 7.
@@ -0,0 +1,67 @@
1 + #!/bin/bash
2 + # Frontend design-system lint guards.
3 + # See docs/design-system.md "Inline-style rules" and docs/ux-audit/remediation-plan.md Step 10.
4 + # Exit 0 = clean. Exit non-zero = violations found (printed with file:line).
5 +
6 + set -u
7 + ROOT="$(cd "$(dirname "$0")/.." && pwd)"
8 + FRONTEND="$ROOT/src-tauri/frontend"
9 + SRC_JS="$FRONTEND/js"
10 + SRC_HTML="$FRONTEND/index.html $FRONTEND/compose.html"
11 + SRC_CSS="$FRONTEND/css/styles.css"
12 +
13 + violations=0
14 +
15 + report() {
16 + local rule="$1"; shift
17 + local msg="$1"; shift
18 + if [ -n "$*" ]; then
19 + echo
20 + echo "[$rule] $msg"
21 + echo "$*"
22 + violations=$((violations + 1))
23 + fi
24 + }
25 +
26 + # 1. No raw hex literals in JS or source HTML (HTML entities &#NNNN; are OK;
27 + # <meta name="theme-color"> is a documented exception — value is set
28 + # programmatically by js/themes.js to track the active theme).
29 + hits=$(grep -rnE '#[0-9a-fA-F]{3,8}\b' "$SRC_JS" $SRC_HTML 2>/dev/null \
30 + | grep -vE '&#[0-9]+;' \
31 + | grep -vE 'meta name="theme-color"' \
32 + || true)
33 + report "no-raw-hex" "Raw hex literal in JS/HTML — use a CSS class or token instead." "$hits"
34 +
35 + # 2. No style.cssText anywhere in JS.
36 + hits=$(grep -rn 'cssText' "$SRC_JS" 2>/dev/null || true)
37 + report "no-csstext" "style.cssText injection — move styles into a CSS class." "$hits"
38 +
39 + # 3. No var(--token, #fallback). Fallback hex defeats theme switching.
40 + hits=$(grep -rnE 'var\(--[a-z-]+,\s*#' "$FRONTEND" --include='*.js' --include='*.html' --include='styles.css' 2>/dev/null | grep -v styles.min.css || true)
41 + report "no-var-fallback-hex" "var(--token, #fallback) — drop the fallback; it bypasses themes." "$hits"
42 +
43 + # 4. No window.confirm / bare confirm() — route through GoingsOn.ui.showConfirmDialog.
44 + hits=$(grep -rnE '\b(window\.)?confirm\(' "$SRC_JS" 2>/dev/null | grep -vE 'showConfirmDialog|confirmDelete|//\s*\*|\*\s' || true)
45 + report "no-window-confirm" "window.confirm() — use GoingsOn.ui.showConfirmDialog instead." "$hits"
46 +
47 + # 5. No inline style= that touches color / background / border / shadow / font / padding values.
48 + hits=$(grep -rnE 'style="[^"]*(color|background|border|shadow|font-size|font-family|padding)' "$SRC_JS" $SRC_HTML 2>/dev/null || true)
49 + report "no-styled-attrs" "Inline style= with color/background/border/shadow/font/padding — use a class." "$hits"
50 +
51 + # 6. Deprecated empty-state classes have been removed.
52 + hits=$(grep -rnE 'empty-dashboard-list|kanban-empty|virtual-scroller-empty' "$FRONTEND" --include='*.js' --include='*.html' --include='styles.css' 2>/dev/null | grep -v styles.min.css || true)
53 + report "no-deprecated-empty-states" "Deprecated class — use .empty-state with --compact / --dashboard / --error." "$hits"
54 +
55 + # 7. No native browser dialogs. Charter rule from Phase 7 roll-up.
56 + hits=$(grep -rnE '\b(window\.)?(confirm|prompt|alert)\(' "$SRC_JS" 2>/dev/null \
57 + | grep -vE 'showConfirmDialog|showPromptDialog|confirmDelete|//\s|\*\s' || true)
58 + report "no-native-dialogs" "window.confirm/prompt/alert are banned — use GoingsOn.ui.show{Confirm,Prompt}Dialog or showToast." "$hits"
59 +
60 + if [ $violations -eq 0 ]; then
61 + echo "frontend lint: clean"
62 + exit 0
63 + else
64 + echo
65 + echo "frontend lint: $violations rule(s) failed"
66 + exit 1
67 + fi
@@ -98,32 +98,33 @@
98 98 /* Clean neobrutalism: bold borders, flat by default, offset shadows on actions + floats */
99 99
100 100 /* Neo-brutalist color palette */
101 - --bg-primary: #E0E4FA;
102 - --bg-secondary: #CDD3F0;
103 - --bg-tertiary: #BAC2E6;
104 - --bg-card: #FFFFFF;
105 -
106 - --text-primary: #000000;
107 - --text-secondary: #2D2D2D;
108 - --text-muted: #6B6B6B; /* WCAG AA contrast (5.7:1 on white) */
109 -
110 - --accent-yellow: #F7D154;
111 - --accent-green: #5CB85C;
112 - --accent-blue: #6196FF;
113 - --accent-purple: #7B68EE;
114 - --accent-red: #DC3545;
115 - --accent-cyan: #17A2B8;
116 -
117 - --border-color: #000000;
118 - --border-width: 2px;
119 - --border-width-sm: 2px;
120 -
121 - /* Aliases for CSS that references these names */
122 - --accent-color: var(--accent-blue);
123 - --accent-primary: var(--accent-blue);
124 - --bg-hover: var(--bg-tertiary);
125 - --border-light: var(--bg-tertiary);
126 - --text-on-accent: var(--bg-card);
101 + /* Themeable: mapped by js/themes.js. Defaults below are the "neobrute" theme. */
102 + --bg-primary: #E0E4FA; /* themeable: background.primary */
103 + --bg-secondary: #CDD3F0; /* themeable: background.secondary */
104 + --bg-tertiary: #BAC2E6; /* themeable: background.tertiary */
105 + --bg-card: #FFFFFF; /* themeable: background.surface */
106 +
107 + --text-primary: #000000; /* themeable: foreground.primary */
108 + --text-secondary: #2D2D2D; /* themeable: foreground.secondary */
109 + --text-muted: #6B6B6B; /* themeable: foreground.muted (WCAG AA 5.7:1 on white) */
110 +
111 + --accent-yellow: #F7D154; /* themeable: accent.yellow */
112 + --accent-green: #5CB85C; /* themeable: accent.green */
113 + --accent-blue: #6196FF; /* themeable: accent.blue */
114 + --accent-purple: #7B68EE; /* themeable: accent.purple */
115 + --accent-red: #DC3545; /* themeable: accent.red */
116 + --accent-cyan: #17A2B8; /* themeable: accent.cyan */
117 +
118 + --border-color: #000000; /* themeable: border.default */
119 + --border-width: 2px; /* theme-invariant */
120 + --border-width-sm: 2px; /* theme-invariant */
121 +
122 + /* Derived aliases — automatically follow their upstream tokens, so themes don't need to override them. */
123 + --accent-color: var(--accent-blue); /* derived from --accent-blue */
124 + --accent-primary: var(--accent-blue); /* derived from --accent-blue */
125 + --bg-hover: var(--bg-tertiary); /* derived from --bg-tertiary */
126 + --border-light: var(--bg-tertiary); /* derived from --bg-tertiary */
127 + --text-on-accent: var(--bg-card); /* derived: relies on accents being saturated mid-tones; review on theme switch */
127 128
128 129 /* Shadow offset scale (used by 3D action elements only) */
129 130 --shadow-offset-xs: 1px;
@@ -190,7 +191,9 @@
190 191 --transition-normal: 0.15s;
191 192 --transition-slow: 0.3s;
192 193
193 - /* Modal overlay */
194 + /* Modal overlay — derived: mixes --text-primary into transparent. On a dark theme this
195 + produces a *light* scrim, which is wrong; dark themes should override with their own
196 + --overlay-color or this should be reworked to use a dedicated --scrim token. */
194 197 --overlay-color: color-mix(in srgb, var(--text-primary) 60%, transparent);
195 198
196 199 }
@@ -792,13 +795,6 @@ body {
792 795 text-align: right;
793 796 }
794 797
795 - /* Virtual Scroller Empty State */
796 - .virtual-scroller-empty {
797 - padding: 2rem;
798 - text-align: center;
799 - color: var(--text-secondary);
800 - }
801 -
802 798 /* Event Table - extends .data-table */
803 799 .event-table tbody tr {
804 800 cursor: pointer;
@@ -1416,11 +1412,60 @@ body {
1416 1412 }
1417 1413 }
1418 1414
1419 - /* Undo Toast */
1420 - .toast-undo {
1415 + .toast {
1416 + position: fixed;
1417 + bottom: var(--space-5);
1418 + right: var(--space-5);
1419 + padding: var(--space-3) var(--space-5);
1420 + background: var(--bg-card);
1421 + color: var(--text-primary);
1422 + border: var(--border-width) solid var(--border-color);
1423 + border-radius: var(--radius-md);
1424 + box-shadow: var(--shadow-brutal-md);
1425 + z-index: 2000;
1426 + font-weight: 600;
1421 1427 display: flex;
1422 1428 align-items: center;
1423 - gap: 1rem;
1429 + gap: var(--space-3);
1430 + animation: toastSlideIn 0.3s ease;
1431 + }
1432 +
1433 + .toast.toast-leaving {
1434 + opacity: 0;
1435 + transform: translateY(10px);
1436 + transition: opacity 0.3s ease, transform 0.3s ease;
1437 + }
1438 +
1439 + .toast-info {
1440 + background: var(--bg-card);
1441 + color: var(--text-primary);
1442 + }
1443 +
1444 + .toast-success {
1445 + background: var(--accent-green);
1446 + color: var(--text-on-accent);
1447 + }
1448 +
1449 + .toast-error {
1450 + background: var(--accent-red);
1451 + color: var(--text-on-accent);
1452 + }
1453 +
1454 + .toast-action {
1455 + background: none;
1456 + border: 1px solid currentColor;
1457 + border-radius: var(--radius-sm);
1458 + color: inherit;
1459 + cursor: pointer;
1460 + padding: 0.15rem 0.5rem;
1461 + font-size: var(--font-size-sm);
1462 + font-weight: 600;
1463 + font-family: inherit;
1464 + }
1465 +
1466 + /* Undo Toast */
1467 + .toast-undo {
1468 + z-index: 10000;
1424 1469 }
1425 1470
1426 1471 .undo-message {
@@ -1674,43 +1719,1076 @@ body {
1674 1719 background-color: var(--bg-card);
1675 1720 box-shadow: 0 0 0 2px var(--accent-blue);
1676 1721 }
1677 -
1678 - .form-textarea {
1679 - min-height: 100px;
1680 - resize: vertical;
1722 +
1723 + .form-textarea {
1724 + min-height: 100px;
1725 + resize: vertical;
1726 + }
1727 +
1728 + .form-actions {
1729 + display: flex;
1730 + justify-content: flex-end;
1731 + gap: 0.75rem;
1732 + margin-top: 1.5rem;
1733 + }
1734 +
1735 + /* Form Validation */
1736 + .form-input[aria-invalid="true"],
1737 + .form-select[aria-invalid="true"],
1738 + .form-textarea[aria-invalid="true"] {
1739 + border-color: var(--accent-red);
1740 + box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent-red) 30%, transparent);
1741 + }
1742 +
1743 + .form-input[aria-invalid="true"]:focus,
1744 + .form-select[aria-invalid="true"]:focus,
1745 + .form-textarea[aria-invalid="true"]:focus {
1746 + box-shadow: 0 0 0 2px var(--accent-red);
1747 + }
1748 +
1749 + .form-error {
1750 + color: var(--accent-red);
1751 + font-size: 0.8rem;
1752 + font-weight: 600;
1753 + margin-top: 0.25rem;
1754 + display: none;
1755 + }
1756 +
1757 + .form-error.visible {
1758 + display: block;
1759 + }
1760 +
1761 + .form-hint {
1762 + font-size: var(--font-size-sm);
1763 + color: var(--text-secondary);
1764 + margin-top: var(--space-1);
1765 + }
1766 +
1767 + .form-hint--preview {
1768 + color: var(--accent-primary);
1769 + }
1770 +
1771 + .form-checkbox-label {
1772 + display: flex;
1773 + align-items: center;
1774 + gap: var(--space-2);
1775 + cursor: pointer;
1776 + }
1777 +
1778 + .empty-state-action {
1779 + margin-top: var(--space-3);
1780 + }
1781 +
1782 + /* ===================================================================
1783 + Layout utility classes (used by JS-rendered markup)
1784 + =================================================================== */
1785 + .stack {
1786 + display: flex;
1787 + flex-direction: column;
1788 + }
1789 + .stack-2 { gap: var(--space-2); }
1790 + .stack-3 { gap: var(--space-3); }
1791 + .stack-4 { gap: var(--space-4); }
1792 +
1793 + .row-flex {
1794 + display: flex;
1795 + align-items: center;
1796 + }
1797 + .row-flex-2 { gap: var(--space-2); }
1798 + .row-flex-3 { gap: var(--space-3); }
1799 + .row-flex-4 { gap: var(--space-4); }
1800 +
1801 + .text-center { text-align: center; }
1802 + .text-muted { color: var(--text-muted); }
1803 + .text-secondary { color: var(--text-secondary); }
1804 + .text-danger { color: var(--accent-red); }
1805 + .text-sm { font-size: var(--font-size-sm); }
1806 + .text-xs { font-size: var(--font-size-xs, 0.75rem); }
1807 +
1808 + .section-divider {
1809 + border-top: var(--border-width-sm) solid var(--border-color);
1810 + padding-top: var(--space-4);
1811 + margin-top: var(--space-4);
1812 + }
1813 +
1814 + /* Sync dashboard primitives */
1815 + .sync-status-row {
1816 + display: flex;
1817 + align-items: center;
1818 + gap: var(--space-3);
1819 + margin-bottom: var(--space-5);
1820 + }
1821 + .sync-status-dot {
1822 + width: 10px;
1823 + height: 10px;
1824 + border-radius: var(--radius-full);
1825 + background: var(--accent-green);
1826 + display: inline-block;
1827 + }
1828 + .sync-stats-grid {
1829 + display: grid;
1830 + grid-template-columns: 1fr 1fr;
1831 + gap: var(--space-4);
1832 + margin-bottom: var(--space-5);
1833 + }
1834 + .sync-stat {
1835 + padding: var(--space-3);
1836 + background: var(--bg-secondary);
1837 + border-radius: var(--radius-md);
1838 + }
1839 + .sync-stat-label {
1840 + font-size: var(--font-size-sm);
1841 + color: var(--text-muted);
1842 + margin-bottom: var(--space-1);
1843 + }
1844 + .sync-stat-value {
1845 + font-size: var(--font-size-md, 0.9rem);
1846 + }
1847 + .sync-encryption-error {
1848 + color: var(--accent-red);
1849 + font-size: var(--font-size-sm);
1850 + margin-top: var(--space-2);
1851 + display: none;
1852 + }
1853 + .sync-encryption-error.visible {
1854 + display: block;
1855 + }
1856 + .sync-section-actions {
1857 + margin-bottom: var(--space-5);
1858 + }
1859 + .sync-banner {
1860 + padding: var(--space-3);
1861 + background: var(--bg-secondary);
1862 + border-radius: var(--radius-md);
1863 + margin-bottom: var(--space-4);
1864 + font-size: var(--font-size-sm);
1865 + color: var(--text-secondary);
1866 + }
1867 + .sync-banner--warning {
1868 + background: var(--bg-secondary);
1869 + border: 1px solid var(--border-color);
1870 + padding: var(--space-4);
1871 + }
1872 + .sync-empty {
1873 + padding: var(--space-5) 0;
1874 + }
1875 + .sync-empty-message {
1876 + margin-bottom: var(--space-5);
1877 + }
1878 + .sync-hint {
1879 + margin-bottom: var(--space-4);
1880 + }
1881 + .sync-status-label {
1882 + font-weight: 500;
1883 + }
1884 + .sync-account-row {
1885 + display: flex;
1886 + align-items: baseline;
1887 + gap: var(--space-3);
1888 + margin-bottom: var(--space-5);
1889 + font-size: var(--font-size-sm);
1890 + }
1891 + .sync-account-label {
1892 + color: var(--text-muted);
1893 + }
1894 + .sync-account-value {
1895 + color: var(--text-primary);
1896 + }
1897 + .sync-account-username {
1898 + color: var(--text-muted);
1899 + }
1900 + .sync-banner-title {
1901 + font-weight: 500;
1902 + margin: 0 0 var(--space-3) 0;
1903 + }
1904 + .sync-banner-body {
1905 + margin: 0 0 var(--space-2) 0;
1906 + font-size: var(--font-size-sm);
1907 + color: var(--text-secondary);
1908 + }
1909 + .sync-banner-tier-line {
1910 + margin: 0 0 var(--space-4) 0;
1911 + font-size: var(--font-size-sm);
1912 + color: var(--text-secondary);
1913 + }
1914 + .sync-banner-actions {
1915 + display: flex;
1916 + gap: var(--space-2);
1917 + align-items: center;
1918 + }
1919 +
1920 + /* Backup / data settings primitives */
1921 + .settings-subheading {
1922 + margin-bottom: var(--space-3);
1923 + font-size: var(--font-size-md, 0.9rem);
1924 + }
1925 + .settings-desc-block {
1926 + font-size: var(--font-size-sm);
1927 + color: var(--text-secondary);
1928 + margin-bottom: var(--space-4);
1929 + }
1930 + .settings-actions-row {
1931 + display: flex;
1932 + flex-wrap: wrap;
1933 + gap: var(--space-3);
1934 + margin-bottom: var(--space-4);
1935 + }
1936 + .settings-actions-row--center {
1937 + align-items: center;
1938 + flex-wrap: nowrap;
1939 + margin-top: var(--space-3);
1940 + }
1941 + .settings-status-text {
1942 + font-size: var(--font-size-sm);
1943 + color: var(--text-secondary);
1944 + }
1945 + .settings-section-block {
1946 + padding-top: var(--space-4);
1947 + border-top: var(--border-width-sm) solid var(--border-color);
1948 + }
1949 + .form-hint--spaced {
1950 + margin-top: var(--space-2);
1951 + }
1952 + .empty-italic {
1953 + color: var(--text-secondary);
1954 + font-style: italic;
1955 + }
1956 +
1957 + .form-grid-2 {
1958 + display: grid;
1959 + grid-template-columns: 1fr 1fr;
1960 + gap: var(--space-4);
1961 + }
1962 +
1963 + /* Email account list / detail primitives */
1964 + .email-detect-status {
1965 + font-size: var(--font-size-sm);
1966 + color: var(--text-secondary);
1967 + margin-top: var(--space-1);
1968 + }
1969 + .email-detect-note {
1970 + font-size: var(--font-size-sm);
1971 + margin-top: var(--space-2);
1972 + padding: var(--space-3) var(--space-3);
1973 + background: var(--bg-tertiary);
1974 + border-left: 3px solid var(--accent-primary);
1975 + border-radius: var(--radius-sm);
1976 + line-height: var(--line-height-normal);
1977 + }
1978 + .account-row {
1979 + padding: var(--space-3);
1980 + background: var(--bg-secondary);
1981 + border-radius: var(--radius-sm);
1982 + margin-bottom: var(--space-2);
1983 + }
1984 + .account-row-header {
1985 + display: flex;
1986 + justify-content: space-between;
1987 + align-items: flex-start;
1988 + }
1989 + .account-row-name {
1990 + font-weight: 600;
1991 + }
1992 + .account-row-meta {
1993 + font-size: var(--font-size-sm);
1994 + color: var(--text-secondary);
1995 + }
1996 + .account-row-sync {
1997 + font-size: var(--font-size-sm);
1998 + color: var(--text-secondary);
1999 + }
2000 + .account-row-provider-badge {
2001 + font-size: var(--font-size-xs, 0.7rem);
2002 + padding: 0.125rem 0.375rem;
2003 + background: var(--accent-blue);
2004 + color: var(--text-on-accent);
2005 + border-radius: var(--radius-xs);
2006 + margin-left: var(--space-2);
2007 + }
2008 + .account-delete-btn {
2009 + color: var(--accent-red);
2010 + }
2011 + .test-conn-result {
2012 + padding: var(--space-2);
2013 + background: var(--bg-secondary);
2014 + border-radius: var(--radius-sm);
2015 + margin-bottom: var(--space-2);
2016 + }
2017 + .test-conn-result--success { color: var(--accent-green); }
2018 + .test-conn-result--error { color: var(--accent-red); }
2019 +
2020 + .folder-list {
2021 + max-height: 150px;
2022 + overflow-y: auto;
2023 + background: var(--bg-secondary);
2024 + padding: var(--space-2);
2025 + border-radius: var(--radius-sm);
2026 + font-family: var(--font-mono);
2027 + font-size: var(--font-size-sm);
2028 + }
2029 + .folder-list-meta {
2030 + font-size: var(--font-size-sm);
2031 + color: var(--text-secondary);
2032 + margin-top: var(--space-2);
2033 + }
2034 + .error-pre {
2035 + background: var(--bg-secondary);
2036 + padding: var(--space-2);
2037 + border-radius: var(--radius-sm);
2038 + font-family: var(--font-mono);
2039 + font-size: var(--font-size-sm);
2040 + white-space: pre-wrap;
2041 + max-height: 150px;
2042 + overflow-y: auto;
2043 + }
2044 + .test-conn-section {
2045 + border-top: var(--border-width-sm) solid var(--border-color);
2046 + margin: var(--space-4) 0;
2047 + padding-top: var(--space-4);
2048 + }
2049 + .oauth-waiting {
2050 + text-align: center;
2051 + padding: var(--space-5);
2052 + }
2053 + .oauth-waiting-title {
2054 + font-size: var(--font-size-lg, 1.25rem);
2055 + margin-bottom: var(--space-4);
2056 + }
2057 + .oauth-waiting-body {
2058 + color: var(--text-secondary);
2059 + margin-bottom: var(--space-5);
2060 + }
2061 + .oauth-waiting-spinner {
2062 + margin: 0 auto var(--space-5);
2063 + }
2064 +
2065 + .contact-card-header { gap: var(--space-3); }
2066 + .contact-card-body { flex: 1; min-width: 0; }
2067 + .contact-detail-header {
2068 + display: flex;
2069 + align-items: center;
Lines truncated
@@ -1 +1 @@
1 - @font-face{font-family:Reglo;src:url('../fonts/Reglo-Bold.woff2') format('woff2');font-weight:700;font-style:normal;font-display:swap}*,::after,::before{box-sizing:border-box;margin:0;padding:0}:root{--timeline-slot-h:12px;--bg-primary:#E0E4FA;--bg-secondary:#CDD3F0;--bg-tertiary:#BAC2E6;--bg-card:#FFFFFF;--text-primary:#000000;--text-secondary:#2D2D2D;--text-muted:#6B6B6B;--accent-yellow:#F7D154;--accent-green:#5CB85C;--accent-blue:#6196FF;--accent-purple:#7B68EE;--accent-red:#DC3545;--accent-cyan:#17A2B8;--border-color:#000000;--border-width:2px;--border-width-sm:2px;--accent-color:var(--accent-blue);--accent-primary:var(--accent-blue);--bg-hover:var(--bg-tertiary);--border-light:var(--bg-tertiary);--text-on-accent:var(--bg-card);--shadow-offset-xs:1px;--shadow-offset-md:3px;--shadow-offset:4px;--shadow-offset-lg:6px;--shadow-offset-xl:8px;--shadow-brutal-xs:var(--shadow-offset-xs) var(--shadow-offset-xs) 0 var(--border-color);--shadow-brutal-md:var(--shadow-offset-md) var(--shadow-offset-md) 0 var(--border-color);--shadow-brutal-lg:var(--shadow-offset-lg) var(--shadow-offset-lg) 0 var(--border-color);--shadow-brutal-xl:var(--shadow-offset-xl) var(--shadow-offset-xl) 0 var(--border-color);--radius-xs:3px;--radius-sm:5px;--radius-md:5px;--radius-lg:10px;--radius-xl:20px;--radius-full:50%;--width-container:1400px;--width-modal:560px;--width-sidebar:280px;--space-1:0.25rem;--space-2:0.5rem;--space-3:0.75rem;--space-4:1rem;--space-5:1.25rem;--space-6:1.5rem;--font-sans:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;--font-serif:Georgia,'Times New Roman',serif;--font-mono:'SF Mono','Consolas','Liberation Mono',monospace;--font-display:'Reglo',var(--font-serif);--font-heading:var(--font-sans);--font-body:var(--font-sans);--font-size-xxs:0.65rem;--font-size-xs:0.7rem;--font-size-sm:0.75rem;--font-size-md:0.8rem;--font-size-base:0.875rem;--font-size-lg:1rem;--font-size-xl:1.1rem;--font-size-2xl:1.25rem;--font-size-3xl:1.5rem;--font-size-4xl:1.75rem;--line-height-tight:1.25;--line-height-normal:1.5;--line-height-relaxed:1.75;--transition-fast:0.1s;--transition-normal:0.15s;--transition-slow:0.3s;--overlay-color:color-mix(in srgb, var(--text-primary) 60%, transparent)}html{font-size:16px}.flex-1{flex:1}.flex-center-gap{display:flex;align-items:center;gap:.5rem}.text-sm-secondary{font-size:.875rem;color:var(--text-secondary)}.text-xs-secondary{font-size:.75rem;color:var(--text-secondary)}.text-accent-red{color:var(--accent-red)}.mb-1{margin-bottom:1rem}.settings-divider{margin-top:1.5rem;padding-top:1.5rem;border-top:2px solid var(--border-color)}.settings-heading{margin-bottom:1rem;font-family:var(--font-heading)}.settings-desc{font-size:.875rem;color:var(--text-secondary);margin-bottom:1rem}.subtask-item{display:flex;align-items:center;gap:.5rem;padding:.5rem;background:var(--bg-secondary);border-radius:4px;margin-bottom:.5rem}.subtask-item-linked{display:flex;align-items:center;gap:.5rem;padding:.5rem;background:var(--bg-tertiary);border-radius:4px;margin-bottom:.5rem;border-left:var(--border-width) solid var(--accent-color)}.subtask-checkbox{cursor:pointer;width:18px;height:18px}.subtask-checkbox-disabled{cursor:not-allowed;width:18px;height:18px;opacity:.5}.subtask-text-done{text-decoration:line-through;opacity:.6}body{font-family:var(--font-sans);background-color:var(--bg-primary);color:var(--text-primary);line-height:1.6;height:100vh;overflow:hidden;display:flex;flex-direction:column}.app-header{background:var(--bg-card);border-bottom:var(--border-width) solid var(--border-color);padding:.75rem 1.5rem;display:flex;justify-content:space-between;align-items:center}.header-content{display:flex;align-items:center;gap:.75rem}.header-actions{display:flex;align-items:center;gap:.5rem}.app-title{font-family:var(--font-display);font-size:1.75rem;font-weight:700;color:var(--text-primary);letter-spacing:-.02em}.app-subtitle{font-size:.875rem;color:var(--text-muted);font-weight:500;line-height:1}.mobile-view-title{display:none}.tab-navigation{display:flex;justify-content:center;gap:.5rem}.tab{display:flex;align-items:center;gap:.5rem;padding:.75rem 1.25rem;text-decoration:none;color:var(--text-primary);background:var(--bg-card);border:var(--border-width) solid var(--border-color);border-radius:var(--radius-md);font-weight:600;transition:background-color .15s ease}.tab:hover{background:var(--bg-secondary)}.tab.active{background-color:var(--accent-blue);color:var(--text-on-accent)}.tab-icon{font-size:1.1rem}.tab-label{font-weight:600;font-size:.9rem}.tab.tab-right{margin-left:auto}.tab-group .subview.hidden{display:none}.pill-nav{display:flex;align-items:center;gap:var(--space-1);padding:0;margin-bottom:1rem;min-height:2rem}.pill{padding:var(--space-1) var(--space-3);border-radius:var(--radius-xl);border:var(--border-width-sm) solid var(--border-color);background:var(--bg-card);font-family:var(--font-sans);font-size:var(--font-size-sm);font-weight:600;cursor:pointer;transition:background-color var(--transition-fast)}.pill:hover{background:var(--bg-tertiary)}.pill.active{background:var(--text-primary);color:var(--bg-card);border-color:var(--text-primary)}.main-content{flex:1;max-width:var(--width-container);width:100%;margin:0 auto;padding:1.5rem 1.75rem 2rem}.page-header{display:flex;justify-content:space-between;align-items:center;gap:.5rem;margin-bottom:1rem}.page-title{font-family:var(--font-heading);font-size:1.75rem;font-weight:700;color:var(--text-primary)}.tab-group{position:relative}.tab-group>.subview>.page-header{position:absolute;top:0;right:0;margin:0;z-index:1}.tab-group .page-header .page-title{display:none}#day-plan-view>.page-header,#project-dashboard-view>.page-header{position:static;margin-bottom:1rem}#project-dashboard-view .page-title{display:block}.btn{display:inline-flex;align-items:center;justify-content:center;gap:.5rem;padding:.625rem 1.25rem;border:var(--border-width) solid var(--border-color);border-radius:var(--radius-sm);font-size:.9rem;font-weight:600;cursor:pointer;transition:background-color .15s ease;text-decoration:none;background:var(--bg-card);color:var(--text-primary)}.btn:hover{background:var(--bg-secondary)}.btn:active{background:var(--bg-tertiary)}.btn:disabled{background:var(--bg-tertiary);color:var(--text-muted);cursor:not-allowed;opacity:.7}.btn:disabled:hover{background:var(--bg-tertiary)}.btn-primary{background-color:var(--accent-blue);color:var(--text-on-accent)}.btn-primary:hover{background-color:color-mix(in srgb,var(--accent-blue) 85%,#000)}.btn-primary:active{background-color:color-mix(in srgb,var(--accent-blue) 70%,#000)}.btn-secondary{background-color:var(--bg-secondary);color:var(--text-primary)}.btn-danger{background-color:var(--accent-red);color:var(--text-on-accent)}.btn-danger:hover{background-color:color-mix(in srgb,var(--accent-red) 85%,#000)}.btn-danger:active{background-color:color-mix(in srgb,var(--accent-red) 70%,#000)}.btn-sm{padding:.375rem .75rem;font-size:.8rem}.quick-add{display:flex;gap:.75rem;margin-bottom:1.5rem}.quick-add-input{flex:1;padding:.875rem 1rem;border:var(--border-width) solid var(--border-color);border-radius:var(--radius-md);background-color:var(--bg-card);font-size:1rem;color:var(--text-primary)}.quick-add-input::placeholder{color:var(--text-muted)}.quick-add-input:focus{outline:0;background-color:var(--accent-blue);color:var(--text-on-accent);box-shadow:0 0 0 2px var(--border-color)}.cards-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:1.25rem}.card{background-color:var(--bg-card);border:var(--border-width) solid var(--border-color);border-radius:var(--radius-md);padding:1.25rem;box-shadow:var(--shadow-offset) var(--shadow-offset) 0 var(--border-color);transition:transform .15s ease,box-shadow .15s ease,background-color .15s ease;cursor:pointer}.card:hover{background-color:var(--bg-secondary);transform:translate(-2px,-2px);box-shadow:calc(var(--shadow-offset) + 2px) calc(var(--shadow-offset) + 2px) 0 var(--border-color)}.card-header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:.75rem}.card-title{font-family:var(--font-heading);font-size:1.1rem;font-weight:700;color:var(--text-primary)}.card-description{font-size:.9rem;color:var(--text-secondary);margin-bottom:1rem}.markdown-content{font-size:.9rem;color:var(--text-secondary);line-height:1.5}.markdown-content p{margin:0 0 .5em 0}.markdown-content p:last-child{margin-bottom:0}.markdown-content ol,.markdown-content ul{margin:0 0 .5em 1.5em;padding:0}.markdown-content code{background:var(--bg-tertiary);padding:.1em .3em;border-radius:3px;font-size:.85em}.markdown-content pre{background:var(--bg-tertiary);padding:.5em;border-radius:4px;overflow-x:auto;margin:0 0 .5em 0}.markdown-content pre code{background:0 0;padding:0}.markdown-content a{color:var(--accent-color)}.markdown-content blockquote{border-left:3px solid var(--border-color);margin:0 0 .5em 0;padding-left:.75em;color:var(--text-secondary)}.markdown-content h1,.markdown-content h2,.markdown-content h3{margin:.5em 0 .25em 0;font-size:1em;font-weight:600;color:var(--text-primary)}.markdown-content table{border-collapse:collapse;margin:.5em 0}.markdown-content td,.markdown-content th{border:1px solid var(--border-color);padding:.25em .5em}.markdown-content img{max-width:100%}.card-meta{display:flex;gap:.5rem;flex-wrap:wrap}.badge,.tag{display:inline-flex;align-items:center;padding:.25rem .625rem;border:var(--border-width-sm) solid var(--border-color);border-radius:var(--radius-sm);font-size:.8125rem;font-weight:600;background:var(--bg-card);color:var(--text-primary)}.badge[data-color=green],.tag[data-color=green]{background-color:color-mix(in srgb,var(--accent-green) 20%,var(--bg-card));border-color:var(--accent-green)}.badge[data-color=yellow],.tag[data-color=yellow]{background-color:color-mix(in srgb,var(--accent-yellow) 20%,var(--bg-card));border-color:var(--accent-yellow)}.badge[data-color=red],.tag[data-color=red]{background-color:color-mix(in srgb,var(--accent-red) 20%,var(--bg-card));border-color:var(--accent-red)}.badge[data-color=cyan],.tag[data-color=cyan]{background-color:color-mix(in srgb,var(--accent-cyan) 20%,var(--bg-card));border-color:var(--accent-cyan)}.badge[data-color=purple],.tag[data-color=purple]{background-color:color-mix(in srgb,var(--accent-purple) 20%,var(--bg-card));border-color:var(--accent-purple)}.badge[data-color=muted],.tag[data-color=muted]{background-color:var(--bg-tertiary);border-color:var(--text-muted)}.tag.status-active{background-color:color-mix(in srgb,var(--accent-green) 20%,var(--bg-card));border-color:var(--accent-green)}.tag.status-on_hold,.tag.status-onhold{background-color:color-mix(in srgb,var(--accent-yellow) 20%,var(--bg-card));border-color:var(--accent-yellow)}.tag.status-archived{background-color:var(--bg-tertiary);border-color:var(--text-muted)}.tag.status-inactive{background-color:color-mix(in srgb,var(--accent-red) 20%,var(--bg-card));border-color:var(--accent-red)}.tag.status-completed{background-color:color-mix(in srgb,var(--accent-cyan) 20%,var(--bg-card));border-color:var(--accent-cyan)}.data-table{width:100%;border-collapse:separate;border-spacing:0;background-color:var(--bg-card);border:var(--border-width) solid var(--border-color);border-radius:var(--radius-md);overflow:hidden;box-shadow:var(--shadow-offset) var(--shadow-offset) 0 var(--border-color)}.data-table td,.data-table th{padding:1rem 1.25rem;text-align:left;border-bottom:2px solid var(--border-color)}.data-table th{background-color:var(--bg-secondary);font-size:.8rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:var(--text-primary)}.data-table tbody tr{transition:background-color .15s ease}.data-table tbody tr:hover{background-color:var(--bg-secondary)}.data-table tbody tr:last-child td{border-bottom:none}.data-table tbody tr.keyboard-selected,.data-table tbody tr.selected{background-color:color-mix(in srgb,var(--accent-blue) 25%,var(--bg-card))}.task-table{width:100%;background:var(--bg-card);border:var(--border-width) solid var(--border-color);border-radius:var(--radius-lg);overflow:hidden;display:flex;flex-direction:column;flex:1;min-height:0;box-shadow:var(--shadow-offset) var(--shadow-offset) 0 var(--border-color)}.task-header-row,.task-row{display:grid;grid-template-columns:1fr 140px 60px 110px 90px 100px 90px;align-items:center;gap:.75rem}.task-header-row{background-color:var(--bg-secondary);border-bottom:2px solid var(--border-color);padding:0 1.25rem}.task-header-row .task-cell{padding:.75rem 0;font-size:.8rem;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--text-primary)}.task-list-container{flex:1;min-height:0;overflow-y:auto;position:relative}.task-row{padding:.75rem 1.25rem;border-bottom:1px solid var(--border-color);transition:background-color .15s ease,opacity .25s ease,transform .25s ease;cursor:pointer}.task-row-removing{opacity:0;transform:translateX(20px)}.task-row:hover{background-color:var(--bg-secondary)}.task-row:last-child{border-bottom:none}.task-row.keyboard-selected,.task-row.selected{background-color:color-mix(in srgb,var(--accent-blue) 25%,var(--bg-card))}.task-cell{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}.task-actions-header{text-align:right}.virtual-scroller-empty{padding:2rem;text-align:center;color:var(--text-secondary)}.event-table tbody tr{cursor:pointer}.task-description{font-weight:600;white-space:normal;display:flex;flex-wrap:wrap;align-items:center;gap:.25rem .5rem}.task-description-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:100%}.task-project{font-size:.85rem;color:var(--text-secondary);white-space:nowrap}.priority-high,.priority-low,.priority-medium{display:inline-block;padding:.25rem .5rem;border-radius:var(--radius-xs);font-weight:700;text-align:center}.priority-high{color:var(--accent-red);background:#fde8ea;background:color-mix(in srgb,var(--accent-red) 15%,var(--bg-card))}.priority-medium{color:var(--accent-yellow);background:#fef8e6;background:color-mix(in srgb,var(--accent-yellow) 15%,var(--bg-card))}.priority-low{color:var(--text-muted);background:var(--bg-secondary)}.sortable{cursor:pointer;user-select:none;white-space:nowrap}.sortable:hover{background:var(--bg-hover)}.sort-arrow{display:inline-block;width:.8em;margin-left:.25rem;opacity:.3}.sort-arrow::after{content:'\2195'}.sortable.sort-asc .sort-arrow::after{content:'\2191'}.sortable.sort-desc .sort-arrow::after{content:'\2193'}.sortable.sort-asc .sort-arrow,.sortable.sort-desc .sort-arrow{opacity:1}.task-overdue .task-description-text{color:var(--accent-red)}.task-overdue .task-due{color:var(--accent-red);font-weight:600}.task-tags{display:flex;gap:.25rem;flex-wrap:wrap}.task-tag{background-color:var(--bg-tertiary);color:var(--text-primary);padding:.125rem .5rem;border-radius:var(--radius-xs);font-size:.75rem;font-weight:600;border:1px solid var(--border-color)}.recurrence-icon{color:var(--accent-purple);font-size:.85rem;font-weight:700}.annotation-badge{background-color:var(--accent-yellow);color:var(--text-primary);padding:.125rem .5rem;border-radius:var(--radius-xs);font-size:.7rem;font-weight:700;border:var(--border-width-sm) solid var(--border-color)}.subtask-badge{background-color:var(--bg-secondary);color:var(--text-primary);padding:.125rem .5rem;border-radius:var(--radius-xs);font-size:.7rem;font-weight:700;border:var(--border-width-sm) solid var(--border-color);margin-left:.25rem}.task-started{border-left:4px solid var(--accent-green)}.task-completed{opacity:.5;text-decoration:line-through}.task-deleted{display:none}.due-overdue{color:var(--accent-red);font-weight:700;background:#fde8ea;background:color-mix(in srgb,var(--accent-red) 15%,var(--bg-card));padding:.25rem .5rem;border-radius:var(--radius-xs)}.due-today{color:var(--accent-yellow);font-weight:700;background:#fef8e6;background:color-mix(in srgb,var(--accent-yellow) 15%,var(--bg-card));padding:.25rem .5rem;border-radius:var(--radius-xs)}.due-soon{color:var(--text-secondary)}.due-future{color:var(--text-muted)}.events-list{display:flex;flex-direction:column;flex:1;min-height:0;gap:1rem}.event-table-virtual{width:100%;background:var(--bg-card);border:var(--border-width) solid var(--border-color);border-radius:var(--radius-lg);overflow:hidden;display:flex;flex-direction:column;flex:1;min-height:0;box-shadow:var(--shadow-offset) var(--shadow-offset) 0 var(--border-color)}.event-header-row,.event-row-virtual{display:grid;grid-template-columns:100px 80px 1fr 150px 40px;align-items:center;gap:.5rem}.event-header-row{background-color:var(--bg-secondary);border-bottom:2px solid var(--border-color);flex-shrink:0}.event-header-row .event-cell{padding:1rem 1.25rem;font-size:.8rem;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--text-primary)}.event-list-container{flex:1;min-height:0;overflow-y:auto;position:relative}.event-row-virtual{padding:.75rem 1.25rem;border-bottom:1px solid var(--border-color);transition:background-color .15s ease;cursor:pointer}.event-row-virtual:hover{background-color:var(--bg-secondary)}.event-row-virtual:last-child{border-bottom:none}.event-row-virtual.event-past{opacity:.7}.event-cell{overflow:hidden;text-overflow:ellipsis}.event-row{cursor:pointer}.event-cell-date{white-space:nowrap}.event-cell-date .event-date-num{font-weight:700;font-size:.9rem;color:var(--text-primary);margin-right:.5rem}.event-date-badge{display:inline-block;padding:.15rem .4rem;background:var(--accent-green);color:var(--text-on-accent);font-size:.7rem;font-weight:700;text-transform:uppercase;border-radius:var(--radius-xs);margin-right:.5rem}.event-cell-time{font-family:var(--font-mono);font-size:.85rem;color:var(--text-secondary)}.event-cell-title{font-weight:600}.event-cell-location{color:var(--text-secondary);font-size:.875rem}.event-date-badge.event-proximity-today{background:var(--accent-green)}.event-date-badge.event-proximity-tomorrow{background:var(--accent-yellow);color:var(--text-primary)}.event-date-badge.event-proximity-week{background:var(--accent-cyan)}.event-date-badge.event-proximity-future{background:var(--accent-blue)}.event-date-badge.event-proximity-past{background:var(--text-muted)}.event-row.event-past{opacity:.7}.no-upcoming-events{text-align:center;padding:2rem;color:var(--text-secondary);font-style:italic}.past-events-section{margin-top:.5rem}.past-events-toggle{display:flex;align-items:center;gap:.75rem;padding:.75rem 1rem;background:var(--bg-secondary);border:var(--border-width-sm) solid var(--border-color);border-radius:var(--radius-sm);cursor:pointer;font-weight:600;color:var(--text-secondary);transition:background-color .15s ease,color .15s ease;list-style:none}.past-events-toggle::-webkit-details-marker{display:none}.past-events-toggle::before{content:'▶';font-size:.7rem;transition:transform .15s ease}.past-events-section[open] .past-events-toggle::before{transform:rotate(90deg)}.past-events-toggle:hover{background:var(--bg-tertiary);color:var(--text-primary)}.past-events-label{flex:1}.past-events-count{background:var(--text-muted);color:var(--text-on-accent);font-size:.75rem;padding:.15rem .5rem;border-radius:var(--radius-sm)}.past-events-section .event-table-past{margin-top:.75rem;opacity:.85}.past-events-section .event-list-container{max-height:300px}#recurring-events-section .event-table-virtual{opacity:1;margin-top:.75rem}#recurring-events-section .event-list-container{max-height:320px}.event-row-virtual.event-recurring .event-recurrence-pattern{font-weight:600;color:var(--accent-primary)}.events-section-heading{margin:1rem 0 .5rem;font-size:.95rem;font-weight:700;color:var(--text-secondary);text-transform:uppercase;letter-spacing:.05em}.event-item{display:flex;gap:1rem;padding:1rem;background-color:var(--bg-card);border:var(--border-width) solid var(--border-color);border-radius:var(--radius-md);transition:background-color .15s ease;cursor:pointer}.event-item:hover{background-color:var(--bg-secondary)}.event-date{flex-shrink:0;width:80px;text-align:center;padding:.75rem;background-color:var(--accent-green);border-radius:var(--radius-sm);color:var(--text-on-accent)}.event-date-day{font-size:.7rem;font-weight:700;text-transform:uppercase;letter-spacing:.05em}.event-date-num{font-size:1.5rem;font-weight:700}.event-content{flex:1}.event-title{font-family:var(--font-heading);font-weight:700;font-size:1.1rem;color:var(--text-primary);margin-bottom:.25rem}.event-details{font-size:.875rem;color:var(--text-secondary);display:flex;gap:1rem}.event-location,.event-time{display:flex;align-items:center;gap:.25rem}.event-project{margin-top:.5rem}.email-list{display:flex;flex-direction:column;background-color:var(--bg-card);border:var(--border-width) solid var(--border-color);border-radius:var(--radius-md);overflow:hidden;flex:1;min-height:0;box-shadow:var(--shadow-offset) var(--shadow-offset) 0 var(--border-color)}.email-list-container{flex:1;min-height:0;overflow-y:auto;position:relative}.email-item{display:flex;gap:1rem;padding:1rem;border-bottom:2px solid var(--border-color);transition:background-color .15s ease;cursor:pointer}.email-item:last-child{border-bottom:none}.email-item:hover{background-color:var(--bg-secondary)}.email-item.unread{background-color:color-mix(in srgb,var(--accent-blue) 20%,var(--bg-card));border-left:4px solid var(--accent-blue)}.email-item.unread .email-subject{font-weight:700}.email-item.unread .email-from{font-weight:700}.email-item.outgoing{border-left:4px solid var(--accent-green)}.email-checkbox{flex-shrink:0;margin-top:.25rem}.email-content{flex:1;min-width:0}.email-header{display:flex;justify-content:space-between;margin-bottom:.25rem;align-items:center;gap:.5rem}.thread-badge{background-color:var(--bg-tertiary);color:var(--text-secondary);font-size:.7rem;font-weight:600;padding:.1rem .4rem;border-radius:var(--radius-md);min-width:1.25rem;text-align:center}.email-from{color:var(--text-primary);font-size:.9rem;font-weight:600}.email-date{color:var(--text-muted);font-size:.8rem;flex-shrink:0;font-weight:600}.email-subject{color:var(--text-primary);font-size:.95rem;margin-bottom:.25rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.email-preview{color:var(--text-muted);font-size:.85rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}@keyframes toastSlideIn{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.toast-undo{display:flex;align-items:center;gap:1rem}.undo-message{flex:1}.undo-btn{padding:.25rem .75rem;background:var(--accent-blue);color:var(--text-on-accent);border:2px solid var(--border-color);border-radius:var(--radius-sm);font-family:inherit;font-size:var(--font-size-sm);font-weight:600;cursor:pointer;transition:background .15s ease}.undo-btn:hover{background:color-mix(in srgb,var(--accent-blue) 80%,#000)}.undo-countdown{font-size:var(--font-size-sm);color:var(--text-muted);min-width:2.5rem;text-align:right}@keyframes modalFadeIn{from{opacity:0}to{opacity:1}}@keyframes modalSlideIn{from{opacity:0;transform:translateY(-20px) scale(.95)}to{opacity:1;transform:translateY(0) scale(1)}}@keyframes modalFadeOut{from{opacity:1}to{opacity:0}}@keyframes modalSlideOut{from{opacity:1;transform:translateY(0) scale(1)}to{opacity:0;transform:translateY(-20px) scale(.95)}}.modal-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background-color:var(--overlay-color);display:flex;align-items:center;justify-content:center;z-index:1000;animation:modalFadeIn .15s ease-out}.modal-overlay.hidden{display:none}.modal-overlay.closing{animation:modalFadeOut .15s ease-in forwards}.modal-container{background-color:var(--bg-card);border:var(--border-width) solid var(--border-color);border-radius:var(--radius-lg);box-shadow:var(--shadow-brutal-xl);max-width:var(--width-modal);width:90%;max-height:90vh;overflow:auto;animation:modalSlideIn .2s ease-out}.modal-container.modal-large{max-width:calc(100vw - 4rem);width:calc(100vw - 4rem);max-height:calc(100vh - 4rem);height:calc(100vh - 4rem);display:flex;flex-direction:column}.modal-container.modal-large .modal-content{flex:1;overflow:auto;display:flex;flex-direction:column}.modal-overlay.closing .modal-container{animation:modalSlideOut .15s ease-in forwards}.modal-header{display:flex;justify-content:space-between;align-items:center;padding:1rem 1.5rem;border-bottom:var(--border-width) solid var(--border-color);background:var(--bg-secondary)}.modal-header h2,.modal-title{font-family:var(--font-heading);font-size:1.25rem;font-weight:700}.modal-close{background:var(--bg-card);border:var(--border-width-sm) solid var(--border-color);border-radius:var(--radius-sm);font-size:1.25rem;color:var(--text-primary);cursor:pointer;line-height:1;width:36px;height:36px;display:flex;align-items:center;justify-content:center;transition:background-color .15s ease}.modal-close:hover{background:var(--accent-blue);color:var(--text-on-accent)}.modal-content{padding:1.5rem}.form-group{margin-bottom:1.25rem}.form-more-toggle{display:block;background:0 0;border:none;cursor:pointer;font-size:.85rem;font-weight:600;color:var(--accent-blue);padding:.25rem 0;margin-bottom:.75rem}.form-more-toggle::before{content:'+ '}.form-more-toggle.expanded::before{content:'- '}.form-extended-fields.hidden{display:none}.recurrence-weekday-label{display:inline-flex;align-items:center;gap:.25rem;padding:.25rem .5rem;font-size:.8rem;border:1px solid var(--border);border-radius:var(--radius-sm);cursor:pointer}.recurrence-weekday-label:has(input:checked){background:var(--accent-primary);color:var(--bg-primary);border-color:var(--accent-primary)}.recurrence-weekday-label input[type=checkbox]{display:none}.recurrence-config .form-input,.recurrence-config .form-select{font-size:.85rem;padding:.25rem .5rem}.form-label{display:block;font-size:.9rem;font-weight:700;color:var(--text-primary);margin-bottom:.5rem}.form-input,.form-select,.form-textarea{width:100%;padding:.75rem 1rem;border:var(--border-width-sm) solid var(--border-color);border-radius:var(--radius-sm);background-color:var(--bg-card);color:var(--text-primary);font-size:1rem;box-shadow:none}.form-input:focus,.form-select:focus,.form-textarea:focus{outline:0;background-color:var(--bg-card);box-shadow:0 0 0 2px var(--accent-blue)}.form-textarea{min-height:100px;resize:vertical}.form-actions{display:flex;justify-content:flex-end;gap:.75rem;margin-top:1.5rem}.form-input[aria-invalid=true],.form-select[aria-invalid=true],.form-textarea[aria-invalid=true]{border-color:var(--accent-red);box-shadow:0 0 0 2px color-mix(in srgb,var(--accent-red) 30%,transparent)}.form-input[aria-invalid=true]:focus,.form-select[aria-invalid=true]:focus,.form-textarea[aria-invalid=true]:focus{box-shadow:0 0 0 2px var(--accent-red)}.form-error{color:var(--accent-red);font-size:.8rem;font-weight:600;margin-top:.25rem;display:none}.form-error.visible{display:block}.app-footer{background-color:var(--bg-card);border-top:var(--border-width) solid var(--border-color);padding:.75rem 1.5rem}.footer-content{max-width:var(--width-container);margin:0 auto;display:flex;justify-content:space-between;align-items:center}.keyboard-hints{display:flex;gap:1rem;font-size:.8rem;color:var(--text-muted)}kbd{display:inline-block;padding:.2rem .5rem;background-color:var(--bg-secondary);border:var(--border-width-sm) solid var(--border-color);border-radius:var(--radius-xs);font-family:var(--font-mono);font-size:.75rem;font-weight:700}.version{font-size:.75rem;color:var(--text-muted);font-weight:600}.empty-state{text-align:center;padding:3rem;color:var(--text-secondary)}.empty-state-icon{font-size:4rem;margin-bottom:1rem}.empty-state-text{font-size:1.1rem;font-weight:600;margin-bottom:1rem}.error-state{text-align:center;padding:2rem;color:var(--accent-red);background:color-mix(in srgb,var(--accent-red) 10%,var(--bg-card));border:var(--border-width-sm) solid var(--accent-red);border-radius:var(--radius-sm);font-weight:600}.view{display:block}.view.hidden{display:none}.filter-bar{display:flex;flex-wrap:wrap;gap:.75rem;margin-bottom:1.5rem;padding:1rem;background-color:var(--bg-card);border:var(--border-width) solid var(--border-color);border-radius:var(--radius-md);box-shadow:var(--shadow-offset) var(--shadow-offset) 0 var(--border-color)}.filter-group{display:flex;align-items:center;gap:.5rem}.filter-label{font-size:.8rem;font-weight:700;color:var(--text-secondary);text-transform:uppercase;letter-spacing:.05em}.filter-select{padding:.5rem .75rem;border:var(--border-width-sm) solid var(--border-color);border-radius:var(--radius-sm);background-color:var(--bg-card);color:var(--text-primary);font-size:.875rem;font-weight:600}.filter-select:focus{outline:0;background-color:var(--accent-blue);color:var(--text-on-accent)}.filter-checkbox{display:flex;align-items:center;gap:.4rem;font-size:.875rem;font-weight:600;color:var(--text-primary);cursor:pointer}.filter-checkbox input[type=checkbox]{width:1rem;height:1rem;cursor:pointer}.btn-link{background:0 0;border:none;box-shadow:none;color:var(--text-secondary);font-size:.875rem;cursor:pointer;text-decoration:underline;padding:.5rem}.btn-link:hover{box-shadow:none;transform:none;color:var(--text-primary)}@media (min-width:1400px){.main-content{max-width:1600px}.cards-grid{grid-template-columns:repeat(auto-fill,minmax(380px,1fr))}.project-dashboard-grid{gap:2rem}.day-plan-sidebar{width:320px}.modal-container{max-width:640px}}@media (max-width:1024px){.saved-views-sidebar{width:180px}.day-plan-sidebar{width:240px}.project-dashboard-grid{grid-template-columns:1fr 1fr;gap:1rem}.project-dashboard-grid .dashboard-column:last-child{grid-column:span 2}.filter-bar{flex-wrap:wrap}.filter-actions{width:100%;justify-content:flex-end;margin-top:.5rem}}@media (max-width:768px){.tab-navigation{flex-wrap:wrap;gap:.5rem}.tab{flex:1 1 auto;min-width:calc(33% - .5rem);justify-content:center;padding:.625rem .75rem}.tab-label{display:none}.tab-icon{font-size:1.25rem}.kbd-hint{display:none}.cards-grid{grid-template-columns:1fr}.task-table{font-size:.85rem}.task-header-row,.task-row{grid-template-columns:1fr 80px 40px 80px}.task-header-row .task-cell:nth-child(n+5),.task-row .task-cell:nth-child(n+5){display:none}.filter-bar{flex-direction:column}.keyboard-hints{display:none}.page-title{font-size:1.5rem}.saved-views-sidebar{display:none}.day-plan-content{flex-direction:column}.day-plan-sidebar{width:100%;max-height:200px}.project-dashboard-grid{grid-template-columns:1fr}.project-dashboard-grid .dashboard-column:last-child{grid-column:span 1}.modal-container{width:95%;max-height:95vh}.bulk-actions-bar{flex-wrap:wrap}.bulk-select-all{width:100%;margin-top:.5rem}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.pagination-controls{display:flex;align-items:center;justify-content:center;gap:1rem;padding:1rem;margin-top:1rem;background:var(--bg-card);border:var(--border-width) solid var(--border-color);border-radius:var(--radius-md)}.pagination-info{font-weight:600;color:var(--text-secondary);font-size:.9rem}.pagination-controls .btn:disabled{opacity:.5;cursor:not-allowed}.btn:focus-visible,.card:focus-visible,.dashboard-item:focus-visible,.email-item:focus-visible,.event-row-virtual:focus-visible,.filter-select:focus-visible,.form-input:focus-visible,.form-select:focus-visible,.form-textarea:focus-visible,.modal-close:focus-visible,.saved-view-item:focus-visible,.snooze-option:focus-visible,.tab:focus-visible,.task-row:focus-visible,.timeline-item:focus-visible,.unscheduled-task:focus-visible{outline:3px solid var(--accent-blue);outline-offset:2px}.event-row,.task-row-clickable{cursor:pointer}.skip-link{position:absolute;top:-100px;left:0;background:var(--accent-blue);color:var(--text-on-accent);padding:.75rem 1.5rem;z-index:9999;font-weight:700;border:var(--border-width) solid var(--border-color);text-decoration:none}.skip-link:focus{top:0}.source-email-link{padding:.75rem;background:var(--bg-secondary);border-radius:var(--radius-sm);border-left:4px solid var(--accent-blue)}.thread-message{margin-bottom:1rem;padding:.75rem;background:var(--bg-secondary);border-radius:var(--radius-sm)}.thread-message-latest{border-left:3px solid var(--accent-blue)}.thread-message-header{display:flex;justify-content:space-between;margin-bottom:.5rem;font-size:.8rem;color:var(--text-secondary)}.thread-message-from{font-weight:700}.email-reader-body{white-space:pre-wrap;font-size:.9rem;line-height:1.6;color:var(--text-primary);word-wrap:break-word;overflow-wrap:break-word}.email-reader-body .email-link{color:var(--accent-blue);text-decoration:underline;cursor:pointer;word-break:break-all}.email-reader-body .email-link:hover{color:var(--accent-cyan)}.email-reader-body hr{border:none;border-top:2px solid var(--border-color);margin:1rem 0}.email-reader-quote{border-left:3px solid var(--text-muted);padding-left:1rem;margin:.5rem 0;color:var(--text-secondary);font-style:italic}.email-quote-toggle{display:inline-block;color:var(--text-muted);font-size:.8125rem;cursor:pointer;padding:.25rem 0;user-select:none}.email-quote-toggle:hover{color:var(--accent-blue)}.email-quote-block{border-left:3px solid var(--text-muted);padding-left:1rem;margin:.25rem 0 .5rem;color:var(--text-secondary)}.email-quote-block.hidden{display:none}.autocomplete-dropdown{background:var(--bg-card);border:1px solid var(--border-color);border-radius:var(--radius-sm);box-shadow:var(--shadow-brutal);z-index:100;max-height:200px;overflow-y:auto}.autocomplete-item{padding:.5rem .75rem;cursor:pointer;font-size:.875rem}.autocomplete-item.active,.autocomplete-item:hover{background:var(--bg-secondary)}.autocomplete-name{font-weight:500}.autocomplete-email{color:var(--text-secondary);margin-left:.25rem}.email-label-badge{display:inline-block;font-size:.6875rem;padding:.125rem .375rem;border-radius:var(--radius-sm);background:var(--accent-blue);color:var(--bg-primary);font-weight:600;vertical-align:middle}.email-reader-container{display:flex;flex-direction:column;height:100%;min-height:0}.email-reader-header{margin-bottom:1rem;padding-bottom:.75rem;border-bottom:1px solid var(--border-color)}.email-sender-contact{display:flex;align-items:center;gap:.5rem;margin-top:.5rem;padding:.4rem .5rem;background:var(--bg-tertiary);border-radius:4px}.email-sender-info{display:flex;flex-direction:column;flex:1;min-width:0}.email-sender-name{font-weight:600;font-size:.85rem}.email-sender-company{font-size:.75rem;color:var(--text-secondary)}.contact-avatar-sm{width:32px;height:32px;border-radius:50%;background:var(--accent-color);color:var(--bg-primary);display:flex;align-items:center;justify-content:center;font-size:.75rem;font-weight:700;flex-shrink:0}.contact-avatar-unknown{background:var(--bg-secondary);color:var(--text-secondary);border:var(--border-width-sm) solid var(--border-color)}.email-reader-thread{flex:1;overflow-y:auto;margin-bottom:1rem;min-height:0}.dropdown{position:relative;display:inline-block}.dropdown-menu{display:none;position:absolute;bottom:100%;left:0;margin-bottom:.25rem;background:var(--bg-card);border:var(--border-width) solid var(--border-color);border-radius:var(--radius-md);box-shadow:var(--shadow-brutal-md);min-width:160px;z-index:100}.dropdown-menu.show{display:block}.dropdown-item{display:block;width:100%;padding:.5rem 1rem;text-align:left;background:0 0;border:none;cursor:pointer;font-size:.875rem;color:var(--text-primary)}.dropdown-item:hover{background:var(--bg-secondary)}.dropdown-item:first-child{border-radius:var(--radius-md) var(--radius-md) 0 0}.dropdown-item:last-child{border-radius:0 0 var(--radius-md) var(--radius-md)}.context-menu{position:fixed;z-index:10000;min-width:180px;max-width:280px;background:var(--bg-card);border:var(--border-width) solid var(--border-color);border-radius:var(--radius-sm);box-shadow:var(--shadow-brutal-lg);padding:.25rem 0;display:none}.context-menu.visible{display:block}.context-menu-item{display:flex;align-items:center;gap:.75rem;padding:.5rem 1rem;font-size:.875rem;font-weight:500;color:var(--text-primary);cursor:pointer;border:none;background:0 0;width:100%;text-align:left;transition:background .1s}.context-menu-item:focus,.context-menu-item:hover{background:var(--accent-blue);color:var(--text-on-accent);outline:0}.context-menu-item:focus-visible{outline:2px solid var(--accent-blue);outline-offset:-2px}.context-menu-header{font-size:.7rem;font-weight:700;color:var(--text-secondary);text-transform:uppercase;letter-spacing:.05em;padding:.5rem 1rem .25rem}.context-menu-item-icon{width:1.25rem;text-align:center;flex-shrink:0}.context-menu-item-label{flex:1}.context-menu-item-subtitle{display:block;font-size:.7rem;color:var(--text-secondary);font-weight:400}.context-menu-item-shortcut{font-size:.75rem;color:var(--text-muted);font-family:var(--font-mono)}.context-menu-item--danger{color:var(--accent-red)}.context-menu-item--danger:hover{background:var(--accent-red);color:var(--text-on-accent)}.context-menu-separator{height:2px;background:var(--border-color);margin:.25rem .5rem}.context-menu-hint{padding:.35rem 1rem;font-size:.7rem;color:var(--text-muted);border-top:1px solid var(--border-color);margin-top:.25rem}::-webkit-scrollbar{width:12px;height:12px}::-webkit-scrollbar-track{background:var(--bg-secondary);border-left:2px solid var(--border-color)}::-webkit-scrollbar-thumb{background:var(--text-muted);border:var(--border-width-sm) solid var(--border-color);border-radius:var(--radius-sm)}::-webkit-scrollbar-thumb:hover{background:var(--text-secondary)}.loading{display:flex;justify-content:center;align-items:center;height:200px;color:var(--text-secondary);font-family:var(--font-heading)}.skeleton-shimmer{display:flex;flex-direction:column;gap:1rem;padding:1rem}.skeleton-shimmer .skeleton-row{display:flex;align-items:center;gap:.75rem;padding:.75rem;background:var(--bg-card);border-radius:var(--radius-md);border:var(--border-width) solid var(--border-color)}.skeleton-shimmer .skeleton-avatar{width:36px;height:36px;border-radius:var(--radius-full);background:linear-gradient(90deg,var(--bg-secondary) 25%,var(--bg-tertiary) 50%,var(--bg-secondary) 75%);background-size:200% 100%;animation:skeleton-pulse 1.5s ease-in-out infinite;flex-shrink:0}.skeleton-shimmer .skeleton-lines{flex:1;display:flex;flex-direction:column;gap:.4rem}.skeleton-shimmer .skeleton-line{height:.75rem;border-radius:var(--radius-sm);background:linear-gradient(90deg,var(--bg-secondary) 25%,var(--bg-tertiary) 50%,var(--bg-secondary) 75%);background-size:200% 100%;animation:skeleton-pulse 1.5s ease-in-out infinite}.skeleton-shimmer .skeleton-line.short{width:40%}.skeleton-shimmer .skeleton-line.medium{width:65%}.skeleton-shimmer .skeleton-line.long{width:90%}@keyframes skeleton-pulse{0%{background-position:200% 0}100%{background-position:-200% 0}}@keyframes spin{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}.spinner{display:inline-block;width:1em;height:1em;border:2px solid currentColor;border-top-color:transparent;border-radius:var(--radius-full);animation:spin .8s linear infinite}.btn-loading{position:relative;pointer-events:none;opacity:.8}.btn-loading .btn-text{visibility:hidden}.btn-loading::after{content:'';position:absolute;left:50%;top:50%;width:1em;height:1em;margin-left:-.5em;margin-top:-.5em;border:2px solid currentColor;border-top-color:transparent;border-radius:var(--radius-full);animation:spin .8s linear infinite}.hidden{display:none!important}.project-dashboard-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:1.5rem;flex:1;min-height:0}.dashboard-column{background:var(--bg-card);border:var(--border-width) solid var(--border-color);padding:1rem;display:flex;flex-direction:column;overflow:hidden}.dashboard-column-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;padding-bottom:.5rem;border-bottom:2px solid var(--border-color)}.dashboard-column-header h3{margin:0;font-size:1rem;font-family:var(--font-heading);font-weight:700}.dashboard-list{flex:1;overflow-y:auto}.dashboard-item{padding:.75rem;background:var(--bg-card);border:var(--border-width-sm) solid var(--border-color);margin-bottom:.5rem;cursor:pointer;box-shadow:var(--shadow-offset) var(--shadow-offset) 0 var(--border-color);transition:transform .15s ease,box-shadow .15s ease,background-color .15s ease}.dashboard-item:hover{background:var(--bg-secondary);transform:translate(-2px,-2px);box-shadow:calc(var(--shadow-offset) + 2px) calc(var(--shadow-offset) + 2px) 0 var(--border-color)}.dashboard-item-title{font-weight:600;margin-bottom:.25rem}.dashboard-item-meta{font-size:.75rem;color:var(--text-secondary)}.empty-dashboard-list{text-align:center;padding:2rem 1rem;color:var(--text-secondary)}.task-badges{display:flex;gap:.25rem;margin-top:.25rem}.task-badge{font-size:.65rem;padding:.15rem .4rem;border:var(--border-width-sm) solid var(--border-color);background:var(--bg-secondary);color:var(--text-primary);font-weight:600}.task-badge.has-items{background:var(--accent-blue);color:var(--text-on-accent)}.task-badge.recurrence{background:var(--accent-purple);color:var(--text-on-accent)}.task-row-clickable{cursor:pointer;transition:background .1s}.task-row-clickable:hover{background:var(--bg-secondary)}.progress-bar-container{width:100%;height:10px;background:var(--bg-secondary);border:var(--border-width-sm) solid var(--border-color);overflow:hidden}.progress-bar{height:100%;background:var(--accent-green);transition:width .3s ease}.no-subtasks{color:var(--text-secondary);font-size:.875rem}#day-plan-view{display:flex;flex-direction:column;flex:1;min-height:0}#day-plan-view .page-header{flex-shrink:0}.day-plan-nav{display:flex;align-items:center;gap:.5rem}.day-plan-date-picker{padding:.5rem;background:var(--bg-card);border:var(--border-width) solid var(--border-color);border-radius:var(--radius-sm);color:var(--text-primary);font-family:var(--font-body)}.day-plan-date-display{font-size:1.25rem;font-weight:700;margin-left:1rem;font-family:var(--font-heading);line-height:1}.day-plan-content{flex:1;min-height:0;display:flex;gap:1.5rem}.day-plan-main{flex:1;min-height:0;display:flex;flex-direction:column;background:var(--bg-card);border:var(--border-width) solid var(--border-color);border-radius:var(--radius-md);overflow:hidden;box-shadow:var(--shadow-offset) var(--shadow-offset) 0 var(--border-color)}.day-plan-sidebar{width:280px;flex-shrink:0;display:flex;flex-direction:column;background:var(--bg-card);border:var(--border-width) solid var(--border-color);border-radius:var(--radius-md);overflow:hidden;box-shadow:var(--shadow-offset) var(--shadow-offset) 0 var(--border-color)}.sidebar-header{padding:1rem;border-bottom:2px solid var(--border-color);flex-shrink:0}.sidebar-header h3{margin:0;font-size:1rem;font-family:var(--font-heading);font-weight:700}.sidebar-task-list{flex:1;overflow-y:auto;padding:.75rem;display:flex;flex-direction:column;gap:.5rem}.timeline-container{flex:1;min-height:0;overflow-y:auto;overflow-x:hidden}.timeline-scroll-area{position:relative;padding:.5rem 1rem 3rem .5rem;min-height:min-content}#timeline-slots{position:relative}#timeline-items{position:absolute;top:.5rem;left:.5rem;right:1rem;bottom:0;pointer-events:none}#timeline-items .timeline-item{pointer-events:auto}.timeline-slot{display:grid;grid-template-columns:50px 1fr;height:var(--timeline-slot-h,12px);position:relative}.timeline-slot.hour-start .timeline-slot-area{border-top:1px dashed color-mix(in srgb,var(--border-color) 50%,transparent)}.timeline-time{font-size:.7rem;color:var(--text-secondary);padding-right:.5rem;text-align:right;font-weight:500;transform:translateY(-.5em)}.timeline-hint{text-align:center;color:var(--text-secondary);font-size:.85rem;font-weight:600;margin:0 0 .5rem;padding:.35rem .75rem;background:var(--bg-secondary);border-radius:var(--radius-sm)}.timeline-slot-area{position:relative;cursor:grab}.timeline-slot-area:hover{background:var(--bg-secondary)}.timeline-item{position:absolute;left:60px;right:10px;border:var(--border-width) solid var(--border-color);border-radius:var(--radius-sm);padding:.25rem .5rem;overflow:hidden;cursor:grab;z-index:10;transition:opacity .15s ease,box-shadow .15s ease}.timeline-item.task{background:var(--accent-green);color:var(--text-primary)}.timeline-item.event{background:var(--accent-blue);color:var(--text-on-accent)}.timeline-item.block{opacity:.85}.timeline-item.block-free_time{background:var(--accent-cyan);color:var(--text-primary)}.timeline-item.block-personal{background:var(--accent-yellow);color:var(--text-primary)}.timeline-item.block-vacation{background:var(--accent-purple);color:var(--text-on-accent)}.timeline-item.block-focus{background:var(--accent-red);color:var(--text-on-accent)}.timeline-item.conflict{box-shadow:0 0 0 3px var(--accent-red)}.timeline-item.selected{box-shadow:0 0 0 3px var(--bg-card),0 0 0 6px var(--accent-blue)}.timeline-item-title{font-weight:600;font-size:.75rem;line-height:1.2;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.timeline-item-meta{font-size:.65rem;opacity:.85;line-height:1.1}.timeline-current-time{position:absolute;left:50px;right:0;height:2px;background:var(--accent-red);z-index:20;pointer-events:none}.timeline-current-time::before{content:'';position:absolute;left:-4px;top:-3px;width:8px;height:8px;background:var(--accent-red);border-radius:var(--radius-full)}.timeline-paint-preview{position:absolute;left:70px;right:10px;background:var(--accent-blue);opacity:.4;border:var(--border-width-sm) dashed var(--border-color);border-radius:var(--radius-sm);z-index:5;pointer-events:none}.timeline-item.dragging{cursor:grabbing;opacity:.8;z-index:100;box-shadow:var(--shadow-brutal-md,4px 4px 0 var(--border-color))}.timeline-container.is-painting{cursor:crosshair;user-select:none}.timeline-container.is-painting .timeline-slot-area{pointer-events:none}.unscheduled-task{padding:.75rem;background:var(--bg-card);border:var(--border-width-sm) solid var(--border-color);border-left:6px solid var(--accent-green);border-radius:var(--radius-sm);cursor:grab;transition:background-color .1s}.unscheduled-task:hover{background:var(--bg-secondary)}.unscheduled-task.priority-high{border-left-color:var(--accent-red)}.unscheduled-task.priority-medium{border-left-color:var(--accent-yellow)}.unscheduled-task.priority-low{border-left-color:var(--accent-green)}.unscheduled-task-title{font-weight:600;margin-bottom:.25rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.unscheduled-task-meta{font-size:.75rem;color:var(--text-secondary)}.empty-unscheduled{text-align:center;color:var(--text-secondary);padding:2rem 1rem}.settings-btn{background:var(--bg-card);border:var(--border-width) solid var(--border-color);border-radius:var(--radius-sm);color:var(--text-primary);font-size:1.25rem;cursor:pointer;padding:.5rem .75rem;margin-left:.5rem;transition:background-color .1s}.settings-btn:hover{background:var(--bg-secondary)}.settings-btn:active{background:var(--bg-tertiary)}.shortcut-hint-btn{font-family:var(--font-mono, monospace);font-weight:700;min-width:2rem;text-align:center;padding:.5rem}.kbd-hint{display:inline-block;font-family:var(--font-mono, monospace);font-size:.65rem;font-weight:600;padding:.1rem .35rem;margin-left:.35rem;border:1px solid currentColor;border-radius:3px;opacity:.6;vertical-align:middle;line-height:1}.settings-section h3{font-size:1rem;color:var(--text-primary)}.settings-section .form-hint{font-size:.75rem;color:var(--text-secondary)}.settings-page-layout{display:flex;min-height:100%}.settings-sidebar{width:200px;flex-shrink:0;border-right:1px solid var(--border-color);padding:1.5rem 0;display:flex;flex-direction:column;gap:.25rem}.settings-back-btn{background:0 0;border:none;text-align:left;padding:.5rem 1.25rem;font-size:.875rem;color:var(--text-secondary);cursor:pointer;margin-bottom:1rem}.settings-back-btn:hover{color:var(--text-primary)}.settings-nav-items{display:flex;flex-direction:column;gap:.125rem}.settings-nav-item{background:0 0;border:none;text-align:left;padding:.5rem 1.25rem;font-size:.875rem;color:var(--text-secondary);cursor:pointer;border-radius:0;border-left:2px solid transparent}.settings-nav-item:hover{background:var(--bg-hover);color:var(--text-primary)}.settings-nav-item.active{color:var(--text-primary);font-weight:600;border-left-color:var(--accent-primary);background:var(--bg-secondary)}.settings-content{flex:1;padding:1.5rem 2rem;max-width:640px}.contact-header-card{display:flex;align-items:center;gap:1.25rem;margin-bottom:1.5rem}.contact-avatar-large{width:64px;height:64px;border-radius:50%;background:var(--accent-primary);color:#fff;display:flex;align-items:center;justify-content:center;font-size:1.5rem;font-weight:700;flex-shrink:0}.contact-info-section{display:flex;flex-direction:column;gap:.375rem;margin-bottom:1.5rem;padding:1rem;background:var(--bg-secondary);border-radius:var(--radius-md)}.contact-info-item{font-size:.875rem}.contact-dashboard-summary{display:flex;gap:1.5rem;margin-bottom:1.5rem}.contact-summary-stat{display:flex;flex-direction:column;align-items:center;padding:.75rem 1.25rem;background:var(--bg-secondary);border-radius:var(--radius-md);min-width:80px}.contact-summary-count{font-size:1.5rem;font-weight:700;color:var(--text-primary)}.contact-summary-label{font-size:.75rem;color:var(--text-secondary);margin-top:.25rem}.contact-timeline{display:flex;flex-direction:column;gap:.25rem}.contact-timeline-item{display:flex;align-items:center;gap:.75rem;padding:.5rem .75rem;border-radius:var(--radius-sm);cursor:pointer;font-size:.875rem}.contact-timeline-item:hover{background:var(--bg-hover)}.contact-timeline-icon{flex-shrink:0;width:2rem;text-align:center;font-size:.8rem}.contact-timeline-title{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.contact-timeline-date{flex-shrink:0;font-size:.75rem;color:var(--text-secondary)}.address-highlight-mirror{position:absolute;top:0;left:0;right:0;bottom:0;pointer-events:none;white-space:nowrap;overflow:hidden;z-index:0;display:flex;align-items:center}.addr-malformed{color:var(--accent-red)}.addr-contact{color:var(--accent-blue)}.addr-verified{color:var(--accent-green)}.addr-ghost{color:var(--text-muted);opacity:.5}.sync-indicator{background:0 0;border:none;cursor:pointer;padding:.25rem .5rem;display:flex;align-items:center}.sync-dot{width:8px;height:8px;border-radius:var(--radius-full);background:var(--text-muted);transition:background var(--transition-slow)}.sync-dot.connected{background:var(--accent-green)}.sync-dot.syncing{background:var(--accent-blue);animation:sync-pulse 1s infinite}.sync-dot.error{background:var(--accent-red)}@keyframes sync-pulse{0%,100%{opacity:1}50%{opacity:.4}}.snooze-options{display:flex;flex-direction:column;gap:.5rem}.snooze-option{display:flex;justify-content:space-between;align-items:center;padding:.75rem 1rem;background:var(--bg-secondary);border:var(--border-width-sm) solid var(--border-color);color:var(--text-primary);cursor:pointer;transition:background-color .1s;text-align:left;width:100%}.snooze-option:hover{background:var(--accent-blue);color:var(--text-on-accent)}.snooze-option-label{font-weight:600}.snooze-option-time{font-size:.75rem;color:var(--text-secondary)}.snooze-option:hover .snooze-option-time{color:var(--text-on-accent)}.snooze-custom{margin-top:.5rem;padding-top:.5rem;border-top:2px solid var(--border-color)}.snooze-badge{display:inline-block;font-size:.65rem;padding:.15rem .4rem;border:var(--border-width-sm) solid var(--border-color);background:var(--accent-yellow);color:var(--text-primary);font-weight:700;margin-top:.25rem}.contact-badge{display:inline-block;font-size:.65rem;padding:.15rem .4rem;border:var(--border-width-sm) solid var(--border-color);background:var(--accent-color);color:var(--bg-primary);font-weight:700;margin-top:.25rem}.bulk-checkbox{width:18px;height:18px;cursor:pointer;accent-color:var(--accent-blue);border:var(--border-width-sm) solid var(--border-color)}.task-actions-cell{text-align:right;white-space:nowrap;display:flex;align-items:center;justify-content:flex-end;gap:.5rem}.task-actions-cell .bulk-checkbox{margin-right:.5rem}.kebab-btn{background:0 0;border:none;cursor:pointer;font-size:1.1rem;line-height:1;padding:.2rem .4rem;border-radius:var(--radius-sm);color:var(--text-secondary);opacity:0;transition:opacity .15s ease}.email-item:focus-within .kebab-btn,.email-item:hover .kebab-btn,.event-row-virtual:focus-within .kebab-btn,.event-row-virtual:hover .kebab-btn,.task-row:focus-within .kebab-btn,.task-row:hover .kebab-btn{opacity:1}.kebab-btn:hover{background:var(--bg-hover);color:var(--text-primary)}.task-recurrence{font-size:.85rem;color:var(--text-secondary)}.task-due{white-space:nowrap}.bulk-actions-bar{display:flex;align-items:center;gap:.5rem;padding:.75rem 1rem;background:var(--accent-blue);color:var(--text-on-accent);border:var(--border-width) solid var(--border-color);box-shadow:var(--shadow-offset) var(--shadow-offset) 0 var(--border-color);margin-bottom:1rem;color:var(--text-primary)}.bulk-actions-bar.hidden{display:none}.bulk-count{font-weight:700;margin-right:1rem;font-family:var(--font-heading)}.bulk-actions-bar .btn{background:var(--bg-card);border:var(--border-width-sm) solid var(--border-color);color:var(--text-primary)}.bulk-actions-bar .btn:hover{background:var(--bg-secondary)}.bulk-select-all{margin-left:auto}.email-checkbox-cell{padding:.75rem .5rem;display:flex;align-items:center}.email-item-with-checkbox{display:flex;align-items:flex-start}.email-item-with-checkbox .email-content{flex:1}.schedule-task-btn{display:flex;align-items:center;gap:.5rem}.time-block-form{display:flex;flex-direction:column;gap:1rem}.time-block-quick-options{display:grid;grid-template-columns:repeat(3,1fr);gap:.5rem}.time-block-quick-btn{padding:.5rem;background:var(--bg-secondary);border:var(--border-width-sm) solid var(--border-color);color:var(--text-primary);cursor:pointer;font-size:.875rem;font-weight:600;transition:background-color .1s}.time-block-quick-btn:hover{background:var(--bg-tertiary)}.time-block-quick-btn.selected{background:var(--accent-blue);color:var(--text-on-accent);box-shadow:inset 0 0 0 2px var(--border-color)}.duration-presets{display:flex;gap:.5rem;flex-wrap:wrap}.duration-preset{padding:.35rem .75rem;background:var(--bg-secondary);border:var(--border-width-sm) solid var(--border-color);color:var(--text-primary);cursor:pointer;font-size:.75rem;font-weight:600;transition:background-color .1s}.duration-preset:hover{background:var(--bg-tertiary)}.duration-preset.selected{background:var(--accent-blue);color:var(--text-on-accent)}.conflict-warning{padding:.75rem;background:var(--accent-red);border:var(--border-width) solid var(--border-color);color:var(--text-on-accent);font-size:.875rem;font-weight:600;margin-top:.5rem}.app-body{display:flex;flex:1;min-height:0;overflow:hidden}.app-body .main-content{flex:1;min-width:0;display:flex;flex-direction:column;overflow-x:visible;overflow-y:auto}#emails-view,#events-view,#projects-view,#tasks-view{padding-bottom:2.5rem}#tasks-view{display:flex;flex-direction:column;flex:1;min-height:0}#tasks-view .bulk-actions-bar,#tasks-view .filter-bar,#tasks-view .page-header{flex-shrink:0}#events-view{display:flex;flex-direction:column;flex:1;min-height:0}#events-view .page-header{flex-shrink:0}#emails-view{display:flex;flex-direction:column;flex:1;min-height:0}#emails-view .bulk-actions-bar,#emails-view .page-header{flex-shrink:0}.saved-views-sidebar{width:200px;flex-shrink:0;background:var(--bg-card);border-right:var(--border-width) solid var(--border-color);display:flex;flex-direction:column;overflow:hidden}.sidebar-section{display:flex;flex-direction:column;flex:1;min-height:0}.sidebar-section-header{display:flex;justify-content:space-between;align-items:center;padding:.75rem 1rem;font-size:.75rem;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--text-secondary);border-bottom:2px solid var(--border-color);background:var(--bg-secondary)}.btn-icon{background:0 0;border:none;color:var(--text-muted);cursor:pointer;padding:.25rem;font-size:.875rem;line-height:1}.btn-icon:hover{color:var(--text-primary)}.pinned-views-list{flex:1;overflow-y:auto;padding:.5rem}.sidebar-empty{text-align:center;padding:1.5rem .5rem;color:var(--text-muted);font-size:.8rem}.saved-view-item{display:flex;align-items:center;gap:.5rem;padding:.5rem .75rem;margin-bottom:.5rem;background:var(--bg-card);border:var(--border-width-sm) solid var(--border-color);border-radius:var(--radius-sm);cursor:pointer;font-size:.85rem;font-weight:600;color:var(--text-primary);box-shadow:var(--shadow-offset) var(--shadow-offset) 0 var(--border-color);transition:transform .15s ease,box-shadow .15s ease,background-color .15s ease,color .15s ease}.saved-view-item:hover{background:var(--accent-blue);color:var(--text-on-accent)}.saved-view-item.active{background:var(--accent-blue);color:var(--text-on-accent);box-shadow:inset 0 0 0 2px var(--border-color)}.saved-view-item .view-icon{font-size:.75rem}.saved-view-item .view-name{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.saved-view-item .view-actions{opacity:0;transition:opacity .1s}.saved-view-item:hover .view-actions{opacity:1}.filter-actions{display:flex;gap:.5rem;margin-left:auto}.contact-avatar{width:40px;height:40px;min-width:40px;border-radius:50%;background-color:var(--accent-blue);color:var(--text-on-accent);display:flex;align-items:center;justify-content:center;font-weight:700;font-size:.85rem;font-family:var(--font-heading);border:2px solid var(--border-color)}.contact-avatar-lg{width:60px;height:60px;min-width:60px;font-size:1.2rem}.contact-card .card-header{display:flex;align-items:center}.contact-nickname{display:block;font-size:.85rem;color:var(--text-secondary);font-style:italic}.contact-company{display:block;font-size:.85rem;color:var(--text-secondary)}.contact-email{font-size:.85rem;color:var(--text-secondary)}.contact-detail .detail-row{margin-bottom:.5rem;font-size:.9rem}.contact-detail .contact-info-section{margin-bottom:1rem;padding-bottom:1rem;border-bottom:1px solid var(--border-light,#e0e0e0)}.contact-detail .contact-notes{margin-bottom:1.5rem}.contact-detail .contact-notes p{margin-top:.25rem;white-space:pre-wrap;color:var(--text-secondary)}.sub-collection{margin-bottom:1.25rem}.sub-collection-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:.5rem}.sub-collection-header h4{margin:0;font-size:.95rem;font-weight:600}.sub-item{display:flex;justify-content:space-between;align-items:center;padding:.4rem 0;border-bottom:1px solid var(--border-light,#e0e0e0);font-size:.9rem}.sub-item:last-child{border-bottom:none}.sub-empty{font-size:.85rem;color:var(--text-secondary);font-style:italic;padding:.25rem 0}.edit-sub-collections{border-top:1px solid var(--border-color);padding-top:1rem;margin-bottom:.5rem}.edit-sub-section{margin-bottom:.75rem}.edit-sub-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:.25rem}.sub-item-compact{font-size:.85rem;color:var(--text-secondary);padding:.125rem 0}@media print{.btn,.context-menu,.filter-bar,.keyboard-hints,.modal-overlay,.pagination,.sidebar,.tabs,.toast{display:none!important}body{background:#fff;color:#000}.main-content{margin:0;padding:0;max-width:100%}.view{padding:0}.data-table{border:1px solid #333;box-shadow:none}.data-table td,.data-table th{border:1px solid #ccc;padding:.5rem}.data-table td,.data-table th{display:table-cell!important}.data-table tbody tr:hover{background:0 0}.task-table{border:1px solid #333;box-shadow:none}.task-list-container{height:auto!important;overflow:visible!important}.task-header-row,.task-row{grid-template-columns:1fr 100px 40px 80px 60px 80px 60px!important}.task-header-row .task-cell,.task-row .task-cell{display:block!important;border:1px solid #ccc;padding:.25rem .5rem}.task-row:hover{background:0 0}.virtual-scroller-spacer-bottom,.virtual-scroller-spacer-top{display:none!important}a{color:#000;text-decoration:underline}.view-header{page-break-after:avoid}.data-table{page-break-inside:avoid}}.weekly-review-content{max-width:900px;margin:0 auto;padding:1rem}.weekly-review-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:1.5rem;padding-bottom:1rem;border-bottom:var(--border-width-sm) solid var(--border-color)}.week-info{display:flex;align-items:center;gap:1rem}.week-dates{font-family:var(--font-heading);font-size:1.25rem;font-weight:700;color:var(--text-primary)}.review-status{padding:.25rem .75rem;border-radius:var(--radius-xs);font-size:.875rem;font-weight:600;border:var(--border-width-sm) solid var(--border-color)}.review-status.completed{background:var(--accent-green);color:var(--text-on-accent)}.review-status.pending{background:var(--accent-yellow);color:var(--text-primary)}.stat-cards{display:flex;gap:1rem;margin-bottom:1rem;flex-wrap:wrap}.stat-card{flex:1;min-width:100px;max-width:150px;padding:1rem;background:var(--bg-card);border:var(--border-width) solid var(--border-color);text-align:center;box-shadow:var(--shadow-offset) var(--shadow-offset) 0 var(--border-color)}.stat-card .stat-number{display:block;font-family:var(--font-heading);font-size:2rem;font-weight:700;color:var(--accent-blue);line-height:1}.stat-card .stat-label{display:block;font-size:.75rem;font-weight:600;color:var(--text-muted);margin-top:.25rem;text-transform:uppercase;letter-spacing:.5px}.stat-card.stat-warning .stat-number{color:var(--accent-yellow)}.stat-card.stat-danger .stat-number{color:var(--accent-red)}.review-section{background:var(--bg-card);border:var(--border-width) solid var(--border-color);padding:1.25rem;margin-bottom:1.5rem;box-shadow:var(--shadow-offset) var(--shadow-offset) 0 var(--border-color)}.section-title{font-family:var(--font-heading);font-size:1.125rem;font-weight:700;color:var(--text-primary);margin-bottom:1rem;padding-bottom:.5rem;border-bottom:var(--border-width-sm) solid var(--border-color)}.review-details{margin-top:.75rem}.review-details summary{cursor:pointer;font-weight:600;color:var(--text-secondary);padding:.5rem;background:var(--bg-secondary);border:var(--border-width-sm) solid var(--border-color);user-select:none}.review-details summary:hover{background:var(--bg-tertiary)}.review-details[open] summary{margin-bottom:.5rem}.review-event-list,.review-task-list{list-style:none;padding:0;margin:0}.review-event-item,.review-task-item{display:flex;align-items:center;gap:.75rem;padding:.5rem .75rem;border-bottom:1px solid var(--border-color)}.review-event-item:last-child,.review-task-item:last-child{border-bottom:none}.review-event-item .event-title,.review-task-item .task-description{flex:1;color:var(--text-primary)}.event-time{font-size:.875rem;font-weight:600;color:var(--text-muted);min-width:80px}.project-badge{font-size:.75rem;padding:.125rem .5rem;background:var(--bg-tertiary);border:1px solid var(--border-color);color:var(--text-secondary)}.due-badge{font-size:.75rem;padding:.125rem .5rem;background:var(--bg-secondary);border:1px solid var(--border-color);color:var(--text-secondary)}.due-badge.overdue{background:var(--accent-red);color:var(--text-on-accent);border-color:var(--accent-red)}.focus-section{background:linear-gradient(135deg,var(--bg-card) 0,color-mix(in srgb,var(--accent-yellow) 15%,var(--bg-card)) 100%)}.focus-task-list{list-style:none;padding:0;margin:0 0 1rem 0}.focus-task-list.available{opacity:.8}.focus-toggle{background:0 0;border:none;font-size:1.25rem;cursor:pointer;color:var(--text-muted);padding:0;line-height:1;transition:transform .15s ease}.focus-toggle:hover{transform:scale(1.2)}.focus-toggle.focused{color:var(--accent-yellow)}.review-task-item.focused{background:color-mix(in srgb,var(--accent-yellow) 10%,var(--bg-card))}.no-focus-message{color:var(--text-muted);font-style:italic;margin-bottom:1rem}.focused-projects{display:flex;gap:.5rem;flex-wrap:wrap;margin-bottom:1rem}.project-tag{background:var(--accent-blue);color:var(--text-on-accent);padding:.25rem .75rem;font-size:.875rem;font-weight:600;border:var(--border-width-sm) solid var(--border-color)}.notes-section{background:var(--bg-card)}.review-notes-input{width:100%;padding:.75rem;font-family:var(--font-mono);font-size:.9rem;border:var(--border-width-sm) solid var(--border-color);background:var(--bg-secondary);color:var(--text-primary);resize:vertical;min-height:100px}.review-notes-input:focus{outline:0;background:var(--bg-card);box-shadow:inset 0 0 0 2px var(--accent-blue)}.review-actions{margin-top:1rem;text-align:center}.tab-badge{display:inline-block;width:8px;height:8px;background:var(--accent-red);border-radius:var(--radius-full);margin-left:.5rem;vertical-align:middle;animation:pulse-badge 2s infinite}@keyframes pulse-badge{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.6;transform:scale(.8)}}.tab-status-dot{width:8px;height:8px;border-radius:50%;display:inline-block;margin-left:.5rem;vertical-align:middle;transition:background-color .3s ease}.tab-status-dot.status-none{display:none}.tab-status-dot.status-green{background-color:var(--accent-green)}.tab-status-dot.status-yellow{background-color:var(--accent-yellow);animation:pulse-badge 2s ease-in-out infinite}.tab-status-dot.status-red{background-color:var(--accent-red);animation:pulse-badge 1.5s ease-in-out infinite}.review-grid{display:grid;grid-template-columns:1fr 1fr;gap:1.5rem;max-width:1200px;margin:0 auto}.review-card{background:var(--bg-card);border:var(--border-width) solid var(--border-color);border-radius:var(--radius-md);padding:1.5rem;box-shadow:var(--shadow-offset) var(--shadow-offset) 0 var(--border-color)}.review-card .card-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;padding-bottom:.75rem;border-bottom:var(--border-width-sm) solid var(--bg-secondary)}.review-card .card-title{font-family:var(--font-heading);font-size:1.1rem;font-weight:700;display:flex;align-items:center;gap:.5rem}.review-card .card-icon{font-size:1.25rem}.review-card .card-badge{font-size:.8rem;padding:.25rem .75rem;border-radius:var(--radius-md);font-weight:600}.week-timeline{grid-column:1/-1}.timeline-visual{display:flex;gap:.5rem;margin-top:1rem}.timeline-day{flex:1;text-align:center;padding:.75rem .5rem;background:var(--bg-secondary);border-radius:var(--radius-md);border:1px solid var(--border-color);position:relative}.timeline-day.today{background:var(--accent-blue);color:var(--text-on-accent);border-width:2px;font-weight:700}.timeline-day.past{opacity:.7}.timeline-day.future{background:var(--bg-card)}.timeline-day .day-name{font-size:.7rem;font-weight:600;text-transform:uppercase;color:var(--text-muted)}.timeline-day .day-number{font-size:1.1rem;font-weight:700}.day-dots{display:flex;justify-content:center;gap:3px;margin-top:.5rem;min-height:8px}.day-dot{width:8px;height:8px;border-radius:var(--radius-full)}.day-dot.task{background:var(--accent-blue)}.day-dot.event{background:var(--accent-purple)}.day-dot.completed{background:var(--accent-green)}.day-dot.overdue{background:var(--accent-red)}.day-dot.vacation-off{background:var(--text-muted);opacity:.5;width:12px;height:4px;border-radius:2px}.day-events{display:flex;flex-direction:column;gap:2px;margin-top:.5rem;text-align:left}.day-event{font-size:.6rem;line-height:1.3;padding:1px 4px;border-left:2px solid var(--accent-purple);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--text-secondary)}.day-event .event-time{font-size:.55rem;font-weight:600;color:var(--accent-purple);margin-right:2px;min-width:auto}.day-event-more{font-size:.55rem;color:var(--text-muted);padding:1px 4px;font-style:italic}.week-timeline-events{grid-column:1/-1}.timeline-events-day{margin-bottom:.75rem}.timeline-events-day:last-child{margin-bottom:0}.timeline-events-day-label{font-family:var(--font-heading);font-size:.8rem;font-weight:700;color:var(--text-secondary);margin-bottom:.25rem;text-transform:uppercase}.vacation-toggles-section{margin-top:1rem;padding-top:1rem;border-top:2px solid var(--border-color)}.vacation-toggles-section h3{margin:0 0 .75rem 0;font-size:.9rem;font-family:var(--font-heading);font-weight:700}.vacation-toggles{display:flex;gap:.5rem}.vacation-toggle{width:2.5rem;height:2.5rem;border-radius:var(--radius-sm);border:var(--border-width) solid var(--border-color);background:var(--bg-secondary);font-family:var(--font-heading);font-weight:700;font-size:.8rem;cursor:pointer;transition:background var(--transition-fast),color var(--transition-fast),border-color var(--transition-fast);display:flex;align-items:center;justify-content:center}.vacation-toggle:hover{background:var(--bg-hover)}.vacation-toggle.active{background:var(--accent-purple);color:var(--text-on-accent);border-color:var(--accent-purple)}.timeline-day.vacation{opacity:.5}.timeline-day.vacation .day-name{text-decoration:line-through}.vacation-day-banner{text-align:center;padding:.5rem 1rem;background:color-mix(in srgb,var(--accent-purple) 15%,var(--bg-secondary));border:var(--border-width-sm) solid var(--accent-purple);border-radius:var(--radius-sm);font-family:var(--font-heading);font-weight:700;font-size:.85rem;color:var(--accent-purple);margin-bottom:.75rem}.stats-row{display:flex;gap:1rem;margin-bottom:1rem}.stat-box{flex:1;text-align:center;padding:1rem;background:var(--bg-secondary);border-radius:var(--radius-md)}.stat-box .stat-number{font-family:var(--font-heading);font-size:2rem;font-weight:800;line-height:1}.stat-box .stat-number.green{color:var(--accent-green)}.stat-box .stat-number.red{color:var(--accent-red)}.stat-box .stat-number.blue{color:var(--accent-blue)}.stat-box .stat-number.purple{color:var(--accent-purple)}.stat-box .stat-label{font-size:.75rem;text-transform:uppercase;color:var(--text-muted);font-weight:600;margin-top:.25rem}.task-list{list-style:none;max-height:200px;overflow-y:auto}.task-item{display:flex;align-items:center;gap:.75rem;padding:.75rem;margin-bottom:.5rem;background:var(--bg-secondary);border-radius:var(--radius-md);cursor:pointer;transition:background-color var(--transition-normal)}.task-item:hover{background:var(--accent-blue);color:var(--text-on-accent)}.task-item.completed{opacity:.6;text-decoration:line-through}.task-checkbox{width:20px;height:20px;border:2px solid var(--border-color);border-radius:var(--radius-xs);display:flex;align-items:center;justify-content:center;flex-shrink:0}.task-checkbox.checked{background:var(--accent-green);color:var(--text-on-accent)}.task-text{flex:1;font-size:.9rem}.task-project{font-size:.75rem;padding:.2rem .5rem;background:var(--bg-card);border-radius:var(--radius-xs);color:var(--text-muted)}.task-due{font-size:.75rem;color:var(--text-muted)}.task-due.overdue{color:var(--accent-red);font-weight:600}.focus-section.full-width{grid-column:1/-1}.focus-grid{display:grid;grid-template-columns:1fr 1fr 1fr;gap:1rem;margin-top:1rem}.focus-slot{padding:1.25rem;background:var(--bg-secondary);border:2px dashed var(--border-color);border-radius:var(--radius-md);min-height:100px;display:flex;flex-direction:column;gap:.5rem}.focus-slot.filled{border-style:solid;background:var(--bg-card)}.focus-slot.primary{border-color:var(--accent-yellow);background:linear-gradient(135deg,var(--bg-card) 0,color-mix(in srgb,var(--accent-yellow) 10%,var(--bg-card)) 100%)}.focus-label{font-size:.7rem;text-transform:uppercase;color:var(--text-muted);font-weight:600}.focus-task{font-weight:600;font-size:.95rem}.focus-meta{font-size:.8rem;color:var(--text-secondary)}.focus-empty{color:var(--text-muted);font-style:italic;font-size:.9rem}.projects-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:.75rem;margin-top:.5rem}.project-health{padding:.75rem;background:var(--bg-secondary);border-radius:var(--radius-md);border-left:4px solid var(--accent-blue)}.project-health.warning{border-left-color:var(--accent-yellow)}.project-health.danger{border-left-color:var(--accent-red)}.project-name{font-weight:600;font-size:.85rem;margin-bottom:.25rem}.project-stats{font-size:.75rem;color:var(--text-muted)}.reflection-section{grid-column:1/-1}.reflection-prompts{display:grid;grid-template-columns:1fr 1fr;gap:1rem;margin-top:1rem}.reflection-prompt{padding:1rem;background:var(--bg-secondary);border-radius:var(--radius-md)}.prompt-label{font-size:.8rem;font-weight:600;color:var(--text-secondary);margin-bottom:.5rem}.prompt-input{width:100%;padding:.75rem;border:var(--border-width-sm) solid var(--border-color);border-radius:var(--radius-md);font-size:.9rem;font-family:inherit;resize:none;background:var(--bg-card)}.prompt-input:focus{outline:0;border-color:var(--accent-blue)}.review-actions-grid{grid-column:1/-1;display:flex;justify-content:flex-end;gap:1rem;padding-top:1rem}.event-item{display:flex;align-items:center;gap:.75rem;padding:.75rem;margin-bottom:.5rem;background:var(--bg-secondary);border-radius:var(--radius-md);border-left:3px solid var(--accent-purple)}.event-item .event-time{font-size:.8rem;font-weight:600;color:var(--accent-purple);min-width:100px}.event-item .event-title{flex:1;font-size:.9rem}.accomplishment-highlight{background:linear-gradient(135deg,color-mix(in srgb,var(--accent-green) 10%,var(--bg-card)) 0,color-mix(in srgb,var(--accent-green) 5%,var(--bg-card)) 100%);border:2px solid var(--accent-green);padding:1rem;border-radius:var(--radius-md);margin-bottom:1rem;display:flex;align-items:center;gap:1rem}.accomplishment-icon{font-size:2rem}.accomplishment-text{font-size:1rem}.accomplishment-text strong{color:var(--accent-green)}.task-list::-webkit-scrollbar{width:6px}.task-list::-webkit-scrollbar-track{background:var(--bg-secondary);border-radius:var(--radius-xs)}.task-list::-webkit-scrollbar-thumb{background:var(--border-color);border-radius:var(--radius-xs)}@media (max-width:900px){.review-grid{grid-template-columns:1fr}.focus-section.full-width,.reflection-section,.week-timeline,.week-timeline-events{grid-column:1}.focus-grid{grid-template-columns:1fr}.reflection-prompts{grid-template-columns:1fr}.projects-grid{grid-template-columns:1fr 1fr}}@media (max-width:600px){.stat-cards{flex-direction:column}.stat-card{max-width:none}.week-info{flex-direction:column;align-items:flex-start;gap:.5rem}.projects-grid{grid-template-columns:1fr}}.focus-slot{transition:background-color .2s ease-out,border-color .2s ease-out}.focus-slot.filled{animation:focusSlotFill .3s ease-out}@keyframes focusSlotFill{0%{transform:scale(.95);opacity:.7}100%{transform:scale(1);opacity:1}}.focus-slot:focus,.focus-slot:focus-within{outline:2px solid var(--accent-blue);outline-offset:2px}.focus-slot[tabindex]:focus{outline:2px solid var(--accent-blue);outline-offset:2px}.focus-section .btn{transition:transform .15s ease-out,opacity .15s ease-out}.focus-section .btn:active{transform:scale(.97)}@media print{.card-badge,.focus-section .btn,.focus-slot .btn,.header,.review-actions-grid,.sidebar,.tab-badge,.tab-nav,.tab-status-dot{display:none!important}.main-content,.weekly-review-content{margin:0;padding:0;width:100%;max-width:100%}.event-item,.focus-slot,.project-health,.reflection-prompt,.review-card,.weekly-review-content,body{background:#fff!important;color:#000!important;-webkit-print-color-adjust:exact;print-color-adjust:exact}.review-card{border:1px solid #ccc!important;box-shadow:none!important;page-break-inside:avoid;margin-bottom:1rem}.focus-slot{border:1px solid #999!important}.focus-slot.primary{border:2px solid #f7d154!important;background:#fffbea!important}.review-grid{display:block!important}.review-card{display:inline-block;vertical-align:top;width:48%;margin-right:2%}.focus-section.full-width,.reflection-section,.week-timeline,.week-timeline-events{width:100%!important;display:block!important}.weekly-review-header{border-bottom:2px solid #333;padding-bottom:1rem;margin-bottom:1.5rem}.week-dates{font-size:1.5rem;font-weight:700}.day-dot{-webkit-print-color-adjust:exact;print-color-adjust:exact}.day-dot.completed{background:#5cb85c!important}.day-dot.event{background:#9b59b6!important}.day-dot.overdue{background:#d9534f!important}.project-health{border-left:4px solid #337ab7!important}.project-health.warning{border-left-color:#f7d154!important}.project-health.danger{border-left-color:#d9534f!important}.focus-grid{display:flex!important;gap:1rem}.focus-slot{flex:1}.reflection-prompts{display:flex!important;gap:1rem}.reflection-prompt{flex:1}.prompt-input{border:1px solid #ccc!important;min-height:80px}.focus-section{page-break-before:auto}.reflection-section{page-break-before:always}}.monthly-review-nav{display:flex;align-items:center;gap:.5rem}.monthly-review-month-display{font-family:var(--font-heading);font-size:1.25rem;font-weight:700;color:var(--text-primary);margin-left:.5rem}.monthly-review-content{max-width:900px;margin:0 auto;padding:1rem}.month-heatmap{margin-bottom:1.5rem;border:var(--border-width-sm) solid var(--border-color);border-radius:var(--radius-md);padding:1rem;background:var(--bg-secondary)}.month-heatmap-header{display:grid;grid-template-columns:repeat(7,1fr);text-align:center;margin-bottom:.5rem}.month-heatmap-day-header{font-family:var(--font-heading);font-size:.75rem;font-weight:600;color:var(--text-secondary);text-transform:uppercase}.month-heatmap-grid{display:grid;grid-template-columns:repeat(7,1fr);gap:3px}.month-heatmap-cell{aspect-ratio:1;display:flex;flex-direction:column;align-items:center;justify-content:center;border-radius:var(--radius-xs);cursor:pointer;transition:transform .1s ease;border:var(--border-width-sm) solid transparent;position:relative;min-height:40px}.month-heatmap-cell:not(.empty):hover{transform:scale(1.1);border-color:var(--border-color);z-index:1}.month-heatmap-cell.empty{cursor:default;background:0 0}.month-heatmap-cell.intensity-0{background:var(--bg-primary)}.month-heatmap-cell.intensity-1{background:color-mix(in srgb,var(--accent-green) 20%,var(--bg-primary))}.month-heatmap-cell.intensity-2{background:color-mix(in srgb,var(--accent-green) 40%,var(--bg-primary))}.month-heatmap-cell.intensity-3{background:color-mix(in srgb,var(--accent-green) 60%,var(--bg-primary))}.month-heatmap-cell.vacation{background:var(--bg-tertiary);opacity:.6}.month-heatmap-cell.today{border-color:var(--accent-primary);border-width:2px}.month-heatmap-cell.past.intensity-0{background:var(--bg-tertiary)}.month-heatmap-day-number{font-family:var(--font-heading);font-size:.8rem;font-weight:600;color:var(--text-primary)}.month-heatmap-dots{display:flex;gap:2px;margin-top:2px}.month-dot{font-size:.6rem;font-weight:700;border-radius:var(--radius-xs);padding:0 3px;line-height:1.3}.month-dot.completed{color:var(--accent-green)}.month-dot.event{color:var(--accent-purple)}.monthly-review-cards{display:grid;grid-template-columns:1fr 1fr;gap:1rem}.review-card.month-goals-card,.review-card.month-stats-card{grid-column:span 1}.review-card.month-patterns-card,.review-card.month-pulse-card{grid-column:span 1}.review-card.month-reflection-card{grid-column:1/-1}.review-card-title{font-family:var(--font-heading);font-size:1rem;font-weight:700;margin-bottom:.75rem;color:var(--text-primary)}.month-stats-grid{display:grid;grid-template-columns:1fr 1fr;gap:.5rem}.month-stat-item{display:flex;flex-direction:column;align-items:center;padding:.5rem;border-radius:var(--radius-xs);background:var(--bg-primary);border:var(--border-width-sm) solid var(--border-color)}.month-stat-value{font-family:var(--font-heading);font-size:1.5rem;font-weight:700;color:var(--text-primary)}.month-stat-label{font-size:.75rem;color:var(--text-secondary);text-transform:uppercase;font-weight:600}.month-stats-highlights{display:flex;gap:1rem;margin-top:.5rem;justify-content:center}.stat-highlight{font-size:.8rem;color:var(--text-secondary)}.month-pulse-list{display:flex;flex-direction:column;gap:.5rem}.month-pulse-item{display:flex;align-items:center;gap:.5rem;padding:.5rem;border-radius:var(--radius-xs);background:var(--bg-primary);border:var(--border-width-sm) solid var(--border-color)}.pulse-name{font-weight:600;flex:1;font-size:.875rem}.pulse-stats{font-size:.75rem;color:var(--text-secondary)}.pulse-arrow{font-size:1rem;font-weight:700}.month-pulse-item.positive .pulse-arrow{color:var(--accent-green)}.month-pulse-item.negative .pulse-arrow{color:var(--accent-red)}.month-pulse-item.neutral .pulse-arrow{color:var(--text-secondary)}.month-goals-list{display:flex;flex-direction:column;gap:.5rem}.month-goal-item{display:flex;align-items:center;gap:.5rem;padding:.5rem .75rem;border-radius:var(--radius-xs);background:var(--bg-primary);border:var(--border-width-sm) solid var(--border-color)}.month-goal-item.empty{cursor:pointer;border-style:dashed;justify-content:center}.month-goal-item.empty:hover{border-color:var(--accent-primary);background:var(--bg-secondary)}.month-goal-item.done{opacity:.7}.month-goal-item.done .month-goal-text{text-decoration:line-through}.month-goal-item.abandoned{opacity:.5}.month-goal-item.abandoned .month-goal-text{text-decoration:line-through}.month-goal-status-btn{background:0 0;border:none;cursor:pointer;font-size:1rem;padding:0;color:var(--text-secondary);width:24px;text-align:center}.month-goal-item.done .month-goal-status-btn{color:var(--accent-green)}.month-goal-item.abandoned .month-goal-status-btn{color:var(--accent-red)}.month-goal-text{flex:1;font-size:.875rem}.month-goal-delete-btn{background:0 0;border:none;cursor:pointer;color:var(--text-tertiary);padding:0 4px;font-size:.75rem;opacity:0;transition:opacity .15s}.month-goal-item:hover .month-goal-delete-btn{opacity:1}.month-goal-placeholder{color:var(--text-tertiary);font-size:.875rem}.month-reflection-fields{display:flex;flex-direction:column;gap:.5rem}.month-reflection-label{font-size:.875rem;font-weight:600;color:var(--text-secondary)}.month-reflection-textarea{width:100%;padding:.5rem;border:var(--border-width-sm) solid var(--border-color);border-radius:var(--radius-xs);background:var(--bg-primary);color:var(--text-primary);font-family:var(--font-body);font-size:.875rem;resize:vertical}.month-reflection-textarea:focus{outline:2px solid var(--accent-primary);outline-offset:-1px}.month-patterns-list{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:.5rem}.month-pattern-item{font-size:.875rem;color:var(--text-secondary);padding:.5rem;background:var(--bg-primary);border-radius:var(--radius-xs);border:var(--border-width-sm) solid var(--border-color)}@media (max-width:640px){.monthly-review-cards{grid-template-columns:1fr}.review-card.month-goals-card,.review-card.month-patterns-card,.review-card.month-pulse-card,.review-card.month-stats-card{grid-column:span 1}.month-heatmap-cell{min-height:32px}.month-heatmap-day-number{font-size:.7rem}.month-heatmap-dots{display:none}}.import-wizard{display:flex;flex-direction:column;gap:1.5rem}.import-step{padding:1rem;background:var(--bg-secondary);border:var(--border-width) solid var(--border-color);border-radius:var(--radius-md)}.import-step h3{margin:0 0 1rem 0;font-size:var(--font-size-md);font-weight:600}.plugin-selector{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:.75rem}.plugin-option{display:flex;flex-direction:column;align-items:flex-start;padding:.75rem 1rem;background:var(--bg-card);border:var(--border-width) solid var(--border-color);border-radius:var(--radius-sm);cursor:pointer;text-align:left;transition:border-color var(--transition-fast),background var(--transition-fast)}.plugin-option:hover{border-color:var(--accent-primary);background:var(--bg-hover)}.plugin-option.selected{border-color:var(--accent-primary);background:color-mix(in srgb,var(--accent-primary) 10%,var(--bg-card));box-shadow:0 0 0 2px color-mix(in srgb,var(--accent-primary) 30%,transparent)}.plugin-option .plugin-name{font-weight:600;margin-bottom:.25rem}.plugin-option .plugin-meta{display:flex;gap:.5rem;font-size:var(--font-size-sm);color:var(--text-muted);margin-bottom:.25rem}.plugin-option .plugin-extensions{color:var(--accent-cyan)}.plugin-option .plugin-types{color:var(--text-secondary)}.plugin-option .plugin-description{font-size:var(--font-size-sm);color:var(--text-secondary);line-height:1.4}.file-selector{display:flex;align-items:center;gap:1rem}.selected-file-name{color:var(--text-secondary);font-family:monospace;font-size:var(--font-size-sm)}.import-preview-container{min-height:100px}.import-preview-table-wrapper{max-height:300px;overflow:auto;border:1px solid var(--border-color);border-radius:var(--radius-sm)}.import-preview-table{font-size:var(--font-size-sm);margin:0}.import-preview-table th{position:sticky;top:0;background:var(--bg-secondary);z-index:1}.import-preview-table td{max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.import-summary{margin:0 0 .75rem 0;color:var(--text-primary)}.import-more{margin:.5rem 0 0 0;color:var(--text-muted);font-style:italic;font-size:var(--font-size-sm)}.import-empty,.import-error{padding:2rem;text-align:center;color:var(--text-muted)}.import-error{color:var(--accent-red)}.import-warnings{margin-top:1rem;padding:.75rem;background:color-mix(in srgb,var(--accent-yellow) 10%,var(--bg-card));border:1px solid var(--accent-yellow);border-radius:var(--radius-sm);font-size:var(--font-size-sm)}.import-warnings ul{margin:.5rem 0 0 1.25rem;padding:0}.import-warnings li{margin-bottom:.25rem}.import-external-types{display:flex;gap:1rem;margin-bottom:1.5rem}.import-type-card{flex:1;display:flex;flex-direction:column;align-items:center;gap:.5rem;padding:1.5rem 1rem;background:var(--bg-card);border:2px solid var(--border-color);border-radius:var(--radius-md);cursor:pointer;transition:border-color .15s,background .15s}.import-type-card:hover{border-color:var(--accent-primary);background:var(--bg-secondary)}.import-type-icon{font-size:2rem}.import-type-label{font-weight:600;color:var(--text-primary)}.import-type-desc{font-size:var(--font-size-sm);color:var(--text-muted)}.plugin-list{display:flex;flex-direction:column;gap:.75rem}.plugin-item{display:flex;justify-content:space-between;align-items:center;padding:1rem;background:var(--bg-card);border:var(--border-width) solid var(--border-color);border-radius:var(--radius-md)}.plugin-item .plugin-info{flex:1}.plugin-item .plugin-name{font-weight:600}.plugin-item .plugin-version{color:var(--text-muted);font-size:var(--font-size-sm);margin-left:.5rem}.plugin-item .plugin-description{margin:.25rem 0;color:var(--text-secondary);font-size:var(--font-size-sm)}.plugin-item .plugin-extensions{font-size:var(--font-size-xs);color:var(--text-muted)}.plugin-item .plugin-actions{margin-left:1rem}.toggle-switch{position:relative;display:inline-block;width:44px;height:24px}.toggle-switch input{opacity:0;width:0;height:0}.toggle-slider{position:absolute;cursor:pointer;top:0;left:0;right:0;bottom:0;background-color:var(--bg-tertiary);border:2px solid var(--border-color);border-radius:var(--radius-xl);transition:background-color var(--transition-fast),border-color var(--transition-fast)}.toggle-slider:before{position:absolute;content:"";height:16px;width:16px;left:2px;bottom:2px;background-color:var(--text-muted);border-radius:var(--radius-full);transition:transform var(--transition-fast),background-color var(--transition-fast)}.toggle-switch input:checked+.toggle-slider{background-color:var(--accent-primary);border-color:var(--accent-primary)}.toggle-switch input:checked+.toggle-slider:before{transform:translateX(20px);background-color:var(--bg-card)}.toggle-switch input:focus+.toggle-slider{box-shadow:0 0 0 2px color-mix(in srgb,var(--accent-primary) 30%,transparent)}.milestones-section{margin-bottom:1.5rem}.milestones-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;padding-bottom:.5rem;border-bottom:2px solid var(--border-color)}.milestones-header h3{margin:0;font-size:1rem;font-family:var(--font-heading);font-weight:700}.milestone-card{background:var(--bg-card);border:var(--border-width) solid var(--border-color);border-radius:var(--radius-md);padding:1rem;margin-bottom:.75rem;transition:background-color .1s;box-shadow:var(--shadow-offset) var(--shadow-offset) 0 var(--border-color)}.milestone-card:hover{background:var(--bg-secondary)}.milestone-card-header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:.5rem}.milestone-card-header h4{margin:0;font-size:.95rem;font-family:var(--font-heading);font-weight:700}.milestone-card-header .milestone-status{font-size:.7rem;font-weight:700;text-transform:uppercase;padding:.15rem .4rem;border-radius:var(--radius-sm);background:var(--bg-secondary);color:var(--text-muted)}.milestone-card-header .milestone-status.completed{background:color-mix(in srgb,var(--accent-green) 15%,var(--bg-secondary));color:var(--accent-green)}.milestone-meta{display:flex;gap:1rem;font-size:.8rem;color:var(--text-muted);margin-bottom:.5rem}.milestone-progress{height:6px;background:var(--bg-secondary);border-radius:var(--radius-full);overflow:hidden;border:var(--border-width-sm) solid var(--border-color)}.milestone-progress-fill{height:100%;background:var(--accent-green);border-radius:var(--radius-full);transition:width var(--transition-fast)}.milestone-actions{display:flex;gap:.5rem;margin-top:.5rem}.milestone-actions button{font-size:.75rem;padding:.2rem .5rem;background:var(--bg-secondary);border:var(--border-width-sm) solid var(--border-color);border-radius:var(--radius-sm);cursor:pointer;color:var(--text-secondary);transition:background var(--transition-fast)}.milestone-actions button:hover{background:var(--bg-hover)}.milestone-actions button.danger:hover{background:color-mix(in srgb,var(--accent-red) 15%,var(--bg-secondary));color:var(--accent-red)}button.milestone-reorder-btn.btn{font-size:.65rem;padding:.15rem .35rem;line-height:1;min-width:1.5rem;text-align:center}.milestones-completed-section{margin-top:.75rem}.milestones-completed-toggle{font-size:.8rem;color:var(--text-secondary);padding:.25rem 0}.milestone-card-summary{padding:.5rem .75rem;opacity:.7}.milestone-card-summary .milestone-info{display:flex;align-items:center;gap:.5rem}.milestone-complete-badge{font-size:.7rem;font-weight:700;padding:.1rem .4rem;border-radius:var(--radius-sm);background:color-mix(in srgb,var(--accent-green) 15%,var(--bg-secondary));color:var(--accent-green)}.mobile-tab-bar{display:none;position:fixed;bottom:0;left:0;right:0;z-index:1100;background:var(--bg-card);border-top:var(--border-width) solid var(--border-color);padding-bottom:env(safe-area-inset-bottom,0);height:calc(52px + env(safe-area-inset-bottom,0px))}.mobile-tab{flex:1;display:flex;align-items:center;justify-content:center;height:52px;background:0 0;border:none;color:var(--text-muted);font-size:.7rem;font-weight:700;font-family:var(--font-sans);text-transform:uppercase;letter-spacing:.05em;cursor:pointer;-webkit-tap-highlight-color:transparent;transition:color .15s ease}.mobile-tab.active{color:var(--accent-blue)}.mobile-tab:active{background:var(--bg-secondary)}.mobile-tab-create{font-size:1.4rem;font-weight:400;color:var(--accent-green);letter-spacing:0;text-transform:none}.mobile-more-popover{display:none;position:fixed;bottom:calc(52px + env(safe-area-inset-bottom,0px));right:0;background:var(--bg-card);border:var(--border-width) solid var(--border-color);border-radius:var(--radius-md);padding:.25rem 0;z-index:1101;min-width:160px;box-shadow:0 -2px 8px rgba(0,0,0,.1)}.mobile-more-popover.visible{display:block}.mobile-more-popover button{display:block;width:100%;padding:.75rem 1rem;background:0 0;border:none;text-align:left;font-size:var(--font-size-sm);font-weight:600;color:var(--text-primary);cursor:pointer}.mobile-more-popover button:active{background:var(--bg-secondary)}.action-sheet{position:fixed;inset:0;z-index:10001;display:flex;flex-direction:column;justify-content:flex-end}.action-sheet.hidden{display:none}.action-sheet-backdrop{position:absolute;inset:0;background:rgba(0,0,0,.4)}.action-sheet-container{position:relative;background:var(--bg-card);border-top:var(--border-width) solid var(--border-color);border-radius:var(--radius-lg) var(--radius-lg) 0 0;padding:.5rem 1rem calc(.5rem + env(safe-area-inset-bottom,0px));max-height:60vh;overflow-y:auto;animation:sheetSlideUp .25s ease-out}.action-sheet-handle{width:36px;height:4px;border-radius:2px;background:var(--text-muted);margin:0 auto .75rem;opacity:.4}.action-sheet-content button{display:flex;align-items:center;gap:.75rem;width:100%;padding:.875rem .5rem;background:0 0;border:none;border-bottom:1px solid var(--bg-secondary);font-size:var(--font-size-base);font-weight:600;color:var(--text-primary);text-align:left;cursor:pointer}.action-sheet-content button:last-child{border-bottom:none}.action-sheet-content button:active{background:var(--bg-secondary)}.action-sheet-content button.danger{color:var(--accent-red)}.action-sheet-cancel{display:block;width:100%;padding:.875rem;margin-top:.5rem;background:var(--bg-secondary);border:var(--border-width-sm) solid var(--border-color);border-radius:var(--radius-md);font-size:var(--font-size-base);font-weight:700;color:var(--text-primary);text-align:center;cursor:pointer}.action-sheet-cancel:active{background:var(--bg-tertiary)}.modal-drag-handle{display:none;width:36px;height:4px;border-radius:2px;background:var(--text-muted);margin:.5rem auto 0;opacity:.4}.mobile-sort-bar{display:none;gap:.5rem;padding:.5rem 0;align-items:center}.mobile-sort-bar select{flex:1;font-size:var(--font-size-sm)}.mobile-filter-toggle{display:none}.swipe-actions-container{position:relative;overflow:hidden}.swipe-actions-bg{position:absolute;top:0;bottom:0;display:flex;align-items:center;padding:0 1rem;font-weight:700;font-size:var(--font-size-sm);color:var(--text-on-accent)}.swipe-actions-bg.swipe-left{right:0;background:var(--accent-green)}.swipe-actions-bg.swipe-right{left:0;background:var(--accent-red)}.swipe-content{position:relative;background:var(--bg-card);transition:transform .15s ease}.pull-to-refresh-indicator{display:none;text-align:center;padding:.75rem;font-size:var(--font-size-sm);color:var(--text-muted);font-weight:600}.pull-to-refresh-indicator.visible{display:block}.event-date-group-header{display:none}.day-plan-sidebar-toggle{display:none}@keyframes sheetSlideUp{from{transform:translateY(100%)}to{transform:translateY(0)}}@keyframes sheetSlideDown{from{transform:translateY(0)}to{transform:translateY(100%)}}@keyframes dialFadeIn{from{opacity:0}to{opacity:1}}@media (max-width:768px){body,html{overflow-x:hidden;max-width:100vw;touch-action:pan-y;overscroll-behavior-x:none}body{padding-top:env(safe-area-inset-top,0);padding-bottom:calc(52px + env(safe-area-inset-bottom,0px));padding-left:env(safe-area-inset-left,0);padding-right:env(safe-area-inset-right,0)}.pill-nav{touch-action:pan-x}.mobile-more-popover,.mobile-tab-bar,.timer-widget{padding-left:env(safe-area-inset-left,0);padding-right:env(safe-area-inset-right,0)}.mobile-tab-bar{display:flex}.tab-navigation{display:none!important}.app-header{display:none}.pill-nav{overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none;padding:var(--space-1) var(--space-3)}.tab-group>.subview>.page-header{position:static}.pill-nav::-webkit-scrollbar{display:none}.main-content{padding:.75rem}.page-header{flex-wrap:wrap;gap:.5rem}.page-header .btn-primary{display:none}.page-title{display:none}.modal-overlay{align-items:flex-end;bottom:calc(52px + env(safe-area-inset-bottom,0px))}.modal-container{width:100%!important;max-width:100%!important;max-height:calc(100vh - 52px - env(safe-area-inset-bottom,0px) - env(safe-area-inset-top,0px));border-radius:var(--radius-lg) var(--radius-lg) 0 0;margin:0;border-bottom:none;padding-bottom:0}.modal-container.modal-large{max-width:100%!important;width:100%!important;max-height:calc(100vh - 52px - env(safe-area-inset-bottom,0px) - env(safe-area-inset-top,0px));border-radius:var(--radius-lg) var(--radius-lg) 0 0}.modal-drag-handle{display:block}.modal-header{padding:.75rem 1rem}.modal-content{padding:1rem}@keyframes modalSlideIn{from{transform:translateY(100%)}to{transform:translateY(0)}}@keyframes modalSlideOut{from{transform:translateY(0)}to{transform:translateY(100%)}}.toast,.toast-undo{bottom:calc(env(safe-area-inset-bottom,0px) + 4.5rem)!important;left:1rem!important;right:1rem!important;max-width:none!important}.task-table{border:none;box-shadow:none;background:0 0}.task-header-row{display:none!important}.task-row{display:flex!important;flex-direction:column;gap:.25rem;padding:.75rem 1rem;margin-bottom:.5rem;background:var(--bg-card);border:var(--border-width-sm) solid var(--border-color);border-radius:var(--radius-md);border-left:4px solid var(--text-muted)}.task-row.task-pending{border-left-color:var(--text-muted)}.task-row .task-cell.priority-h~.task-cell:first-child,.task-row:has(.priority-h){border-left-color:var(--accent-red)}.task-row:has(.priority-m){border-left-color:var(--accent-yellow)}.task-row:has(.priority-l){border-left-color:var(--text-muted)}.task-row .task-cell{display:flex!important;overflow:visible;padding:0}.task-cell.task-description{font-weight:600;font-size:var(--font-size-base)}.task-cell.task-due,.task-cell.task-project{font-size:var(--font-size-sm);color:var(--text-secondary)}.task-row .task-cell.task-project::before{content:none}.task-cell.task-progress,.task-cell.task-recurrence,.task-row .task-cell:nth-child(3){display:none!important}.task-cell.task-project{order:2}.task-cell.task-due{order:3}.task-cell.task-description{order:1}.task-cell.task-actions-cell{order:4;justify-content:flex-end}.task-cell.task-progress:has(.progress-bar-container){display:flex!important;order:5}.task-actions-cell .bulk-checkbox{display:none}.kebab-btn{opacity:1}.mobile-sort-bar{display:flex}.mobile-filter-toggle{display:inline-flex;align-items:center;gap:.25rem;padding:.5rem .75rem;background:var(--bg-card);border:var(--border-width-sm) solid var(--border-color);border-radius:var(--radius-sm);font-size:var(--font-size-sm);font-weight:600;cursor:pointer}.filter-bar{display:none!important}.filter-bar.mobile-visible{display:flex!important;flex-direction:column;position:fixed;bottom:calc(52px + env(safe-area-inset-bottom,0px));left:0;right:0;background:var(--bg-card);border-top:var(--border-width) solid var(--border-color);border-radius:var(--radius-lg) var(--radius-lg) 0 0;padding:1rem;z-index:1050;box-shadow:0 -4px 12px rgba(0,0,0,.1)}.event-header-row{display:none!important}.event-row-virtual{display:flex!important;flex-direction:column;gap:.125rem;padding:.75rem 1rem;border-bottom:1px solid var(--bg-secondary)}.event-cell-date{font-weight:700;font-size:var(--font-size-sm);color:var(--text-secondary)}.event-cell-time{font-size:var(--font-size-sm);color:var(--text-muted)}.event-cell-title{font-weight:600;font-size:var(--font-size-base)}.event-cell-location{font-size:var(--font-size-sm);color:var(--text-secondary)}.event-date-group-header{display:flex;position:sticky;top:0;z-index:5;padding:.5rem 1rem;background:var(--bg-secondary);font-weight:700;font-size:var(--font-size-sm);text-transform:uppercase;letter-spacing:.05em;color:var(--text-primary);border-bottom:var(--border-width-sm) solid var(--border-color)}.email-item{padding:.625rem .75rem}.email-from{font-size:var(--font-size-sm)}.email-subject{font-size:var(--font-size-base)}.email-preview{display:none}.email-date{font-size:var(--font-size-xs)}.email-item .bulk-checkbox{display:none}.day-plan-content{flex-direction:column}.day-plan-sidebar{width:100%;max-height:none;border-top:var(--border-width-sm) solid var(--border-color);order:2}.day-plan-sidebar.collapsed .sidebar-task-list{display:none}.day-plan-sidebar-toggle{display:flex;align-items:center;justify-content:space-between;width:100%;padding:.625rem .75rem;background:var(--bg-secondary);border:none;border-bottom:1px solid var(--border-color);font-size:var(--font-size-sm);font-weight:700;cursor:pointer;color:var(--text-primary)}.day-plan-main{order:1}.day-plan-nav{flex-wrap:wrap;gap:.25rem}.weekly-review-content{padding:0}.monthly-review-content{padding:0}.month-reflection-textarea,.prompt-input{resize:none;overflow:hidden}.monthly-review-nav{flex-wrap:wrap;gap:.25rem}.monthly-review-month-display{font-size:1rem}.day-summary-sheet{padding:.5rem 0}.day-summary-date{font-size:1rem;font-weight:700;margin-bottom:.75rem;color:var(--text-primary)}.day-summary-stats{display:flex;gap:.5rem;margin-bottom:1rem}.day-summary-chip{padding:.25rem .75rem;background:var(--bg-secondary);border-radius:var(--radius-sm);font-size:var(--font-size-sm);font-weight:600;color:var(--text-secondary)}.day-summary-list{list-style:none;padding:0;margin:0 0 1rem 0}.day-summary-item{padding:.5rem 0;border-bottom:1px solid var(--bg-secondary);font-size:var(--font-size-sm);color:var(--text-primary)}.day-summary-time{font-weight:600;color:var(--text-secondary);margin-right:.5rem}.day-summary-more{color:var(--text-muted);font-style:italic}.day-summary-empty{color:var(--text-muted);font-size:var(--font-size-sm);margin:.5rem 0 1rem}.day-summary-go-btn{width:100%;margin-top:.5rem}.bulk-actions-bar{position:fixed;bottom:calc(52px + env(safe-area-inset-bottom,0px));left:0;right:0;z-index:1050;border-radius:var(--radius-lg) var(--radius-lg) 0 0;box-shadow:0 -4px 12px rgba(0,0,0,.15)}.pagination-controls{padding:.5rem}.pagination-controls .btn{padding:.5rem .75rem;font-size:var(--font-size-sm)}}@media (hover:none){.task-row:hover{background-color:transparent}.task-row-clickable:hover{background:0 0}.event-row-virtual:hover{background-color:transparent}.email-item:hover{background-color:transparent}.card:hover{background-color:var(--bg-card);transform:none;box-shadow:var(--shadow-offset) var(--shadow-offset) 0 var(--border-color)}.btn:hover{background:var(--bg-card)}.btn-primary:hover{background-color:var(--accent-blue)}.btn-danger:hover{background-color:var(--accent-red)}.dashboard-item:hover{background:var(--bg-card);transform:none;box-shadow:var(--shadow-offset) var(--shadow-offset) 0 var(--border-color)}.kanban-card:hover{background:var(--bg-card);transform:none;box-shadow:var(--shadow-offset) var(--shadow-offset) 0 var(--border-color)}.saved-view-item:hover{background:var(--bg-card);color:var(--text-primary);transform:none;box-shadow:var(--shadow-offset) var(--shadow-offset) 0 var(--border-color)}.context-menu-item:hover{background:0 0;color:var(--text-primary)}.modal-close:hover{background:var(--bg-card);color:var(--text-primary)}.month-heatmap-cell:hover{background:0 0;transform:none}.email-item .kebab-btn,.event-row-virtual .kebab-btn,.task-row .kebab-btn{opacity:1}.shortcut-hint-btn{display:none}.time-block-quick-options{grid-template-columns:1fr}.duration-preset,.time-block-quick-btn{min-height:44px;padding:.75rem;font-size:1rem}}body.is-touch .email-item .kebab-btn,body.is-touch .event-row-virtual .kebab-btn,body.is-touch .task-row .kebab-btn{opacity:1}body.is-touch .shortcut-hint-btn{display:none}.view-toggle{display:flex;gap:0;margin-left:auto}.view-toggle-btn{padding:.35rem .75rem;border:var(--border-width-sm) solid var(--border-color);background:var(--bg-secondary);font-family:var(--font-body);font-size:var(--font-size-md);cursor:pointer;transition:background var(--transition-fast),box-shadow var(--transition-fast)}.view-toggle-btn.active{background:var(--bg-card);font-weight:600}.view-toggle-btn:first-child{border-radius:var(--radius-xs) 0 0 var(--radius-xs)}.view-toggle-btn:last-child{border-radius:0 var(--radius-xs) var(--radius-xs) 0;border-left:none}.kanban-board{display:grid;grid-template-columns:repeat(3,1fr);gap:1rem;padding:.5rem 0;min-height:400px}.kanban-column{background:var(--bg-card);border:var(--border-width) solid var(--border-color);display:flex;flex-direction:column;min-height:300px;max-height:calc(100vh - 200px)}.kanban-column-header{padding:.75rem 1rem;border-bottom:2px solid var(--border-color);font-family:var(--font-heading);font-weight:700;display:flex;justify-content:space-between;align-items:center}.kanban-column-count{font-family:var(--font-body);font-size:var(--font-size-sm);color:var(--text-secondary)}.kanban-column-body{flex:1;overflow-y:auto;padding:.5rem;display:flex;flex-direction:column;gap:.5rem}.kanban-column.drag-over{background-color:var(--bg-tertiary)}.kanban-empty{text-align:center;padding:2rem 1rem;color:var(--text-secondary);font-size:var(--font-size-sm)}.kanban-card{padding:.75rem;background:var(--bg-card);border:var(--border-width-sm) solid var(--border-color);cursor:grab;box-shadow:var(--shadow-offset) var(--shadow-offset) 0 var(--border-color);transition:transform .15s ease,box-shadow .15s ease,background-color .15s ease;border-left:4px solid transparent}.kanban-card:hover{background:var(--bg-secondary);transform:translate(-2px,-2px);box-shadow:calc(var(--shadow-offset) + 2px) calc(var(--shadow-offset) + 2px) 0 var(--border-color)}.kanban-card.dragging{opacity:.5;cursor:grabbing}.kanban-card.priority-high{border-left-color:var(--accent-red)}.kanban-card.priority-medium{border-left-color:var(--accent-yellow)}.kanban-card.priority-low{border-left-color:var(--accent-green)}.kanban-card-title{font-weight:600;margin-bottom:.25rem}.kanban-card-meta{font-size:var(--font-size-sm);color:var(--text-secondary);display:flex;gap:.5rem;flex-wrap:wrap}.kanban-card-due.overdue{color:var(--accent-red);font-weight:600}.progress-bar-mini{height:3px;background:var(--bg-tertiary);border-radius:2px;margin-top:.5rem}.progress-bar-mini .progress-fill{height:100%;background:var(--accent-green);border-radius:2px}@media (max-width:768px){.kanban-board{grid-template-columns:1fr}.kanban-column{max-height:none}}.timer-widget{position:fixed;bottom:0;left:0;right:0;z-index:900;background:var(--bg-primary);border-top:var(--border-width) solid var(--border-color);box-shadow:0 -2px 8px rgba(0,0,0,.1);padding:.5rem 1rem;transition:transform .2s ease}.timer-widget.hidden{transform:translateY(100%);pointer-events:none}.timer-widget-inner{display:flex;align-items:center;gap:1rem;max-width:800px;margin:0 auto}.timer-task-name{flex:1;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.timer-elapsed{font-family:var(--font-mono, monospace);font-size:1.125rem;font-weight:700;color:var(--accent-color);min-width:5rem;text-align:center}.timer-actions{display:flex;gap:.5rem}.focus-overlay{position:fixed;inset:0;z-index:1000;background:var(--bg-primary);display:flex;align-items:center;justify-content:center;transition:opacity .3s ease}.focus-overlay.hidden{opacity:0;pointer-events:none}.focus-overlay-content{text-align:center;max-width:400px;width:100%;padding:2rem}.focus-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:2rem}.focus-label{font-family:var(--font-heading);font-size:1.25rem;font-weight:700}.focus-presets{display:flex;gap:.5rem}.focus-preset-btn.active{background:var(--accent-color);color:var(--bg-primary);border-color:var(--accent-color)}.focus-countdown{font-family:var(--font-mono, monospace);font-size:4rem;font-weight:700;line-height:1;margin-bottom:1.5rem;color:var(--text-primary)}.focus-progress-bar{height:6px;background:var(--bg-tertiary);border-radius:3px;margin-bottom:1.5rem;overflow:hidden}.focus-progress-fill{height:100%;background:var(--accent-color);border-radius:3px;transition:width 1s linear}.focus-task-name{color:var(--text-secondary);margin-bottom:2rem;font-size:.9rem}.focus-actions{display:flex;gap:1rem;justify-content:center}.time-summary-section{margin-bottom:1rem}.time-summary-toggle{display:flex;align-items:center;gap:.5rem;width:100%;padding:.5rem;background:0 0;border:none;font-family:var(--font-heading);font-size:.875rem;font-weight:700;color:var(--text-primary);cursor:pointer;text-align:left}.time-summary-toggle:hover{color:var(--accent-color)}.time-summary-toggle-icon{font-size:.625rem;transition:transform .15s ease}.time-summary-body{padding:.5rem;overflow:hidden;transition:max-height .2s ease;max-height:500px}.time-summary-body.collapsed{max-height:0;padding:0 .5rem}.time-summary-today{display:flex;justify-content:space-between;align-items:center;padding:.5rem 0;border-bottom:1px solid var(--border-color);margin-bottom:.5rem}.time-summary-today-label{font-weight:600;font-size:.875rem}.time-summary-today-value{font-family:var(--font-mono, monospace);font-weight:700;font-size:1rem;color:var(--accent-color)}.time-summary-week-header{font-size:.75rem;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--text-secondary);margin-bottom:.5rem}.time-summary-project{margin-bottom:.5rem}.time-summary-project-info{display:flex;justify-content:space-between;align-items:center;font-size:.8125rem;margin-bottom:.25rem}.time-summary-project-name{color:var(--text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}.time-summary-project-time{font-family:var(--font-mono, monospace);font-weight:600;font-size:.75rem;color:var(--text-secondary);margin-left:.5rem;flex-shrink:0}.time-summary-bar{height:4px;background:var(--bg-tertiary);border-radius:2px;overflow:hidden}.time-summary-bar-fill{height:100%;background:var(--accent-color);border-radius:2px}.unscheduled-task-actions{display:flex;gap:.25rem;margin-top:.375rem}.unscheduled-task-actions .btn{font-size:.7rem;padding:.125rem .375rem;min-height:auto;line-height:1.4}.task-time-badge{display:inline-block;font-family:var(--font-mono, monospace);font-size:.7rem;font-weight:600;color:var(--text-secondary);background:var(--bg-secondary);border:var(--border-width-sm) solid var(--border-color);border-radius:var(--radius-sm);padding:.05rem .35rem;margin-left:.375rem;vertical-align:middle;white-space:nowrap}.task-time-badge.over-estimate{color:var(--accent-red);border-color:var(--accent-red)}.task-started-icon{display:inline-block;width:0;height:0;border-style:solid;border-width:5px 0 5px 8px;border-color:transparent transparent transparent var(--accent-green,#22c55e);margin-right:.375rem;vertical-align:middle;cursor:pointer;opacity:.8;flex-shrink:0}.task-started-icon:hover{opacity:1}.task-timer-active{display:inline-block;width:8px;height:8px;background:var(--accent-red);border-radius:var(--radius-full);margin-left:.375rem;vertical-align:middle;animation:timer-pulse 1.5s ease-in-out infinite}@keyframes timer-pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.4;transform:scale(.8)}}@media (max-width:768px){.timer-widget{bottom:calc(52px + env(safe-area-inset-bottom,0px))}.focus-countdown{font-size:3rem}}.timer-active-banner{display:flex;align-items:center;gap:1rem;padding:.875rem 1rem;background:var(--bg-secondary);border:var(--border-width) solid var(--accent-color);border-radius:var(--radius-md);margin-bottom:1.5rem}.timer-active-info{flex:1;min-width:0}.timer-active-label{display:block;font-size:.75rem;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--accent-color);margin-bottom:.125rem}.timer-active-task{display:block;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.timer-active-elapsed{font-family:var(--font-mono, monospace);font-size:1.25rem;font-weight:700;color:var(--accent-color);min-width:5rem;text-align:center}.timer-active-actions{display:flex;gap:.5rem}.timer-focus-split{display:flex;align-items:center;gap:.375rem;padding:.5rem 0;margin-bottom:.5rem}.timer-focus-split-label{font-size:.8125rem;color:var(--text-secondary);font-weight:600;margin-right:.25rem}.timer-split-input{width:3.5rem;padding:.25rem .375rem;font-size:.875rem;font-family:var(--font-mono, monospace);text-align:center;border:var(--border-width) solid var(--border-color);border-radius:var(--radius-sm);background:var(--bg-primary);color:var(--text-primary)}.timer-focus-split-sep{font-size:.8125rem;color:var(--text-secondary)}.timer-task-list{display:flex;flex-direction:column;gap:0}.timer-task-item{display:flex;align-items:center;gap:1rem;padding:.75rem .5rem;border-bottom:1px solid var(--border-color)}.timer-task-item:last-child{border-bottom:none}.timer-task-info{flex:1;min-width:0}.timer-task-desc{display:block;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.timer-task-meta{display:flex;gap:.5rem;flex-wrap:wrap;margin-top:.25rem;font-size:.8125rem;color:var(--text-secondary)}.timer-task-project{font-weight:600}.timer-task-priority{font-weight:600}.timer-task-priority.priority-h,.timer-task-priority.priority-high{color:var(--accent-red)}.timer-task-priority.priority-m,.timer-task-priority.priority-medium{color:var(--accent-yellow,var(--accent-color))}.timer-task-estimate,.timer-task-tracked{font-family:var(--font-mono, monospace);font-size:.75rem}.timer-task-actions{display:flex;gap:.375rem;flex-shrink:0}@media (max-width:768px){.timer-active-banner{flex-wrap:wrap}.timer-active-elapsed{font-size:1rem}.timer-task-item{flex-wrap:wrap}.timer-task-actions{width:100%;justify-content:flex-end}}.task-overview-section{margin-bottom:1.5rem;padding:1rem;background:var(--bg-card);border:var(--border-width) solid var(--border-color);box-shadow:var(--shadow-offset) var(--shadow-offset) 0 var(--border-color)}.task-overview-section-title{font-family:var(--font-heading);font-size:1rem;font-weight:700;margin-bottom:.75rem;display:flex;align-items:center;gap:.5rem}.task-overview-count{font-weight:400;font-size:.85rem;color:var(--text-secondary)}.task-overview-stats{display:grid;grid-template-columns:repeat(4,1fr);gap:.75rem;margin-bottom:1rem}.task-overview-stat{text-align:center;padding:.5rem;background:var(--bg-secondary);border:var(--border-width-sm) solid var(--border-color)}.task-overview-stat-value{font-family:var(--font-heading);font-size:1.25rem;font-weight:700}.task-overview-stat-label{font-size:.75rem;color:var(--text-secondary);margin-top:.25rem}.task-overview-heatmap-nav{display:flex;align-items:center;justify-content:center;gap:1rem;margin-bottom:.75rem;font-family:var(--font-heading);font-weight:700}.task-overview-meta{display:flex;flex-direction:column;gap:.5rem}.task-overview-badges{display:flex;flex-wrap:wrap;gap:.5rem;margin-bottom:.25rem}.task-overview-details{display:flex;flex-direction:column;gap:.25rem;color:var(--text-secondary);font-size:.9rem}.task-overview-subtask-list{display:flex;flex-direction:column;gap:.25rem;margin-bottom:.75rem}.task-overview-subtask{display:flex;align-items:center;gap:.5rem;padding:.25rem 0}.completed-text{text-decoration:line-through;color:var(--text-secondary)}.task-overview-add-form{display:flex;gap:.5rem;margin-top:.5rem}.task-overview-add-form .form-input{flex:1}.task-overview-sessions{display:flex;flex-direction:column;gap:.25rem;font-size:.9rem;color:var(--text-secondary)}.task-overview-session{padding:.25rem 0;border-bottom:var(--border-width-sm) solid var(--border-color)}.task-overview-annotations{display:flex;flex-direction:column;gap:.5rem;margin-bottom:.75rem}.task-overview-annotation{padding:.5rem;background:var(--bg-secondary);border:var(--border-width-sm) solid var(--border-color)}.task-overview-annotation-date{font-size:.75rem;color:var(--text-secondary);margin-bottom:.25rem}.task-overview-completion-list{margin-top:.75rem;font-size:.9rem;color:var(--text-secondary)}.task-overview-completion-item{padding:.25rem 0;border-bottom:var(--border-width-sm) solid var(--border-color)}.progress-bar{height:6px;background:var(--bg-secondary);border:var(--border-width-sm) solid var(--border-color);margin-bottom:.75rem;overflow:hidden}.progress-fill{height:100%;background:var(--accent-green);transition:width .3s ease}.progress-bar.over-estimate .progress-fill{background:var(--accent-red)}.badge-completed{background:var(--accent-green);color:var(--text-on-accent);padding:.1rem .5rem;font-size:.75rem}.badge-started{background:var(--accent-blue);color:var(--text-on-accent);padding:.1rem .5rem;font-size:.75rem}.badge-pending{background:var(--bg-secondary);padding:.1rem .5rem;font-size:.75rem}.badge-priority-h{background:var(--accent-red);color:var(--text-on-accent);padding:.1rem .5rem;font-size:.75rem}.badge-priority-m{background:var(--accent-yellow);padding:.1rem .5rem;font-size:.75rem}.badge-priority-l{background:var(--bg-secondary);padding:.1rem .5rem;font-size:.75rem}.badge-focus{background:var(--accent-primary);color:var(--text-on-accent);padding:.1rem .5rem;font-size:.75rem}.badge-overdue{background:var(--accent-red);color:var(--text-on-accent);padding:.1rem .5rem;font-size:.75rem}.badge-snoozed{background:var(--accent-yellow);padding:.1rem .5rem;font-size:.75rem}@media (max-width:600px){.task-overview-stats{grid-template-columns:repeat(2,1fr)}}.toggle-nudge-dot{display:inline-block;width:7px;height:7px;background:var(--accent-red);border-radius:var(--radius-full);margin-left:.35rem;vertical-align:middle;animation:pulse-badge 2s infinite}.view-toggle-btn{position:relative}.day-review-inline{max-width:640px;padding:1rem}.events-calendar-container{padding:0}.calendar-nav{display:flex;align-items:center;gap:.5rem;margin-bottom:1rem}.calendar-nav-label{font-family:var(--font-heading);font-size:1.1rem;font-weight:700;margin-left:.5rem}.cal-month-grid{border:var(--border-width) solid var(--border-color);overflow:hidden;background:var(--bg-card);box-shadow:var(--shadow-offset) var(--shadow-offset) 0 var(--border-color)}.cal-month-cells,.cal-month-header{display:grid;grid-template-columns:repeat(7,1fr)}.cal-month-day-header{font-family:var(--font-heading);font-size:.75rem;font-weight:600;text-align:center;padding:.5rem;text-transform:uppercase;background:var(--bg-secondary);border-bottom:var(--border-width-sm) solid var(--border-color)}.cal-month-cell{min-height:90px;border:var(--border-width-sm) solid var(--border-color);padding:.25rem;cursor:pointer;transition:background var(--transition-fast)}.cal-month-cell:hover{background:var(--bg-secondary)}.cal-month-cell.other-month{opacity:.4}.cal-month-cell.today{border-color:var(--accent-primary);border-width:2px}.cal-month-cell-header{margin-bottom:.15rem}.cal-day-number{font-family:var(--font-heading);font-size:.8rem;font-weight:600}.cal-event-chip{font-size:.7rem;padding:1px 4px;margin-top:2px;border-radius:var(--radius-xs);background:var(--accent-blue);color:var(--text-on-accent);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:pointer}.cal-event-chip:hover{opacity:.85}.cal-event-chip.block-focus{background:var(--accent-red)}.cal-event-chip.block-personal{background:var(--accent-yellow);color:var(--text-primary)}.cal-event-chip.block-free_time{background:var(--accent-green);color:var(--text-primary)}.cal-event-more{font-size:.65rem;color:var(--text-secondary);padding:1px 4px}.month-day-detail{margin-top:1rem;border:var(--border-width) solid var(--border-color);padding:1rem;background:var(--bg-card);box-shadow:var(--shadow-offset) var(--shadow-offset) 0 var(--border-color)}.cal-day-detail-event{display:flex;gap:.75rem;padding:.5rem 0;border-bottom:1px solid var(--border-color);cursor:pointer}.cal-day-detail-event:hover{background:var(--bg-secondary)}.cal-detail-time{font-weight:600;white-space:nowrap;min-width:100px}.cal-detail-location{color:var(--text-secondary);font-size:.85rem}.cal-week-grid{border:var(--border-width) solid var(--border-color);overflow:hidden;background:var(--bg-card);box-shadow:var(--shadow-offset) var(--shadow-offset) 0 var(--border-color)}.cal-week-header{display:grid;grid-template-columns:60px repeat(7,1fr);border-bottom:var(--border-width-sm) solid var(--border-color);background:var(--bg-secondary)}.cal-week-day-header{text-align:center;padding:.5rem;font-family:var(--font-heading)}.cal-week-day-header.today{background:color-mix(in srgb,var(--accent-primary) 15%,transparent)}.cal-week-day-name{font-size:.75rem;text-transform:uppercase;font-weight:600}.cal-week-day-num{font-size:1rem;font-weight:700;display:block}.cal-week-allday-row{display:grid;grid-template-columns:60px repeat(7,1fr);border-bottom:var(--border-width-sm) solid var(--border-color);min-height:28px}.cal-allday-label{font-size:.7rem;color:var(--text-secondary);display:flex;align-items:center;justify-content:center}.cal-week-allday-cell{padding:2px;border-left:1px solid var(--border-color)}.cal-week-body{display:grid;grid-template-columns:60px repeat(7,1fr);position:relative;overflow-y:auto;max-height:70vh}.cal-week-time-gutter{position:relative}.cal-week-hour-label{position:absolute;right:.5rem;font-size:.7rem;color:var(--text-secondary);transform:translateY(-.5em)}.cal-week-day-col{position:relative;border-left:1px solid var(--border-color)}.cal-week-day-col.today{background:color-mix(in srgb,var(--accent-primary) 5%,transparent)}.cal-week-hour-line{position:absolute;left:0;right:0;border-top:1px dashed color-mix(in srgb,var(--border-color) 50%,transparent)}.cal-week-event{position:absolute;left:2px;right:2px;border:var(--border-width-sm) solid var(--border-color);border-radius:var(--radius-sm);padding:2px 4px;background:var(--accent-blue);color:var(--text-on-accent);overflow:hidden;cursor:pointer;z-index:10;font-size:.7rem}.cal-week-event:hover{opacity:.85}.cal-week-event-title{font-weight:600}.cal-week-event-time{font-size:.65rem;opacity:.85}.cal-week-event.block-focus{background:var(--accent-red)}.cal-week-event.block-personal{background:var(--accent-yellow);color:var(--text-primary)}.cal-week-event.block-free_time{background:var(--accent-green);color:var(--text-primary)}@media (max-width:600px){:root{--timeline-slot-h:22px}.cal-month-cell{min-height:64px;padding:4px}.cal-day-number{font-size:.95rem;font-weight:600}.cal-event-chip{font-size:.65rem}.cal-week-body{max-height:60vh}.cal-mobile-day{display:flex;flex-direction:column;height:70vh;user-select:none}.cal-mobile-day-header{padding:.75rem;font-weight:600;text-align:center;border-bottom:1px solid var(--border-color)}.cal-mobile-day-header.today{color:var(--accent-primary)}.cal-mobile-allday{padding:.5rem;display:flex;flex-direction:column;gap:.25rem;border-bottom:1px solid var(--border-color)}.cal-mobile-day-body{position:relative;flex:1;overflow-y:auto;display:grid;grid-template-columns:48px 1fr}.cal-mobile-day-col{position:relative;border-left:1px solid var(--border-color)}}@media (max-width:768px){.settings-page-layout{flex-direction:column}.settings-sidebar{width:100%;border-right:none;border-bottom:1px solid var(--border-color);padding:.75rem;flex-direction:row;flex-wrap:wrap;align-items:center}.settings-back-btn{margin-bottom:0;margin-right:.5rem;padding:.5rem}.settings-nav-items{flex-direction:row;flex-wrap:wrap;gap:.25rem}.settings-nav-item{padding:.5rem .75rem;border-left:none;border-radius:var(--radius-sm)}.settings-nav-item.active{border-left-color:transparent}.settings-content{padding:1rem}}
1 > \ No newline at end of file
1 + @font-face{font-family:Reglo;src:url('../fonts/Reglo-Bold.woff2') format('woff2');font-weight:700;font-style:normal;font-display:swap}*,::after,::before{box-sizing:border-box;margin:0;padding:0}:root{--timeline-slot-h:12px;--bg-primary:#E0E4FA;--bg-secondary:#CDD3F0;--bg-tertiary:#BAC2E6;--bg-card:#FFFFFF;--text-primary:#000000;--text-secondary:#2D2D2D;--text-muted:#6B6B6B;--accent-yellow:#F7D154;--accent-green:#5CB85C;--accent-blue:#6196FF;--accent-purple:#7B68EE;--accent-red:#DC3545;--accent-cyan:#17A2B8;--border-color:#000000;--border-width:2px;--border-width-sm:2px;--accent-color:var(--accent-blue);--accent-primary:var(--accent-blue);--bg-hover:var(--bg-tertiary);--border-light:var(--bg-tertiary);--text-on-accent:var(--bg-card);--shadow-offset-xs:1px;--shadow-offset-md:3px;--shadow-offset:4px;--shadow-offset-lg:6px;--shadow-offset-xl:8px;--shadow-brutal-xs:var(--shadow-offset-xs) var(--shadow-offset-xs) 0 var(--border-color);--shadow-brutal-md:var(--shadow-offset-md) var(--shadow-offset-md) 0 var(--border-color);--shadow-brutal-lg:var(--shadow-offset-lg) var(--shadow-offset-lg) 0 var(--border-color);--shadow-brutal-xl:var(--shadow-offset-xl) var(--shadow-offset-xl) 0 var(--border-color);--radius-xs:3px;--radius-sm:5px;--radius-md:5px;--radius-lg:10px;--radius-xl:20px;--radius-full:50%;--width-container:1400px;--width-modal:560px;--width-sidebar:280px;--space-1:0.25rem;--space-2:0.5rem;--space-3:0.75rem;--space-4:1rem;--space-5:1.25rem;--space-6:1.5rem;--font-sans:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;--font-serif:Georgia,'Times New Roman',serif;--font-mono:'SF Mono','Consolas','Liberation Mono',monospace;--font-display:'Reglo',var(--font-serif);--font-heading:var(--font-sans);--font-body:var(--font-sans);--font-size-xxs:0.65rem;--font-size-xs:0.7rem;--font-size-sm:0.75rem;--font-size-md:0.8rem;--font-size-base:0.875rem;--font-size-lg:1rem;--font-size-xl:1.1rem;--font-size-2xl:1.25rem;--font-size-3xl:1.5rem;--font-size-4xl:1.75rem;--line-height-tight:1.25;--line-height-normal:1.5;--line-height-relaxed:1.75;--transition-fast:0.1s;--transition-normal:0.15s;--transition-slow:0.3s;--overlay-color:color-mix(in srgb, var(--text-primary) 60%, transparent)}html{font-size:16px}.flex-1{flex:1}.flex-center-gap{display:flex;align-items:center;gap:.5rem}.text-sm-secondary{font-size:.875rem;color:var(--text-secondary)}.text-xs-secondary{font-size:.75rem;color:var(--text-secondary)}.text-accent-red{color:var(--accent-red)}.mb-1{margin-bottom:1rem}.settings-divider{margin-top:1.5rem;padding-top:1.5rem;border-top:2px solid var(--border-color)}.settings-heading{margin-bottom:1rem;font-family:var(--font-heading)}.settings-desc{font-size:.875rem;color:var(--text-secondary);margin-bottom:1rem}.subtask-item{display:flex;align-items:center;gap:.5rem;padding:.5rem;background:var(--bg-secondary);border-radius:4px;margin-bottom:.5rem}.subtask-item-linked{display:flex;align-items:center;gap:.5rem;padding:.5rem;background:var(--bg-tertiary);border-radius:4px;margin-bottom:.5rem;border-left:var(--border-width) solid var(--accent-color)}.subtask-checkbox{cursor:pointer;width:18px;height:18px}.subtask-checkbox-disabled{cursor:not-allowed;width:18px;height:18px;opacity:.5}.subtask-text-done{text-decoration:line-through;opacity:.6}body{font-family:var(--font-sans);background-color:var(--bg-primary);color:var(--text-primary);line-height:1.6;height:100vh;overflow:hidden;display:flex;flex-direction:column}.app-header{background:var(--bg-card);border-bottom:var(--border-width) solid var(--border-color);padding:.75rem 1.5rem;display:flex;justify-content:space-between;align-items:center}.header-content{display:flex;align-items:center;gap:.75rem}.header-actions{display:flex;align-items:center;gap:.5rem}.app-title{font-family:var(--font-display);font-size:1.75rem;font-weight:700;color:var(--text-primary);letter-spacing:-.02em}.app-subtitle{font-size:.875rem;color:var(--text-muted);font-weight:500;line-height:1}.mobile-view-title{display:none}.tab-navigation{display:flex;justify-content:center;gap:.5rem}.tab{display:flex;align-items:center;gap:.5rem;padding:.75rem 1.25rem;text-decoration:none;color:var(--text-primary);background:var(--bg-card);border:var(--border-width) solid var(--border-color);border-radius:var(--radius-md);font-weight:600;transition:background-color .15s ease}.tab:hover{background:var(--bg-secondary)}.tab.active{background-color:var(--accent-blue);color:var(--text-on-accent)}.tab-icon{font-size:1.1rem}.tab-label{font-weight:600;font-size:.9rem}.tab.tab-right{margin-left:auto}.tab-group .subview.hidden{display:none}.pill-nav{display:flex;align-items:center;gap:var(--space-1);padding:0;margin-bottom:1rem;min-height:2rem}.pill{padding:var(--space-1) var(--space-3);border-radius:var(--radius-xl);border:var(--border-width-sm) solid var(--border-color);background:var(--bg-card);font-family:var(--font-sans);font-size:var(--font-size-sm);font-weight:600;cursor:pointer;transition:background-color var(--transition-fast)}.pill:hover{background:var(--bg-tertiary)}.pill.active{background:var(--text-primary);color:var(--bg-card);border-color:var(--text-primary)}.main-content{flex:1;max-width:var(--width-container);width:100%;margin:0 auto;padding:1.5rem 1.75rem 2rem}.page-header{display:flex;justify-content:space-between;align-items:center;gap:.5rem;margin-bottom:1rem}.page-title{font-family:var(--font-heading);font-size:1.75rem;font-weight:700;color:var(--text-primary)}.tab-group{position:relative}.tab-group>.subview>.page-header{position:absolute;top:0;right:0;margin:0;z-index:1}.tab-group .page-header .page-title{display:none}#day-plan-view>.page-header,#project-dashboard-view>.page-header{position:static;margin-bottom:1rem}#project-dashboard-view .page-title{display:block}.btn{display:inline-flex;align-items:center;justify-content:center;gap:.5rem;padding:.625rem 1.25rem;border:var(--border-width) solid var(--border-color);border-radius:var(--radius-sm);font-size:.9rem;font-weight:600;cursor:pointer;transition:background-color .15s ease;text-decoration:none;background:var(--bg-card);color:var(--text-primary)}.btn:hover{background:var(--bg-secondary)}.btn:active{background:var(--bg-tertiary)}.btn:disabled{background:var(--bg-tertiary);color:var(--text-muted);cursor:not-allowed;opacity:.7}.btn:disabled:hover{background:var(--bg-tertiary)}.btn-primary{background-color:var(--accent-blue);color:var(--text-on-accent)}.btn-primary:hover{background-color:color-mix(in srgb,var(--accent-blue) 85%,#000)}.btn-primary:active{background-color:color-mix(in srgb,var(--accent-blue) 70%,#000)}.btn-secondary{background-color:var(--bg-secondary);color:var(--text-primary)}.btn-danger{background-color:var(--accent-red);color:var(--text-on-accent)}.btn-danger:hover{background-color:color-mix(in srgb,var(--accent-red) 85%,#000)}.btn-danger:active{background-color:color-mix(in srgb,var(--accent-red) 70%,#000)}.btn-sm{padding:.375rem .75rem;font-size:.8rem}.quick-add{display:flex;gap:.75rem;margin-bottom:1.5rem}.quick-add-input{flex:1;padding:.875rem 1rem;border:var(--border-width) solid var(--border-color);border-radius:var(--radius-md);background-color:var(--bg-card);font-size:1rem;color:var(--text-primary)}.quick-add-input::placeholder{color:var(--text-muted)}.quick-add-input:focus{outline:0;background-color:var(--accent-blue);color:var(--text-on-accent);box-shadow:0 0 0 2px var(--border-color)}.cards-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:1.25rem}.card{background-color:var(--bg-card);border:var(--border-width) solid var(--border-color);border-radius:var(--radius-md);padding:1.25rem;box-shadow:var(--shadow-offset) var(--shadow-offset) 0 var(--border-color);transition:transform .15s ease,box-shadow .15s ease,background-color .15s ease;cursor:pointer}.card:hover{background-color:var(--bg-secondary);transform:translate(-2px,-2px);box-shadow:calc(var(--shadow-offset) + 2px) calc(var(--shadow-offset) + 2px) 0 var(--border-color)}.card-header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:.75rem}.card-title{font-family:var(--font-heading);font-size:1.1rem;font-weight:700;color:var(--text-primary)}.card-description{font-size:.9rem;color:var(--text-secondary);margin-bottom:1rem}.markdown-content{font-size:.9rem;color:var(--text-secondary);line-height:1.5}.markdown-content p{margin:0 0 .5em 0}.markdown-content p:last-child{margin-bottom:0}.markdown-content ol,.markdown-content ul{margin:0 0 .5em 1.5em;padding:0}.markdown-content code{background:var(--bg-tertiary);padding:.1em .3em;border-radius:3px;font-size:.85em}.markdown-content pre{background:var(--bg-tertiary);padding:.5em;border-radius:4px;overflow-x:auto;margin:0 0 .5em 0}.markdown-content pre code{background:0 0;padding:0}.markdown-content a{color:var(--accent-color)}.markdown-content blockquote{border-left:3px solid var(--border-color);margin:0 0 .5em 0;padding-left:.75em;color:var(--text-secondary)}.markdown-content h1,.markdown-content h2,.markdown-content h3{margin:.5em 0 .25em 0;font-size:1em;font-weight:600;color:var(--text-primary)}.markdown-content table{border-collapse:collapse;margin:.5em 0}.markdown-content td,.markdown-content th{border:1px solid var(--border-color);padding:.25em .5em}.markdown-content img{max-width:100%}.card-meta{display:flex;gap:.5rem;flex-wrap:wrap}.badge,.tag{display:inline-flex;align-items:center;padding:.25rem .625rem;border:var(--border-width-sm) solid var(--border-color);border-radius:var(--radius-sm);font-size:.8125rem;font-weight:600;background:var(--bg-card);color:var(--text-primary)}.badge[data-color=green],.tag[data-color=green]{background-color:color-mix(in srgb,var(--accent-green) 20%,var(--bg-card));border-color:var(--accent-green)}.badge[data-color=yellow],.tag[data-color=yellow]{background-color:color-mix(in srgb,var(--accent-yellow) 20%,var(--bg-card));border-color:var(--accent-yellow)}.badge[data-color=red],.tag[data-color=red]{background-color:color-mix(in srgb,var(--accent-red) 20%,var(--bg-card));border-color:var(--accent-red)}.badge[data-color=cyan],.tag[data-color=cyan]{background-color:color-mix(in srgb,var(--accent-cyan) 20%,var(--bg-card));border-color:var(--accent-cyan)}.badge[data-color=purple],.tag[data-color=purple]{background-color:color-mix(in srgb,var(--accent-purple) 20%,var(--bg-card));border-color:var(--accent-purple)}.badge[data-color=muted],.tag[data-color=muted]{background-color:var(--bg-tertiary);border-color:var(--text-muted)}.tag.status-active{background-color:color-mix(in srgb,var(--accent-green) 20%,var(--bg-card));border-color:var(--accent-green)}.tag.status-on_hold,.tag.status-onhold{background-color:color-mix(in srgb,var(--accent-yellow) 20%,var(--bg-card));border-color:var(--accent-yellow)}.tag.status-archived{background-color:var(--bg-tertiary);border-color:var(--text-muted)}.tag.status-inactive{background-color:color-mix(in srgb,var(--accent-red) 20%,var(--bg-card));border-color:var(--accent-red)}.tag.status-completed{background-color:color-mix(in srgb,var(--accent-cyan) 20%,var(--bg-card));border-color:var(--accent-cyan)}.data-table{width:100%;border-collapse:separate;border-spacing:0;background-color:var(--bg-card);border:var(--border-width) solid var(--border-color);border-radius:var(--radius-md);overflow:hidden;box-shadow:var(--shadow-offset) var(--shadow-offset) 0 var(--border-color)}.data-table td,.data-table th{padding:1rem 1.25rem;text-align:left;border-bottom:2px solid var(--border-color)}.data-table th{background-color:var(--bg-secondary);font-size:.8rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:var(--text-primary)}.data-table tbody tr{transition:background-color .15s ease}.data-table tbody tr:hover{background-color:var(--bg-secondary)}.data-table tbody tr:last-child td{border-bottom:none}.data-table tbody tr.keyboard-selected,.data-table tbody tr.selected{background-color:color-mix(in srgb,var(--accent-blue) 25%,var(--bg-card))}.task-table{width:100%;background:var(--bg-card);border:var(--border-width) solid var(--border-color);border-radius:var(--radius-lg);overflow:hidden;display:flex;flex-direction:column;flex:1;min-height:0;box-shadow:var(--shadow-offset) var(--shadow-offset) 0 var(--border-color)}.task-header-row,.task-row{display:grid;grid-template-columns:1fr 140px 60px 110px 90px 100px 90px;align-items:center;gap:.75rem}.task-header-row{background-color:var(--bg-secondary);border-bottom:2px solid var(--border-color);padding:0 1.25rem}.task-header-row .task-cell{padding:.75rem 0;font-size:.8rem;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--text-primary)}.task-list-container{flex:1;min-height:0;overflow-y:auto;position:relative}.task-row{padding:.75rem 1.25rem;border-bottom:1px solid var(--border-color);transition:background-color .15s ease,opacity .25s ease,transform .25s ease;cursor:pointer}.task-row-removing{opacity:0;transform:translateX(20px)}.task-row:hover{background-color:var(--bg-secondary)}.task-row:last-child{border-bottom:none}.task-row.keyboard-selected,.task-row.selected{background-color:color-mix(in srgb,var(--accent-blue) 25%,var(--bg-card))}.task-cell{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}.task-actions-header{text-align:right}.event-table tbody tr{cursor:pointer}.task-description{font-weight:600;white-space:normal;display:flex;flex-wrap:wrap;align-items:center;gap:.25rem .5rem}.task-description-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:100%}.task-project{font-size:.85rem;color:var(--text-secondary);white-space:nowrap}.priority-high,.priority-low,.priority-medium{display:inline-block;padding:.25rem .5rem;border-radius:var(--radius-xs);font-weight:700;text-align:center}.priority-high{color:var(--accent-red);background:#fde8ea;background:color-mix(in srgb,var(--accent-red) 15%,var(--bg-card))}.priority-medium{color:var(--accent-yellow);background:#fef8e6;background:color-mix(in srgb,var(--accent-yellow) 15%,var(--bg-card))}.priority-low{color:var(--text-muted);background:var(--bg-secondary)}.sortable{cursor:pointer;user-select:none;white-space:nowrap}.sortable:hover{background:var(--bg-hover)}.sort-arrow{display:inline-block;width:.8em;margin-left:.25rem;opacity:.3}.sort-arrow::after{content:'\2195'}.sortable.sort-asc .sort-arrow::after{content:'\2191'}.sortable.sort-desc .sort-arrow::after{content:'\2193'}.sortable.sort-asc .sort-arrow,.sortable.sort-desc .sort-arrow{opacity:1}.task-overdue .task-description-text{color:var(--accent-red)}.task-overdue .task-due{color:var(--accent-red);font-weight:600}.task-tags{display:flex;gap:.25rem;flex-wrap:wrap}.task-tag{background-color:var(--bg-tertiary);color:var(--text-primary);padding:.125rem .5rem;border-radius:var(--radius-xs);font-size:.75rem;font-weight:600;border:1px solid var(--border-color)}.recurrence-icon{color:var(--accent-purple);font-size:.85rem;font-weight:700}.annotation-badge{background-color:var(--accent-yellow);color:var(--text-primary);padding:.125rem .5rem;border-radius:var(--radius-xs);font-size:.7rem;font-weight:700;border:var(--border-width-sm) solid var(--border-color)}.subtask-badge{background-color:var(--bg-secondary);color:var(--text-primary);padding:.125rem .5rem;border-radius:var(--radius-xs);font-size:.7rem;font-weight:700;border:var(--border-width-sm) solid var(--border-color);margin-left:.25rem}.task-started{border-left:4px solid var(--accent-green)}.task-completed{opacity:.5;text-decoration:line-through}.task-deleted{display:none}.due-overdue{color:var(--accent-red);font-weight:700;background:#fde8ea;background:color-mix(in srgb,var(--accent-red) 15%,var(--bg-card));padding:.25rem .5rem;border-radius:var(--radius-xs)}.due-today{color:var(--accent-yellow);font-weight:700;background:#fef8e6;background:color-mix(in srgb,var(--accent-yellow) 15%,var(--bg-card));padding:.25rem .5rem;border-radius:var(--radius-xs)}.due-soon{color:var(--text-secondary)}.due-future{color:var(--text-muted)}.events-list{display:flex;flex-direction:column;flex:1;min-height:0;gap:1rem}.event-table-virtual{width:100%;background:var(--bg-card);border:var(--border-width) solid var(--border-color);border-radius:var(--radius-lg);overflow:hidden;display:flex;flex-direction:column;flex:1;min-height:0;box-shadow:var(--shadow-offset) var(--shadow-offset) 0 var(--border-color)}.event-header-row,.event-row-virtual{display:grid;grid-template-columns:100px 80px 1fr 150px 40px;align-items:center;gap:.5rem}.event-header-row{background-color:var(--bg-secondary);border-bottom:2px solid var(--border-color);flex-shrink:0}.event-header-row .event-cell{padding:1rem 1.25rem;font-size:.8rem;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--text-primary)}.event-list-container{flex:1;min-height:0;overflow-y:auto;position:relative}.event-row-virtual{padding:.75rem 1.25rem;border-bottom:1px solid var(--border-color);transition:background-color .15s ease;cursor:pointer}.event-row-virtual:hover{background-color:var(--bg-secondary)}.event-row-virtual:last-child{border-bottom:none}.event-row-virtual.event-past{opacity:.7}.event-cell{overflow:hidden;text-overflow:ellipsis}.event-row{cursor:pointer}.event-cell-date{white-space:nowrap}.event-cell-date .event-date-num{font-weight:700;font-size:.9rem;color:var(--text-primary);margin-right:.5rem}.event-date-badge{display:inline-block;padding:.15rem .4rem;background:var(--accent-green);color:var(--text-on-accent);font-size:.7rem;font-weight:700;text-transform:uppercase;border-radius:var(--radius-xs);margin-right:.5rem}.event-cell-time{font-family:var(--font-mono);font-size:.85rem;color:var(--text-secondary)}.event-cell-title{font-weight:600}.event-cell-location{color:var(--text-secondary);font-size:.875rem}.event-date-badge.event-proximity-today{background:var(--accent-green)}.event-date-badge.event-proximity-tomorrow{background:var(--accent-yellow);color:var(--text-primary)}.event-date-badge.event-proximity-week{background:var(--accent-cyan)}.event-date-badge.event-proximity-future{background:var(--accent-blue)}.event-date-badge.event-proximity-past{background:var(--text-muted)}.event-row.event-past{opacity:.7}.no-upcoming-events{text-align:center;padding:2rem;color:var(--text-secondary);font-style:italic}.past-events-section{margin-top:.5rem}.past-events-toggle{display:flex;align-items:center;gap:.75rem;padding:.75rem 1rem;background:var(--bg-secondary);border:var(--border-width-sm) solid var(--border-color);border-radius:var(--radius-sm);cursor:pointer;font-weight:600;color:var(--text-secondary);transition:background-color .15s ease,color .15s ease;list-style:none}.past-events-toggle::-webkit-details-marker{display:none}.past-events-toggle::before{content:'▶';font-size:.7rem;transition:transform .15s ease}.past-events-section[open] .past-events-toggle::before{transform:rotate(90deg)}.past-events-toggle:hover{background:var(--bg-tertiary);color:var(--text-primary)}.past-events-label{flex:1}.past-events-count{background:var(--text-muted);color:var(--text-on-accent);font-size:.75rem;padding:.15rem .5rem;border-radius:var(--radius-sm)}.past-events-section .event-table-past{margin-top:.75rem;opacity:.85}.past-events-section .event-list-container{max-height:300px}#recurring-events-section .event-table-virtual{opacity:1;margin-top:.75rem}#recurring-events-section .event-list-container{max-height:320px}.event-row-virtual.event-recurring .event-recurrence-pattern{font-weight:600;color:var(--accent-primary)}.events-section-heading{margin:1rem 0 .5rem;font-size:.95rem;font-weight:700;color:var(--text-secondary);text-transform:uppercase;letter-spacing:.05em}.event-item{display:flex;gap:1rem;padding:1rem;background-color:var(--bg-card);border:var(--border-width) solid var(--border-color);border-radius:var(--radius-md);transition:background-color .15s ease;cursor:pointer}.event-item:hover{background-color:var(--bg-secondary)}.event-date{flex-shrink:0;width:80px;text-align:center;padding:.75rem;background-color:var(--accent-green);border-radius:var(--radius-sm);color:var(--text-on-accent)}.event-date-day{font-size:.7rem;font-weight:700;text-transform:uppercase;letter-spacing:.05em}.event-date-num{font-size:1.5rem;font-weight:700}.event-content{flex:1}.event-title{font-family:var(--font-heading);font-weight:700;font-size:1.1rem;color:var(--text-primary);margin-bottom:.25rem}.event-details{font-size:.875rem;color:var(--text-secondary);display:flex;gap:1rem}.event-location,.event-time{display:flex;align-items:center;gap:.25rem}.event-project{margin-top:.5rem}.email-list{display:flex;flex-direction:column;background-color:var(--bg-card);border:var(--border-width) solid var(--border-color);border-radius:var(--radius-md);overflow:hidden;flex:1;min-height:0;box-shadow:var(--shadow-offset) var(--shadow-offset) 0 var(--border-color)}.email-list-container{flex:1;min-height:0;overflow-y:auto;position:relative}.email-item{display:flex;gap:1rem;padding:1rem;border-bottom:2px solid var(--border-color);transition:background-color .15s ease;cursor:pointer}.email-item:last-child{border-bottom:none}.email-item:hover{background-color:var(--bg-secondary)}.email-item.unread{background-color:color-mix(in srgb,var(--accent-blue) 20%,var(--bg-card));border-left:4px solid var(--accent-blue)}.email-item.unread .email-subject{font-weight:700}.email-item.unread .email-from{font-weight:700}.email-item.outgoing{border-left:4px solid var(--accent-green)}.email-checkbox{flex-shrink:0;margin-top:.25rem}.email-content{flex:1;min-width:0}.email-header{display:flex;justify-content:space-between;margin-bottom:.25rem;align-items:center;gap:.5rem}.thread-badge{background-color:var(--bg-tertiary);color:var(--text-secondary);font-size:.7rem;font-weight:600;padding:.1rem .4rem;border-radius:var(--radius-md);min-width:1.25rem;text-align:center}.email-from{color:var(--text-primary);font-size:.9rem;font-weight:600}.email-date{color:var(--text-muted);font-size:.8rem;flex-shrink:0;font-weight:600}.email-subject{color:var(--text-primary);font-size:.95rem;margin-bottom:.25rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.email-preview{color:var(--text-muted);font-size:.85rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}@keyframes toastSlideIn{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.toast{position:fixed;bottom:var(--space-5);right:var(--space-5);padding:var(--space-3) var(--space-5);background:var(--bg-card);color:var(--text-primary);border:var(--border-width) solid var(--border-color);border-radius:var(--radius-md);box-shadow:var(--shadow-brutal-md);z-index:2000;font-weight:600;display:flex;align-items:center;gap:var(--space-3);animation:toastSlideIn .3s ease}.toast.toast-leaving{opacity:0;transform:translateY(10px);transition:opacity .3s ease,transform .3s ease}.toast-info{background:var(--bg-card);color:var(--text-primary)}.toast-success{background:var(--accent-green);color:var(--text-on-accent)}.toast-error{background:var(--accent-red);color:var(--text-on-accent)}.toast-action{background:0 0;border:1px solid currentColor;border-radius:var(--radius-sm);color:inherit;cursor:pointer;padding:.15rem .5rem;font-size:var(--font-size-sm);font-weight:600;font-family:inherit}.toast-undo{z-index:10000}.undo-message{flex:1}.undo-btn{padding:.25rem .75rem;background:var(--accent-blue);color:var(--text-on-accent);border:2px solid var(--border-color);border-radius:var(--radius-sm);font-family:inherit;font-size:var(--font-size-sm);font-weight:600;cursor:pointer;transition:background .15s ease}.undo-btn:hover{background:color-mix(in srgb,var(--accent-blue) 80%,#000)}.undo-countdown{font-size:var(--font-size-sm);color:var(--text-muted);min-width:2.5rem;text-align:right}@keyframes modalFadeIn{from{opacity:0}to{opacity:1}}@keyframes modalSlideIn{from{opacity:0;transform:translateY(-20px) scale(.95)}to{opacity:1;transform:translateY(0) scale(1)}}@keyframes modalFadeOut{from{opacity:1}to{opacity:0}}@keyframes modalSlideOut{from{opacity:1;transform:translateY(0) scale(1)}to{opacity:0;transform:translateY(-20px) scale(.95)}}.modal-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background-color:var(--overlay-color);display:flex;align-items:center;justify-content:center;z-index:1000;animation:modalFadeIn .15s ease-out}.modal-overlay.hidden{display:none}.modal-overlay.closing{animation:modalFadeOut .15s ease-in forwards}.modal-container{background-color:var(--bg-card);border:var(--border-width) solid var(--border-color);border-radius:var(--radius-lg);box-shadow:var(--shadow-brutal-xl);max-width:var(--width-modal);width:90%;max-height:90vh;overflow:auto;animation:modalSlideIn .2s ease-out}.modal-container.modal-large{max-width:calc(100vw - 4rem);width:calc(100vw - 4rem);max-height:calc(100vh - 4rem);height:calc(100vh - 4rem);display:flex;flex-direction:column}.modal-container.modal-large .modal-content{flex:1;overflow:auto;display:flex;flex-direction:column}.modal-overlay.closing .modal-container{animation:modalSlideOut .15s ease-in forwards}.modal-header{display:flex;justify-content:space-between;align-items:center;padding:1rem 1.5rem;border-bottom:var(--border-width) solid var(--border-color);background:var(--bg-secondary)}.modal-header h2,.modal-title{font-family:var(--font-heading);font-size:1.25rem;font-weight:700}.modal-close{background:var(--bg-card);border:var(--border-width-sm) solid var(--border-color);border-radius:var(--radius-sm);font-size:1.25rem;color:var(--text-primary);cursor:pointer;line-height:1;width:36px;height:36px;display:flex;align-items:center;justify-content:center;transition:background-color .15s ease}.modal-close:hover{background:var(--accent-blue);color:var(--text-on-accent)}.modal-content{padding:1.5rem}.form-group{margin-bottom:1.25rem}.form-more-toggle{display:block;background:0 0;border:none;cursor:pointer;font-size:.85rem;font-weight:600;color:var(--accent-blue);padding:.25rem 0;margin-bottom:.75rem}.form-more-toggle::before{content:'+ '}.form-more-toggle.expanded::before{content:'- '}.form-extended-fields.hidden{display:none}.recurrence-weekday-label{display:inline-flex;align-items:center;gap:.25rem;padding:.25rem .5rem;font-size:.8rem;border:1px solid var(--border);border-radius:var(--radius-sm);cursor:pointer}.recurrence-weekday-label:has(input:checked){background:var(--accent-primary);color:var(--bg-primary);border-color:var(--accent-primary)}.recurrence-weekday-label input[type=checkbox]{display:none}.recurrence-config .form-input,.recurrence-config .form-select{font-size:.85rem;padding:.25rem .5rem}.form-label{display:block;font-size:.9rem;font-weight:700;color:var(--text-primary);margin-bottom:.5rem}.form-input,.form-select,.form-textarea{width:100%;padding:.75rem 1rem;border:var(--border-width-sm) solid var(--border-color);border-radius:var(--radius-sm);background-color:var(--bg-card);color:var(--text-primary);font-size:1rem;box-shadow:none}.form-input:focus,.form-select:focus,.form-textarea:focus{outline:0;background-color:var(--bg-card);box-shadow:0 0 0 2px var(--accent-blue)}.form-textarea{min-height:100px;resize:vertical}.form-actions{display:flex;justify-content:flex-end;gap:.75rem;margin-top:1.5rem}.form-input[aria-invalid=true],.form-select[aria-invalid=true],.form-textarea[aria-invalid=true]{border-color:var(--accent-red);box-shadow:0 0 0 2px color-mix(in srgb,var(--accent-red) 30%,transparent)}.form-input[aria-invalid=true]:focus,.form-select[aria-invalid=true]:focus,.form-textarea[aria-invalid=true]:focus{box-shadow:0 0 0 2px var(--accent-red)}.form-error{color:var(--accent-red);font-size:.8rem;font-weight:600;margin-top:.25rem;display:none}.form-error.visible{display:block}.form-hint{font-size:var(--font-size-sm);color:var(--text-secondary);margin-top:var(--space-1)}.form-hint--preview{color:var(--accent-primary)}.form-checkbox-label{display:flex;align-items:center;gap:var(--space-2);cursor:pointer}.empty-state-action{margin-top:var(--space-3)}.stack{display:flex;flex-direction:column}.stack-2{gap:var(--space-2)}.stack-3{gap:var(--space-3)}.stack-4{gap:var(--space-4)}.row-flex{display:flex;align-items:center}.row-flex-2{gap:var(--space-2)}.row-flex-3{gap:var(--space-3)}.row-flex-4{gap:var(--space-4)}.text-center{text-align:center}.text-muted{color:var(--text-muted)}.text-secondary{color:var(--text-secondary)}.text-danger{color:var(--accent-red)}.text-sm{font-size:var(--font-size-sm)}.text-xs{font-size:var(--font-size-xs, .75rem)}.section-divider{border-top:var(--border-width-sm) solid var(--border-color);padding-top:var(--space-4);margin-top:var(--space-4)}.sync-status-row{display:flex;align-items:center;gap:var(--space-3);margin-bottom:var(--space-5)}.sync-status-dot{width:10px;height:10px;border-radius:var(--radius-full);background:var(--accent-green);display:inline-block}.sync-stats-grid{display:grid;grid-template-columns:1fr 1fr;gap:var(--space-4);margin-bottom:var(--space-5)}.sync-stat{padding:var(--space-3);background:var(--bg-secondary);border-radius:var(--radius-md)}.sync-stat-label{font-size:var(--font-size-sm);color:var(--text-muted);margin-bottom:var(--space-1)}.sync-stat-value{font-size:var(--font-size-md, .9rem)}.sync-encryption-error{color:var(--accent-red);font-size:var(--font-size-sm);margin-top:var(--space-2);display:none}.sync-encryption-error.visible{display:block}.sync-section-actions{margin-bottom:var(--space-5)}.sync-banner{padding:var(--space-3);background:var(--bg-secondary);border-radius:var(--radius-md);margin-bottom:var(--space-4);font-size:var(--font-size-sm);color:var(--text-secondary)}.sync-banner--warning{background:var(--bg-secondary);border:1px solid var(--border-color);padding:var(--space-4)}.sync-empty{padding:var(--space-5) 0}.sync-empty-message{margin-bottom:var(--space-5)}.sync-hint{margin-bottom:var(--space-4)}.sync-status-label{font-weight:500}.sync-account-row{display:flex;align-items:baseline;gap:var(--space-3);margin-bottom:var(--space-5);font-size:var(--font-size-sm)}.sync-account-label{color:var(--text-muted)}.sync-account-value{color:var(--text-primary)}.sync-account-username{color:var(--text-muted)}.sync-banner-title{font-weight:500;margin:0 0 var(--space-3) 0}.sync-banner-body{margin:0 0 var(--space-2) 0;font-size:var(--font-size-sm);color:var(--text-secondary)}.sync-banner-tier-line{margin:0 0 var(--space-4) 0;font-size:var(--font-size-sm);color:var(--text-secondary)}.sync-banner-actions{display:flex;gap:var(--space-2);align-items:center}.settings-subheading{margin-bottom:var(--space-3);font-size:var(--font-size-md, .9rem)}.settings-desc-block{font-size:var(--font-size-sm);color:var(--text-secondary);margin-bottom:var(--space-4)}.settings-actions-row{display:flex;flex-wrap:wrap;gap:var(--space-3);margin-bottom:var(--space-4)}.settings-actions-row--center{align-items:center;flex-wrap:nowrap;margin-top:var(--space-3)}.settings-status-text{font-size:var(--font-size-sm);color:var(--text-secondary)}.settings-section-block{padding-top:var(--space-4);border-top:var(--border-width-sm) solid var(--border-color)}.form-hint--spaced{margin-top:var(--space-2)}.empty-italic{color:var(--text-secondary);font-style:italic}.form-grid-2{display:grid;grid-template-columns:1fr 1fr;gap:var(--space-4)}.email-detect-status{font-size:var(--font-size-sm);color:var(--text-secondary);margin-top:var(--space-1)}.email-detect-note{font-size:var(--font-size-sm);margin-top:var(--space-2);padding:var(--space-3) var(--space-3);background:var(--bg-tertiary);border-left:3px solid var(--accent-primary);border-radius:var(--radius-sm);line-height:var(--line-height-normal)}.account-row{padding:var(--space-3);background:var(--bg-secondary);border-radius:var(--radius-sm);margin-bottom:var(--space-2)}.account-row-header{display:flex;justify-content:space-between;align-items:flex-start}.account-row-name{font-weight:600}.account-row-meta{font-size:var(--font-size-sm);color:var(--text-secondary)}.account-row-sync{font-size:var(--font-size-sm);color:var(--text-secondary)}.account-row-provider-badge{font-size:var(--font-size-xs, .7rem);padding:.125rem .375rem;background:var(--accent-blue);color:var(--text-on-accent);border-radius:var(--radius-xs);margin-left:var(--space-2)}.account-delete-btn{color:var(--accent-red)}.test-conn-result{padding:var(--space-2);background:var(--bg-secondary);border-radius:var(--radius-sm);margin-bottom:var(--space-2)}.test-conn-result--success{color:var(--accent-green)}.test-conn-result--error{color:var(--accent-red)}.folder-list{max-height:150px;overflow-y:auto;background:var(--bg-secondary);padding:var(--space-2);border-radius:var(--radius-sm);font-family:var(--font-mono);font-size:var(--font-size-sm)}.folder-list-meta{font-size:var(--font-size-sm);color:var(--text-secondary);margin-top:var(--space-2)}.error-pre{background:var(--bg-secondary);padding:var(--space-2);border-radius:var(--radius-sm);font-family:var(--font-mono);font-size:var(--font-size-sm);white-space:pre-wrap;max-height:150px;overflow-y:auto}.test-conn-section{border-top:var(--border-width-sm) solid var(--border-color);margin:var(--space-4) 0;padding-top:var(--space-4)}.oauth-waiting{text-align:center;padding:var(--space-5)}.oauth-waiting-title{font-size:var(--font-size-lg, 1.25rem);margin-bottom:var(--space-4)}.oauth-waiting-body{color:var(--text-secondary);margin-bottom:var(--space-5)}.oauth-waiting-spinner{margin:0 auto var(--space-5)}.contact-card-header{gap:var(--space-3)}.contact-card-body{flex:1;min-width:0}.contact-detail-header{display:flex;align-items:center;gap:var(--space-4);margin-bottom:var(--space-5)}.contact-detail-name{margin:0}.contact-detail-tags{margin-bottom:var(--space-4)}.form-actions--spaced{margin-top:var(--space-5)}.milestones-empty{color:var(--text-muted);font-size:var(--font-size-sm);margin-bottom:var(--space-4)}.bulk-modal-prompt{color:var(--text-secondary);margin-bottom:var(--space-2)}.bulk-modal-prompt--wide{margin-bottom:var(--space-3)}.bulk-modal-option-btn{width:100%;text-align:left;margin-bottom:var(--space-1)}.bulk-modal-scroll{max-height:300px;overflow-y:auto}.bulk-priority-row{display:flex;gap:var(--space-2)}.snooze-hint{font-size:var(--font-size-sm);color:var(--text-secondary);margin:0 0 var(--space-3) 0}.time-tracking-empty{color:var(--text-secondary);padding:var(--space-4) 0}.backups-empty{color:var(--text-secondary);font-style:italic}.backup-item{display:flex;align-items:center;justify-content:space-between;padding:var(--space-3);border:1px solid var(--border-color);border-radius:var(--radius-md);margin-bottom:var(--space-2)}.backup-item-meta{font-size:var(--font-size-sm);color:var(--text-secondary)}.export-desc{font-size:var(--font-size-sm);color:var(--text-secondary);margin-bottom:var(--space-5)}.export-note{margin-top:var(--space-4);padding:var(--space-3);background:var(--bg-secondary);border-radius:var(--radius-sm)}.export-note-text{font-size:var(--font-size-sm);color:var(--text-secondary);margin:0}.paint-time-preview{font-weight:600;color:var(--accent-primary);margin-bottom:var(--space-2)}.recurrence-config{display:none;margin-top:var(--space-2);padding:var(--space-2);border:1px solid var(--border-color);border-radius:var(--radius-sm)}.recurrence-config:not(.hidden){display:block}.recurrence-preview{font-size:var(--font-size-sm);color:var(--accent-primary);font-weight:600;margin-bottom:var(--space-2)}.recurrence-sublabel{font-size:var(--font-size-xs, .75rem)}.recurrence-unit{font-size:var(--font-size-sm)}.recurrence-day-label{display:flex;align-items:center;gap:var(--space-2);font-size:var(--font-size-sm)}.recurrence-nth-toggle{font-size:var(--font-size-sm);margin:var(--space-1) 0}.recurrence-row{margin-bottom:var(--space-2)}.recurrence-row.recurrence-hidden{display:none}.recurrence-interval-row{display:flex;align-items:center;gap:var(--space-2)}.recurrence-num{width:4rem}.recurrence-weekdays{display:flex;gap:var(--space-1);flex-wrap:wrap}.recurrence-monthly{margin-bottom:0}.recurrence-monthly.recurrence-hidden{display:none}.recurrence-monthly-stack{display:flex;flex-direction:column;gap:var(--space-2)}.recurrence-monthly-week{width:auto}.btn-danger-text{color:var(--accent-red)}.confirm-message-wrap{margin-bottom:var(--space-5)}.confirm-message{color:var(--text-secondary);line-height:var(--line-height-normal)}.review-intro{color:var(--text-secondary);font-size:var(--font-size-sm);margin:0 0 var(--space-4)}.review-intro--weekly{margin:var(--space-2) 0 var(--space-4)}.no-events-day{color:var(--text-secondary)}.loading--error{color:var(--accent-red)}.project-dashboard-desc{color:var(--text-secondary);margin-bottom:var(--space-4)}.pending-key-hint{position:fixed;bottom:var(--space-5);left:50%;transform:translateX(-50%);padding:var(--space-2) var(--space-4);background:var(--bg-card);color:var(--text-primary);border:var(--border-width) solid var(--border-color);border-radius:var(--radius-md);box-shadow:var(--shadow-brutal-md);z-index:10000;font-size:var(--font-size-sm);white-space:nowrap;animation:toastSlideIn .15s ease}.pending-key-hint-label{opacity:.7}.pending-key-hint kbd{background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:var(--radius-xs);padding:.125rem .4rem;font-family:inherit;font-size:var(--font-size-sm)}.shortcuts-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:var(--overlay-color);display:flex;align-items:center;justify-content:center;z-index:3000}.shortcuts-overlay-panel{background:var(--bg-card);border-radius:var(--radius-md);padding:var(--space-5);max-width:600px;max-height:80vh;overflow-y:auto}.shortcuts-title{margin-top:0;margin-bottom:var(--space-4);border-bottom:1px solid var(--border-color);padding-bottom:var(--space-2)}.shortcuts-grid{display:grid;grid-template-columns:1fr 1fr;gap:var(--space-5)}.shortcuts-group-heading{color:var(--text-secondary);font-size:var(--font-size-sm);margin-bottom:var(--space-2)}.shortcuts-group-heading+.shortcut-row{margin-top:0}.shortcuts-group-heading-spaced{color:var(--text-secondary);font-size:var(--font-size-sm);margin-bottom:var(--space-2);margin-top:var(--space-3)}.shortcuts-close{margin-top:var(--space-5);text-align:center}.shortcut-row{display:flex;align-items:center;gap:var(--space-2);margin-bottom:var(--space-2)}.shortcut-row kbd{background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:var(--radius-xs);padding:.25rem .5rem;font-family:inherit;font-size:var(--font-size-sm);min-width:1.5rem;text-align:center}.shortcut-row span{color:var(--text-secondary);font-size:var(--font-size-sm);margin-left:auto}.quick-add-syntax{font-size:var(--font-size-sm);color:var(--text-secondary);margin-top:var(--space-2);line-height:var(--line-height-relaxed)}.quick-add-syntax>span{margin-right:var(--space-3)}.quick-add-syntax kbd{font-size:.7rem;padding:.1rem .3rem;border:1px solid var(--border-color);border-radius:var(--radius-xs);background:var(--bg-secondary)}.virtual-scroller-wrapper{position:relative;width:100%}.virtual-scroller-spacer{height:0;width:100%}.meta-text{font-size:var(--font-size-sm);color:var(--text-muted)}.meta-text--secondary{color:var(--text-secondary)}.subtasks-empty{color:var(--text-secondary);text-align:center;padding:var(--space-4)}.subtask-linked-tag{color:var(--accent-color);font-size:var(--font-size-sm)}.task-actions-bar{display:flex;flex-wrap:wrap;gap:var(--space-2);margin-top:var(--space-4);padding-top:var(--space-3);border-top:1px solid var(--border-color)}.loading--error{color:var(--accent-red)}.error-state--padded{padding:var(--space-4)}.event-cell--shrink{flex:0 0 auto;padding-right:0}.hr-soft{border:none;border-top:1px solid var(--border-color);margin:var(--space-2) 0}.card-badge--success{background:var(--accent-green);color:var(--text-on-accent)}.card-badge--warning{background:var(--accent-yellow)}.card-badge--info{background:var(--accent-blue);color:var(--text-on-accent)}.card-badge--danger{background:var(--accent-red);color:var(--text-on-accent)}.card-badge--neutral{background:var(--bg-secondary)}.empty-italic--muted{color:var(--text-muted);font-style:italic}.review-history-block{margin-top:var(--space-5);padding-top:var(--space-4);border-top:1px dashed var(--border-color)}.review-history-heading{font-size:var(--font-size-sm);color:var(--text-muted);margin-bottom:var(--space-3)}.email-attachment-row{display:flex;align-items:center;gap:var(--space-2);padding:.375rem 0}.email-attachment-size{font-size:var(--font-size-sm);color:var(--text-muted);flex-shrink:0}.email-attachments-block{margin-bottom:var(--space-3);padding:var(--space-2) var(--space-3);background:var(--bg-secondary);border-radius:var(--radius-sm);border:1px solid var(--border-color)}.email-attachments-heading{font-size:var(--font-size-sm);font-weight:600;color:var(--text-secondary);margin-bottom:var(--space-1)}.email-thread-count{color:var(--text-muted);font-size:var(--font-size-sm)}.email-subject-line{font-weight:600;font-size:1.1rem}.email-meta-line{font-size:var(--font-size-sm);color:var(--text-secondary);margin-top:var(--space-1)}.email-snoozed-tag{color:var(--accent-yellow)}.email-actions-bar{border-top:1px solid var(--border-color);padding-top:var(--space-4);margin-top:auto}.email-attachment-tag{margin-right:var(--space-2)}.email-attachment-remove-btn{background:0 0;border:none;color:var(--accent-red);cursor:pointer;padding:0;font-family:inherit}.email-draft-item{cursor:pointer;padding:var(--space-3)}.email-draft-meta{font-size:var(--font-size-sm);color:var(--text-secondary)}.label-existing-line{font-size:var(--font-size-sm);color:var(--text-secondary);margin-top:var(--space-1)}.search-no-results{padding:var(--space-5);text-align:center;color:var(--text-secondary)}.email-attachment-name{flex:1;min-width:0}.email-draft-subject{font-weight:500}.compose-attachment-total{display:flex;gap:var(--space-2);align-items:baseline;padding-top:var(--space-2);margin-top:var(--space-1);font-size:var(--font-size-sm);color:var(--text-muted);border-top:1px dashed var(--border-color)}.compose-attachment-total.over-warn{color:var(--accent-yellow)}.compose-attachment-total.over-cap{color:var(--accent-red)}.compose-attachment-warn{font-style:italic}.about-info-list{display:grid;grid-template-columns:max-content 1fr;column-gap:var(--space-4);row-gap:var(--space-2);margin:var(--space-4) 0;font-size:var(--font-size-sm)}.about-info-list dt{font-weight:600;color:var(--text-secondary)}.about-info-list dd{margin:0;font-family:var(--font-mono)}.about-updater-note{margin-top:var(--space-4)}.filter-count{font-size:var(--font-size-sm);color:var(--text-muted);margin-right:var(--space-2);white-space:nowrap}.filter-count--capped{color:var(--accent-yellow);font-weight:600}@media (hover:none){#task-view-toggle{display:none}}.swipe-peek{position:absolute;top:0;bottom:0;display:flex;align-items:center;padding:0 var(--space-3);font-size:var(--font-size-sm);font-weight:600;color:var(--text-on-accent);pointer-events:none;opacity:0;transition:opacity .1s linear;z-index:0}.swipe-peek--right{left:0;background:var(--accent-blue);justify-content:flex-start}.swipe-peek--left{right:0;background:var(--accent-blue);justify-content:flex-end}.swipe-peek--success{background:var(--accent-green)}.swipe-peek--warn{background:var(--accent-yellow);color:var(--text-primary)}.swipe-peek--danger{background:var(--accent-red)}.swipe-peek--ready{box-shadow:inset 0 0 0 2px var(--text-on-accent)}.email-item,.event-row-virtual,.task-row{position:relative;overflow:hidden}.attachment-item{display:flex;align-items:center;gap:var(--space-3);padding:var(--space-2) 0;border-bottom:1px solid var(--border-color)}.attachment-icon{font-size:1.2rem}.attachment-meta{font-size:var(--font-size-sm);color:var(--text-muted)}.attachment-sync-warning{font-size:var(--font-size-sm);color:var(--text-muted)}.attachment-delete-btn{color:var(--accent-red)}.attachment-info{flex:1;min-width:0}.attachment-filename{font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.attachment-actions{display:flex;gap:var(--space-1)}.attachment-attach-row{margin-top:var(--space-4)}.link-task-item{display:flex;align-items:center;gap:var(--space-2);padding:var(--space-2);background:var(--bg-secondary);border-radius:var(--radius-sm);margin-bottom:var(--space-2);cursor:pointer}.link-task-item:hover{background:var(--bg-tertiary)}.link-task-project{font-size:var(--font-size-sm);color:var(--text-secondary)}.link-task-prompt{color:var(--text-secondary);margin-bottom:var(--space-4)}.link-task-desc{flex:1}.link-task-list{max-height:400px;overflow-y:auto}.form-actions--top-spaced{margin-top:var(--space-4)}.about-panel{text-align:center;padding:var(--space-4)}.about-title{font-family:var(--font-display);margin-bottom:var(--space-4)}.about-tagline{color:var(--text-secondary);margin-bottom:var(--space-2)}.about-version{color:var(--text-muted);font-size:var(--font-size-sm)}.welcome-panel{line-height:var(--line-height-relaxed)}.welcome-intro{color:var(--text-secondary);margin-bottom:var(--space-4)}.welcome-section{margin-bottom:var(--space-4)}.welcome-subhead{font-size:var(--font-size-md, .9rem);margin-bottom:var(--space-2)}.welcome-subhead--tight{margin-bottom:var(--space-1)}.welcome-step-stack{display:flex;flex-direction:column;gap:var(--space-2)}.welcome-step-btn{text-align:left}.welcome-tabs-list{color:var(--text-secondary);font-size:var(--font-size-sm);padding-left:var(--space-4);margin:0}.review-more-line{color:var(--text-muted);font-size:var(--font-size-sm);margin-top:var(--space-2)}.modal-attachments{font-size:var(--font-size-sm);margin-top:var(--space-1)}.account-row-info{flex:1}.account-row-actions{display:flex;align-items:center;gap:var(--space-2);margin-bottom:var(--space-2)}.account-row-quick{display:flex;gap:var(--space-2)}.account-row-quick .btn{flex:1}.account-list{margin-bottom:var(--space-4);max-height:400px;overflow-y:auto}.form-actions-spacer{flex:1}.oauth-block{margin-bottom:var(--space-5)}.oauth-block-title{font-weight:500;margin-bottom:var(--space-3)}.oauth-buttons{display:flex;flex-wrap:wrap;gap:var(--space-2)}.oauth-buttons .btn{flex:1;min-width:120px}.oauth-helptext{font-size:var(--font-size-sm);color:var(--text-secondary);margin-top:var(--space-2)}.imap-block-divider{border-top:var(--border-width-sm) solid var(--border-color);margin:var(--space-4) 0;padding-top:var(--space-4)}.imap-block-title{font-weight:500;margin-bottom:var(--space-3)}.test-conn-results{margin-bottom:var(--space-4)}.sync-results{margin-bottom:var(--space-4)}.sync-result-banner{padding:var(--space-3);background:var(--bg-secondary);border-radius:var(--radius-sm);margin-bottom:var(--space-2)}.sync-result-grid{display:grid;grid-template-columns:1fr 1fr;gap:var(--space-2);margin-bottom:var(--space-2)}.sync-result-tile{padding:var(--space-2);background:var(--bg-secondary);border-radius:var(--radius-sm)}.app-footer{background-color:var(--bg-card);border-top:var(--border-width) solid var(--border-color);padding:.75rem 1.5rem}.footer-content{max-width:var(--width-container);margin:0 auto;display:flex;justify-content:space-between;align-items:center}.keyboard-hints{display:flex;gap:1rem;font-size:.8rem;color:var(--text-muted)}kbd{display:inline-block;padding:.2rem .5rem;background-color:var(--bg-secondary);border:var(--border-width-sm) solid var(--border-color);border-radius:var(--radius-xs);font-family:var(--font-mono);font-size:.75rem;font-weight:700}.welcome-hint{color:var(--text-secondary);font-size:var(--font-size-sm);margin-bottom:var(--space-2)}.version{font-size:.75rem;color:var(--text-muted);font-weight:600}.empty-state{text-align:center;padding:3rem;color:var(--text-secondary)}.empty-state--compact{padding:2rem 1rem;font-size:var(--font-size-sm)}.empty-state--dashboard{padding:2rem 1rem}.empty-state--error{color:var(--accent-red)}.empty-state-icon{font-size:4rem;margin-bottom:1rem}.empty-state-text{font-size:1.1rem;font-weight:600;margin-bottom:1rem}.error-state{text-align:center;padding:2rem;color:var(--accent-red);background:color-mix(in srgb,var(--accent-red) 10%,var(--bg-card));border:var(--border-width-sm) solid var(--accent-red);border-radius:var(--radius-sm);font-weight:600}.view{display:block}.view.hidden{display:none}.filter-bar{display:flex;flex-wrap:wrap;gap:.75rem;margin-bottom:1.5rem;padding:1rem;background-color:var(--bg-card);border:var(--border-width) solid var(--border-color);border-radius:var(--radius-md);box-shadow:var(--shadow-offset) var(--shadow-offset) 0 var(--border-color)}.filter-group{display:flex;align-items:center;gap:.5rem}.filter-label{font-size:.8rem;font-weight:700;color:var(--text-secondary);text-transform:uppercase;letter-spacing:.05em}.filter-select{padding:.5rem .75rem;border:var(--border-width-sm) solid var(--border-color);border-radius:var(--radius-sm);background-color:var(--bg-card);color:var(--text-primary);font-size:.875rem;font-weight:600}.filter-select:focus{outline:0;background-color:var(--accent-blue);color:var(--text-on-accent)}.filter-checkbox{display:flex;align-items:center;gap:.4rem;font-size:.875rem;font-weight:600;color:var(--text-primary);cursor:pointer}.filter-checkbox input[type=checkbox]{width:1rem;height:1rem;cursor:pointer}.btn-link{background:0 0;border:none;box-shadow:none;color:var(--text-secondary);font-size:.875rem;cursor:pointer;text-decoration:underline;padding:.5rem}.btn-link:hover{box-shadow:none;transform:none;color:var(--text-primary)}@media (min-width:1400px){.main-content{max-width:1600px}.cards-grid{grid-template-columns:repeat(auto-fill,minmax(380px,1fr))}.project-dashboard-grid{gap:2rem}.day-plan-sidebar{width:320px}.modal-container{max-width:640px}}@media (max-width:1024px){.saved-views-sidebar{width:180px}.day-plan-sidebar{width:240px}.project-dashboard-grid{grid-template-columns:1fr 1fr;gap:1rem}.project-dashboard-grid .dashboard-column:last-child{grid-column:span 2}.filter-bar{flex-wrap:wrap}.filter-actions{width:100%;justify-content:flex-end;margin-top:.5rem}}@media (max-width:768px){.tab-navigation{flex-wrap:wrap;gap:.5rem}.tab{flex:1 1 auto;min-width:calc(33% - .5rem);justify-content:center;padding:.625rem .75rem}.tab-label{display:none}.tab-icon{font-size:1.25rem}.kbd-hint{display:none}.cards-grid{grid-template-columns:1fr}.task-table{font-size:.85rem}.task-header-row,.task-row{grid-template-columns:1fr 80px 40px 80px}.task-header-row .task-cell:nth-child(n+5),.task-row .task-cell:nth-child(n+5){display:none}.filter-bar{flex-direction:column}.keyboard-hints{display:none}.page-title{font-size:1.5rem}.saved-views-sidebar{display:none}.day-plan-content{flex-direction:column}.day-plan-sidebar{width:100%;max-height:200px}.project-dashboard-grid{grid-template-columns:1fr}.project-dashboard-grid .dashboard-column:last-child{grid-column:span 1}.modal-container{width:95%;max-height:95vh}.bulk-actions-bar{flex-wrap:wrap}.bulk-select-all{width:100%;margin-top:.5rem}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.pagination-controls{display:flex;align-items:center;justify-content:center;gap:1rem;padding:1rem;margin-top:1rem;background:var(--bg-card);border:var(--border-width) solid var(--border-color);border-radius:var(--radius-md)}.pagination-info{font-weight:600;color:var(--text-secondary);font-size:.9rem}.pagination-controls .btn:disabled{opacity:.5;cursor:not-allowed}.btn:focus-visible,.card:focus-visible,.dashboard-item:focus-visible,.email-item:focus-visible,.event-row-virtual:focus-visible,.filter-select:focus-visible,.form-input:focus-visible,.form-select:focus-visible,.form-textarea:focus-visible,.modal-close:focus-visible,.saved-view-item:focus-visible,.snooze-option:focus-visible,.tab:focus-visible,.task-row:focus-visible,.timeline-item:focus-visible,.unscheduled-task:focus-visible{outline:3px solid var(--accent-blue);outline-offset:2px}.event-row,.task-row-clickable{cursor:pointer}.skip-link{position:absolute;top:-100px;left:0;background:var(--accent-blue);color:var(--text-on-accent);padding:.75rem 1.5rem;z-index:9999;font-weight:700;border:var(--border-width) solid var(--border-color);text-decoration:none}.skip-link:focus{top:0}.source-email-link{padding:.75rem;background:var(--bg-secondary);border-radius:var(--radius-sm);border-left:4px solid var(--accent-blue)}.thread-message{margin-bottom:1rem;padding:.75rem;background:var(--bg-secondary);border-radius:var(--radius-sm)}.thread-message-latest{border-left:3px solid var(--accent-blue)}.thread-message-header{display:flex;justify-content:space-between;margin-bottom:.5rem;font-size:.8rem;color:var(--text-secondary)}.thread-message-from{font-weight:700}.email-reader-body{white-space:pre-wrap;font-size:.9rem;line-height:1.6;color:var(--text-primary);word-wrap:break-word;overflow-wrap:break-word}.email-reader-body .email-link{color:var(--accent-blue);text-decoration:underline;cursor:pointer;word-break:break-all}.email-reader-body .email-link:hover{color:var(--accent-cyan)}.email-reader-body hr{border:none;border-top:2px solid var(--border-color);margin:1rem 0}.email-reader-quote{border-left:3px solid var(--text-muted);padding-left:1rem;margin:.5rem 0;color:var(--text-secondary);font-style:italic}.email-quote-toggle{display:inline-block;color:var(--text-muted);font-size:.8125rem;cursor:pointer;padding:.25rem 0;user-select:none}.email-quote-toggle:hover{color:var(--accent-blue)}.email-quote-block{border-left:3px solid var(--text-muted);padding-left:1rem;margin:.25rem 0 .5rem;color:var(--text-secondary)}.email-quote-block.hidden{display:none}.autocomplete-dropdown{background:var(--bg-card);border:1px solid var(--border-color);border-radius:var(--radius-sm);box-shadow:var(--shadow-brutal);z-index:100;max-height:200px;overflow-y:auto}.autocomplete-item{padding:.5rem .75rem;cursor:pointer;font-size:.875rem}.autocomplete-item.active,.autocomplete-item:hover{background:var(--bg-secondary)}.autocomplete-name{font-weight:500}.autocomplete-email{color:var(--text-secondary);margin-left:.25rem}.email-label-badge{display:inline-block;font-size:.6875rem;padding:.125rem .375rem;border-radius:var(--radius-sm);background:var(--accent-blue);color:var(--bg-primary);font-weight:600;vertical-align:middle}.email-reader-container{display:flex;flex-direction:column;height:100%;min-height:0}.email-reader-header{margin-bottom:1rem;padding-bottom:.75rem;border-bottom:1px solid var(--border-color)}.email-sender-contact{display:flex;align-items:center;gap:.5rem;margin-top:.5rem;padding:.4rem .5rem;background:var(--bg-tertiary);border-radius:4px}.email-sender-info{display:flex;flex-direction:column;flex:1;min-width:0}.email-sender-name{font-weight:600;font-size:.85rem}.email-sender-company{font-size:.75rem;color:var(--text-secondary)}.contact-avatar-sm{width:32px;height:32px;border-radius:50%;background:var(--accent-color);color:var(--bg-primary);display:flex;align-items:center;justify-content:center;font-size:.75rem;font-weight:700;flex-shrink:0}.contact-avatar-unknown{background:var(--bg-secondary);color:var(--text-secondary);border:var(--border-width-sm) solid var(--border-color)}.email-reader-thread{flex:1;overflow-y:auto;margin-bottom:1rem;min-height:0}.dropdown{position:relative;display:inline-block}.dropdown-menu{display:none;position:absolute;bottom:100%;left:0;margin-bottom:.25rem;background:var(--bg-card);border:var(--border-width) solid var(--border-color);border-radius:var(--radius-md);box-shadow:var(--shadow-brutal-md);min-width:160px;z-index:100}.dropdown-menu.show{display:block}.dropdown-item{display:block;width:100%;padding:.5rem 1rem;text-align:left;background:0 0;border:none;cursor:pointer;font-size:.875rem;color:var(--text-primary)}.dropdown-item:hover{background:var(--bg-secondary)}.dropdown-item:first-child{border-radius:var(--radius-md) var(--radius-md) 0 0}.dropdown-item:last-child{border-radius:0 0 var(--radius-md) var(--radius-md)}.context-menu{position:fixed;z-index:10000;min-width:180px;max-width:280px;background:var(--bg-card);border:var(--border-width) solid var(--border-color);border-radius:var(--radius-sm);box-shadow:var(--shadow-brutal-lg);padding:.25rem 0;display:none}.context-menu.visible{display:block}.context-menu-item{display:flex;align-items:center;gap:.75rem;padding:.5rem 1rem;font-size:.875rem;font-weight:500;color:var(--text-primary);cursor:pointer;border:none;background:0 0;width:100%;text-align:left;transition:background .1s}.context-menu-item:focus,.context-menu-item:hover{background:var(--accent-blue);color:var(--text-on-accent);outline:0}.context-menu-item:focus-visible{outline:2px solid var(--accent-blue);outline-offset:-2px}.context-menu-header{font-size:.7rem;font-weight:700;color:var(--text-secondary);text-transform:uppercase;letter-spacing:.05em;padding:.5rem 1rem .25rem}.context-menu-item-icon{width:1.25rem;text-align:center;flex-shrink:0}.context-menu-item-label{flex:1}.context-menu-item-subtitle{display:block;font-size:.7rem;color:var(--text-secondary);font-weight:400}.context-menu-item-shortcut{font-size:.75rem;color:var(--text-muted);font-family:var(--font-mono)}.context-menu-item--danger{color:var(--accent-red)}.context-menu-item--danger:hover{background:var(--accent-red);color:var(--text-on-accent)}.context-menu-separator{height:2px;background:var(--border-color);margin:.25rem .5rem}.context-menu-hint{padding:.35rem 1rem;font-size:.7rem;color:var(--text-muted);border-top:1px solid var(--border-color);margin-top:.25rem}::-webkit-scrollbar{width:12px;height:12px}::-webkit-scrollbar-track{background:var(--bg-secondary);border-left:2px solid var(--border-color)}::-webkit-scrollbar-thumb{background:var(--text-muted);border:var(--border-width-sm) solid var(--border-color);border-radius:var(--radius-sm)}::-webkit-scrollbar-thumb:hover{background:var(--text-secondary)}.loading{display:flex;justify-content:center;align-items:center;height:200px;color:var(--text-secondary);font-family:var(--font-heading)}.skeleton-shimmer{display:flex;flex-direction:column;gap:1rem;padding:1rem}.skeleton-shimmer .skeleton-row{display:flex;align-items:center;gap:.75rem;padding:.75rem;background:var(--bg-card);border-radius:var(--radius-md);border:var(--border-width) solid var(--border-color)}.skeleton-shimmer .skeleton-avatar{width:36px;height:36px;border-radius:var(--radius-full);background:linear-gradient(90deg,var(--bg-secondary) 25%,var(--bg-tertiary) 50%,var(--bg-secondary) 75%);background-size:200% 100%;animation:skeleton-pulse 1.5s ease-in-out infinite;flex-shrink:0}.skeleton-shimmer .skeleton-lines{flex:1;display:flex;flex-direction:column;gap:.4rem}.skeleton-shimmer .skeleton-line{height:.75rem;border-radius:var(--radius-sm);background:linear-gradient(90deg,var(--bg-secondary) 25%,var(--bg-tertiary) 50%,var(--bg-secondary) 75%);background-size:200% 100%;animation:skeleton-pulse 1.5s ease-in-out infinite}.skeleton-shimmer .skeleton-line.short{width:40%}.skeleton-shimmer .skeleton-line.medium{width:65%}.skeleton-shimmer .skeleton-line.long{width:90%}@keyframes skeleton-pulse{0%{background-position:200% 0}100%{background-position:-200% 0}}@keyframes spin{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}.spinner{display:inline-block;width:1em;height:1em;border:2px solid currentColor;border-top-color:transparent;border-radius:var(--radius-full);animation:spin .8s linear infinite}.btn-loading{position:relative;pointer-events:none;opacity:.8}.btn-loading .btn-text{visibility:hidden}.btn-loading::after{content:'';position:absolute;left:50%;top:50%;width:1em;height:1em;margin-left:-.5em;margin-top:-.5em;border:2px solid currentColor;border-top-color:transparent;border-radius:var(--radius-full);animation:spin .8s linear infinite}.hidden{display:none!important}.project-dashboard-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:1.5rem;flex:1;min-height:0}.dashboard-column{background:var(--bg-card);border:var(--border-width) solid var(--border-color);padding:1rem;display:flex;flex-direction:column;overflow:hidden}.dashboard-column-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;padding-bottom:.5rem;border-bottom:2px solid var(--border-color)}.dashboard-column-header h3{margin:0;font-size:1rem;font-family:var(--font-heading);font-weight:700}.dashboard-list{flex:1;overflow-y:auto}.dashboard-item{padding:.75rem;background:var(--bg-card);border:var(--border-width-sm) solid var(--border-color);margin-bottom:.5rem;cursor:pointer;box-shadow:var(--shadow-offset) var(--shadow-offset) 0 var(--border-color);transition:transform .15s ease,box-shadow .15s ease,background-color .15s ease}.dashboard-item:hover{background:var(--bg-secondary);transform:translate(-2px,-2px);box-shadow:calc(var(--shadow-offset) + 2px) calc(var(--shadow-offset) + 2px) 0 var(--border-color)}.dashboard-item-title{font-weight:600;margin-bottom:.25rem}.dashboard-item-meta{font-size:.75rem;color:var(--text-secondary)}.task-badges{display:flex;gap:.25rem;margin-top:.25rem}.task-badge{font-size:.65rem;padding:.15rem .4rem;border:var(--border-width-sm) solid var(--border-color);background:var(--bg-secondary);color:var(--text-primary);font-weight:600}.task-badge.has-items{background:var(--accent-blue);color:var(--text-on-accent)}.task-badge.recurrence{background:var(--accent-purple);color:var(--text-on-accent)}.task-row-clickable{cursor:pointer;transition:background .1s}.task-row-clickable:hover{background:var(--bg-secondary)}.progress-bar-container{width:100%;height:10px;background:var(--bg-secondary);border:var(--border-width-sm) solid var(--border-color);overflow:hidden}.progress-bar{height:100%;background:var(--accent-green);transition:width .3s ease}.no-subtasks{color:var(--text-secondary);font-size:.875rem}#day-plan-view{display:flex;flex-direction:column;flex:1;min-height:0}#day-plan-view .page-header{flex-shrink:0}.day-plan-nav{display:flex;align-items:center;gap:.5rem}.day-plan-date-picker{padding:.5rem;background:var(--bg-card);border:var(--border-width) solid var(--border-color);border-radius:var(--radius-sm);color:var(--text-primary);font-family:var(--font-body)}.day-plan-date-display{font-size:1.25rem;font-weight:700;margin-left:1rem;font-family:var(--font-heading);line-height:1}.day-plan-content{flex:1;min-height:0;display:flex;gap:1.5rem}.day-plan-main{flex:1;min-height:0;display:flex;flex-direction:column;background:var(--bg-card);border:var(--border-width) solid var(--border-color);border-radius:var(--radius-md);overflow:hidden;box-shadow:var(--shadow-offset) var(--shadow-offset) 0 var(--border-color)}.day-plan-sidebar{width:280px;flex-shrink:0;display:flex;flex-direction:column;background:var(--bg-card);border:var(--border-width) solid var(--border-color);border-radius:var(--radius-md);overflow:hidden;box-shadow:var(--shadow-offset) var(--shadow-offset) 0 var(--border-color)}.sidebar-header{padding:1rem;border-bottom:2px solid var(--border-color);flex-shrink:0}.sidebar-header h3{margin:0;font-size:1rem;font-family:var(--font-heading);font-weight:700}.sidebar-task-list{flex:1;overflow-y:auto;padding:.75rem;display:flex;flex-direction:column;gap:.5rem}.timeline-container{flex:1;min-height:0;overflow-y:auto;overflow-x:hidden}.timeline-scroll-area{position:relative;padding:.5rem 1rem 3rem .5rem;min-height:min-content}#timeline-slots{position:relative}#timeline-items{position:absolute;top:.5rem;left:.5rem;right:1rem;bottom:0;pointer-events:none}#timeline-items .timeline-item{pointer-events:auto}.timeline-slot{display:grid;grid-template-columns:50px 1fr;height:var(--timeline-slot-h,12px);position:relative}.timeline-slot.hour-start .timeline-slot-area{border-top:1px dashed color-mix(in srgb,var(--border-color) 50%,transparent)}.timeline-time{font-size:.7rem;color:var(--text-secondary);padding-right:.5rem;text-align:right;font-weight:500;transform:translateY(-.5em)}.timeline-hint{text-align:center;color:var(--text-secondary);font-size:.85rem;font-weight:600;margin:0 0 .5rem;padding:.35rem .75rem;background:var(--bg-secondary);border-radius:var(--radius-sm)}.timeline-slot-area{position:relative;cursor:grab}.timeline-slot-area:hover{background:var(--bg-secondary)}.timeline-item{position:absolute;left:60px;right:10px;border:var(--border-width) solid var(--border-color);border-radius:var(--radius-sm);padding:.25rem .5rem;overflow:hidden;cursor:grab;z-index:10;transition:opacity .15s ease,box-shadow .15s ease}.timeline-item.task{background:var(--accent-green);color:var(--text-primary)}.timeline-item.event{background:var(--accent-blue);color:var(--text-on-accent)}.timeline-item.block{opacity:.85}.timeline-item.block-free_time{background:var(--accent-cyan);color:var(--text-primary)}.timeline-item.block-personal{background:var(--accent-yellow);color:var(--text-primary)}.timeline-item.block-vacation{background:var(--accent-purple);color:var(--text-on-accent)}.timeline-item.block-focus{background:var(--accent-red);color:var(--text-on-accent)}.timeline-item.conflict{box-shadow:0 0 0 3px var(--accent-red)}.timeline-item.selected{box-shadow:0 0 0 3px var(--bg-card),0 0 0 6px var(--accent-blue)}.timeline-item-title{font-weight:600;font-size:.75rem;line-height:1.2;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.timeline-item-meta{font-size:.65rem;opacity:.85;line-height:1.1}.timeline-current-time{position:absolute;left:50px;right:0;height:2px;background:var(--accent-red);z-index:20;pointer-events:none}.timeline-current-time::before{content:'';position:absolute;left:-4px;top:-3px;width:8px;height:8px;background:var(--accent-red);border-radius:var(--radius-full)}.timeline-paint-preview{position:absolute;left:70px;right:10px;background:var(--accent-blue);opacity:.4;border:var(--border-width-sm) dashed var(--border-color);border-radius:var(--radius-sm);z-index:5;pointer-events:none}.timeline-item.dragging{cursor:grabbing;opacity:.8;z-index:100;box-shadow:var(--shadow-brutal-md,4px 4px 0 var(--border-color))}.timeline-container.is-painting{cursor:crosshair;user-select:none}.timeline-container.is-painting .timeline-slot-area{pointer-events:none}.unscheduled-task{padding:.75rem;background:var(--bg-card);border:var(--border-width-sm) solid var(--border-color);border-left:6px solid var(--accent-green);border-radius:var(--radius-sm);cursor:grab;transition:background-color .1s}.unscheduled-task:hover{background:var(--bg-secondary)}.unscheduled-task.priority-high{border-left-color:var(--accent-red)}.unscheduled-task.priority-medium{border-left-color:var(--accent-yellow)}.unscheduled-task.priority-low{border-left-color:var(--accent-green)}.unscheduled-task-title{font-weight:600;margin-bottom:.25rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.unscheduled-task-meta{font-size:.75rem;color:var(--text-secondary)}.empty-unscheduled{text-align:center;color:var(--text-secondary);padding:2rem 1rem}.settings-btn{background:var(--bg-card);border:var(--border-width) solid var(--border-color);border-radius:var(--radius-sm);color:var(--text-primary);font-size:1.25rem;cursor:pointer;padding:.5rem .75rem;margin-left:.5rem;transition:background-color .1s}.settings-btn:hover{background:var(--bg-secondary)}.settings-btn:active{background:var(--bg-tertiary)}.shortcut-hint-btn{font-family:var(--font-mono, monospace);font-weight:700;min-width:2rem;text-align:center;padding:.5rem}.kbd-hint{display:inline-block;font-family:var(--font-mono, monospace);font-size:.65rem;font-weight:600;padding:.1rem .35rem;margin-left:.35rem;border:1px solid currentColor;border-radius:3px;opacity:.6;vertical-align:middle;line-height:1}.settings-section h3{font-size:1rem;color:var(--text-primary)}.settings-section .form-hint{font-size:.75rem;color:var(--text-secondary)}.settings-overlay{position:fixed;inset:0;z-index:70;display:flex;align-items:center;justify-content:center;padding:var(--space-4)}.settings-overlay.hidden{display:none}.settings-overlay-backdrop{position:absolute;inset:0;background:rgba(0,0,0,.35)}.settings-overlay-card{position:relative;background:var(--bg-primary);border:1px solid var(--border-color);border-radius:var(--radius-lg,8px);box-shadow:0 12px 40px rgba(0,0,0,.25);width:min(960px,100%);height:min(720px,100%);max-height:calc(100vh - 2 * var(--space-4));overflow:hidden;display:flex;flex-direction:column}.settings-overlay-card.hidden{display:none}.visually-hidden{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}@media (max-width:768px){.settings-overlay{padding:0}.settings-overlay-card{width:100%;height:100%;max-height:100%;border-radius:0;border:none}}.settings-page-layout{display:flex;min-height:100%}.settings-sidebar{width:200px;flex-shrink:0;border-right:1px solid var(--border-color);padding:1.5rem 0;display:flex;flex-direction:column;gap:.25rem}.settings-back-btn{background:0 0;border:none;text-align:left;padding:.5rem 1.25rem;font-size:.875rem;color:var(--text-secondary);cursor:pointer;margin-bottom:1rem}.settings-back-btn:hover{color:var(--text-primary)}.settings-nav-items{display:flex;flex-direction:column;gap:.125rem}.settings-nav-item{background:0 0;border:none;text-align:left;padding:.5rem 1.25rem;font-size:.875rem;color:var(--text-secondary);cursor:pointer;border-radius:0;border-left:2px solid transparent}.settings-nav-item:hover{background:var(--bg-hover);color:var(--text-primary)}.settings-nav-item.active{color:var(--text-primary);font-weight:600;border-left-color:var(--accent-primary);background:var(--bg-secondary)}.settings-content{flex:1;padding:1.5rem 2rem;max-width:640px;overflow-y:auto}.contact-header-card{display:flex;align-items:center;gap:1.25rem;margin-bottom:1.5rem}.contact-avatar-large{width:64px;height:64px;border-radius:50%;background:var(--accent-primary);color:#fff;display:flex;align-items:center;justify-content:center;font-size:1.5rem;font-weight:700;flex-shrink:0}.contact-info-section{display:flex;flex-direction:column;gap:.375rem;margin-bottom:1.5rem;padding:1rem;background:var(--bg-secondary);border-radius:var(--radius-md)}.contact-info-item{font-size:.875rem}.contact-dashboard-summary{display:flex;gap:1.5rem;margin-bottom:1.5rem}.contact-summary-stat{display:flex;flex-direction:column;align-items:center;padding:.75rem 1.25rem;background:var(--bg-secondary);border-radius:var(--radius-md);min-width:80px}.contact-summary-count{font-size:1.5rem;font-weight:700;color:var(--text-primary)}.contact-summary-label{font-size:.75rem;color:var(--text-secondary);margin-top:.25rem}.contact-timeline{display:flex;flex-direction:column;gap:.25rem}.contact-timeline-item{display:flex;align-items:center;gap:.75rem;padding:.5rem .75rem;border-radius:var(--radius-sm);cursor:pointer;font-size:.875rem}.contact-timeline-item:hover{background:var(--bg-hover)}.contact-timeline-icon{flex-shrink:0;width:2rem;text-align:center;font-size:.8rem}.contact-timeline-title{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.contact-timeline-date{flex-shrink:0;font-size:.75rem;color:var(--text-secondary)}.address-highlight-mirror{position:absolute;top:0;left:0;right:0;bottom:0;pointer-events:none;white-space:nowrap;overflow:hidden;z-index:0;display:flex;align-items:center}.addr-malformed{color:var(--accent-red)}.addr-contact{color:var(--accent-blue)}.addr-verified{color:var(--accent-green)}.addr-ghost{color:var(--text-muted);opacity:.5}.sync-indicator{background:0 0;border:none;cursor:pointer;padding:.25rem .5rem;display:flex;align-items:center;gap:var(--space-2);border-radius:var(--radius-sm);transition:background var(--transition-fast)}.sync-indicator:hover{background:var(--bg-hover)}.sync-dot{width:8px;height:8px;border-radius:var(--radius-full);background:var(--text-muted);transition:background var(--transition-slow);flex-shrink:0}.sync-dot.connected{background:var(--accent-green)}.sync-dot.syncing{background:var(--accent-blue);animation:sync-pulse 1s infinite}.sync-dot.warn{background:var(--accent-yellow)}.sync-dot.error{background:var(--accent-red)}.sync-label{font-size:var(--font-size-sm);color:var(--text-secondary);white-space:nowrap}@media (max-width:1100px){.sync-label{display:none}}@keyframes sync-pulse{0%,100%{opacity:1}50%{opacity:.4}}.snooze-options{display:flex;flex-direction:column;gap:.5rem}.snooze-option{display:flex;justify-content:space-between;align-items:center;padding:.75rem 1rem;background:var(--bg-secondary);border:var(--border-width-sm) solid var(--border-color);color:var(--text-primary);cursor:pointer;transition:background-color .1s;text-align:left;width:100%}.snooze-option:hover{background:var(--accent-blue);color:var(--text-on-accent)}.snooze-option-label{font-weight:600}.snooze-option-time{font-size:.75rem;color:var(--text-secondary)}.snooze-option:hover .snooze-option-time{color:var(--text-on-accent)}.snooze-custom{margin-top:.5rem;padding-top:.5rem;border-top:2px solid var(--border-color)}.snooze-badge{display:inline-block;font-size:.65rem;padding:.15rem .4rem;border:var(--border-width-sm) solid var(--border-color);background:var(--accent-yellow);color:var(--text-primary);font-weight:700;margin-top:.25rem}.contact-badge{display:inline-block;font-size:.65rem;padding:.15rem .4rem;border:var(--border-width-sm) solid var(--border-color);background:var(--accent-color);color:var(--bg-primary);font-weight:700;margin-top:.25rem}.bulk-checkbox{width:18px;height:18px;cursor:pointer;accent-color:var(--accent-blue);border:var(--border-width-sm) solid var(--border-color)}.task-actions-cell{text-align:right;white-space:nowrap;display:flex;align-items:center;justify-content:flex-end;gap:.5rem}.task-actions-cell .bulk-checkbox{margin-right:.5rem}.kebab-btn{background:0 0;border:none;cursor:pointer;font-size:1.1rem;line-height:1;padding:.2rem .4rem;border-radius:var(--radius-sm);color:var(--text-secondary);opacity:0;transition:opacity .15s ease}.email-item:focus-within .kebab-btn,.email-item:hover .kebab-btn,.event-row-virtual:focus-within .kebab-btn,.event-row-virtual:hover .kebab-btn,.task-row:focus-within .kebab-btn,.task-row:hover .kebab-btn{opacity:1}.kebab-btn:hover{background:var(--bg-hover);color:var(--text-primary)}.task-recurrence{font-size:.85rem;color:var(--text-secondary)}.task-due{white-space:nowrap}.bulk-actions-bar{display:flex;align-items:center;gap:.5rem;padding:.75rem 1rem;background:var(--accent-blue);color:var(--text-on-accent);border:var(--border-width) solid var(--border-color);box-shadow:var(--shadow-offset) var(--shadow-offset) 0 var(--border-color);margin-bottom:1rem;color:var(--text-primary)}.bulk-actions-bar.hidden{display:none}.bulk-count{font-weight:700;margin-right:1rem;font-family:var(--font-heading)}.bulk-actions-bar .btn{background:var(--bg-card);border:var(--border-width-sm) solid var(--border-color);color:var(--text-primary)}.bulk-actions-bar .btn:hover{background:var(--bg-secondary)}.bulk-select-all{margin-left:auto}.email-checkbox-cell{padding:.75rem .5rem;display:flex;align-items:center}.email-item-with-checkbox{display:flex;align-items:flex-start}.email-item-with-checkbox .email-content{flex:1}.schedule-task-btn{display:flex;align-items:center;gap:.5rem}.time-block-form{display:flex;flex-direction:column;gap:1rem}.time-block-quick-options{display:grid;grid-template-columns:repeat(3,1fr);gap:.5rem}.time-block-quick-btn{padding:.5rem;background:var(--bg-secondary);border:var(--border-width-sm) solid var(--border-color);color:var(--text-primary);cursor:pointer;font-size:.875rem;font-weight:600;transition:background-color .1s}.time-block-quick-btn:hover{background:var(--bg-tertiary)}.time-block-quick-btn.selected{background:var(--accent-blue);color:var(--text-on-accent);box-shadow:inset 0 0 0 2px var(--border-color)}.duration-presets{display:flex;gap:.5rem;flex-wrap:wrap}.duration-preset{padding:.35rem .75rem;background:var(--bg-secondary);border:var(--border-width-sm) solid var(--border-color);color:var(--text-primary);cursor:pointer;font-size:.75rem;font-weight:600;transition:background-color .1s}.duration-preset:hover{background:var(--bg-tertiary)}.duration-preset.selected{background:var(--accent-blue);color:var(--text-on-accent)}.conflict-warning{padding:.75rem;background:var(--accent-red);border:var(--border-width) solid var(--border-color);color:var(--text-on-accent);font-size:.875rem;font-weight:600;margin-top:.5rem}.app-body{display:flex;flex:1;min-height:0;overflow:hidden}.app-body .main-content{flex:1;min-width:0;display:flex;flex-direction:column;overflow-x:visible;overflow-y:auto}#emails-view,#events-view,#projects-view,#tasks-view{padding-bottom:2.5rem}#tasks-view{display:flex;flex-direction:column;flex:1;min-height:0}#tasks-view .bulk-actions-bar,#tasks-view .filter-bar,#tasks-view .page-header{flex-shrink:0}#events-view{display:flex;flex-direction:column;flex:1;min-height:0}#events-view .page-header{flex-shrink:0}#emails-view{display:flex;flex-direction:column;flex:1;min-height:0}#emails-view .bulk-actions-bar,#emails-view .page-header{flex-shrink:0}.saved-views-sidebar{width:200px;flex-shrink:0;background:var(--bg-card);border-right:var(--border-width) solid var(--border-color);display:flex;flex-direction:column;overflow:hidden}.sidebar-section{display:flex;flex-direction:column;flex:1;min-height:0}.sidebar-section-header{display:flex;justify-content:space-between;align-items:center;padding:.75rem 1rem;font-size:.75rem;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--text-secondary);border-bottom:2px solid var(--border-color);background:var(--bg-secondary)}.btn-icon{background:0 0;border:none;color:var(--text-muted);cursor:pointer;padding:.25rem;font-size:.875rem;line-height:1}.btn-icon:hover{color:var(--text-primary)}.pinned-views-list{flex:1;overflow-y:auto;padding:.5rem}.sidebar-empty{text-align:center;padding:1.5rem .5rem;color:var(--text-muted);font-size:.8rem}.saved-view-item{display:flex;align-items:center;gap:.5rem;padding:.5rem .75rem;margin-bottom:.5rem;background:var(--bg-card);border:var(--border-width-sm) solid var(--border-color);border-radius:var(--radius-sm);cursor:pointer;font-size:.85rem;font-weight:600;color:var(--text-primary);box-shadow:var(--shadow-offset) var(--shadow-offset) 0 var(--border-color);transition:transform .15s ease,box-shadow .15s ease,background-color .15s ease,color .15s ease}.saved-view-item:hover{background:var(--accent-blue);color:var(--text-on-accent)}.saved-view-item.active{background:var(--accent-blue);color:var(--text-on-accent);box-shadow:inset 0 0 0 2px var(--border-color)}.saved-view-item .view-icon{font-size:.75rem}.saved-view-item .view-name{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.saved-view-item .view-actions{opacity:0;transition:opacity .1s}.saved-view-item:hover .view-actions{opacity:1}.filter-actions{display:flex;gap:.5rem;margin-left:auto}.contact-avatar{width:40px;height:40px;min-width:40px;border-radius:50%;background-color:var(--accent-blue);color:var(--text-on-accent);display:flex;align-items:center;justify-content:center;font-weight:700;font-size:.85rem;font-family:var(--font-heading);border:2px solid var(--border-color)}.contact-avatar-lg{width:60px;height:60px;min-width:60px;font-size:1.2rem}.contact-card .card-header{display:flex;align-items:center}.contact-nickname{display:block;font-size:.85rem;color:var(--text-secondary);font-style:italic}.contact-company{display:block;font-size:.85rem;color:var(--text-secondary)}.contact-email{font-size:.85rem;color:var(--text-secondary)}.contact-detail .detail-row{margin-bottom:.5rem;font-size:.9rem}.contact-detail .contact-info-section{margin-bottom:1rem;padding-bottom:1rem;border-bottom:1px solid var(--border-light)}.contact-detail .contact-notes{margin-bottom:1.5rem}.contact-detail .contact-notes p{margin-top:.25rem;white-space:pre-wrap;color:var(--text-secondary)}.sub-collection{margin-bottom:1.25rem}.sub-collection-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:.5rem}.sub-collection-header h4{margin:0;font-size:.95rem;font-weight:600}.sub-item{display:flex;justify-content:space-between;align-items:center;padding:.4rem 0;border-bottom:1px solid var(--border-light);font-size:.9rem}.sub-item:last-child{border-bottom:none}.sub-empty{font-size:.85rem;color:var(--text-secondary);font-style:italic;padding:.25rem 0}.edit-sub-collections{border-top:1px solid var(--border-color);padding-top:1rem;margin-bottom:.5rem}.edit-sub-section{margin-bottom:.75rem}.edit-sub-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:.25rem}.sub-item-compact{font-size:.85rem;color:var(--text-secondary);padding:.125rem 0;display:flex;align-items:center;gap:var(--space-2);justify-content:space-between}.sub-item-actions{display:inline-flex;gap:var(--space-1);flex-shrink:0}.reminder-options{display:flex;flex-wrap:wrap;gap:var(--space-2) var(--space-4);margin-top:var(--space-2)}.reminder-option{display:inline-flex;align-items:center;gap:var(--space-2);font-size:var(--font-size-sm);color:var(--text-secondary);cursor:pointer}.task-drawer{position:fixed;top:0;right:0;bottom:0;width:min(480px,100vw);background:var(--bg-primary);border-left:1px solid var(--border-color);box-shadow:-4px 0 18px rgba(0,0,0,.18);transform:translateX(100%);transition:transform 180ms ease-out;z-index:60;display:flex;flex-direction:column;overflow:hidden}.task-drawer.visible{transform:translateX(0)}.task-drawer-header{display:flex;align-items:center;gap:var(--space-3);padding:var(--space-3) var(--space-4);border-bottom:1px solid var(--border-color);background:var(--bg-secondary);flex-shrink:0}.task-drawer-title{flex:1;margin:0;font-size:var(--font-size-lg, 1.05rem);font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.task-drawer-close{flex-shrink:0}.task-drawer-actions{display:flex;gap:var(--space-2);flex-shrink:0}.task-drawer-content{flex:1;overflow-y:auto;padding:var(--space-4)}.task-row--active{background:var(--bg-accent,var(--bg-secondary));box-shadow:inset 3px 0 0 var(--accent-color,var(--text-primary))}@media (max-width:768px){.task-drawer{width:100vw;border-left:none}}@media print{.btn,.context-menu,.filter-bar,.keyboard-hints,.modal-overlay,.pagination,.sidebar,.tabs,.toast{display:none!important}body{background:#fff;color:#000}.main-content{margin:0;padding:0;max-width:100%}.view{padding:0}.data-table{border:1px solid #333;box-shadow:none}.data-table td,.data-table th{border:1px solid #ccc;padding:.5rem}.data-table td,.data-table th{display:table-cell!important}.data-table tbody tr:hover{background:0 0}.task-table{border:1px solid #333;box-shadow:none}.task-list-container{height:auto!important;overflow:visible!important}.task-header-row,.task-row{grid-template-columns:1fr 100px 40px 80px 60px 80px 60px!important}.task-header-row .task-cell,.task-row .task-cell{display:block!important;border:1px solid #ccc;padding:.25rem .5rem}.task-row:hover{background:0 0}.virtual-scroller-spacer{display:none!important}a{color:#000;text-decoration:underline}.view-header{page-break-after:avoid}.data-table{page-break-inside:avoid}}.weekly-review-content{max-width:900px;margin:0 auto;padding:1rem}.weekly-review-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:1.5rem;padding-bottom:1rem;border-bottom:var(--border-width-sm) solid var(--border-color)}.week-info{display:flex;align-items:center;gap:1rem}.week-dates{font-family:var(--font-heading);font-size:1.25rem;font-weight:700;color:var(--text-primary)}.review-status{padding:.25rem .75rem;border-radius:var(--radius-xs);font-size:.875rem;font-weight:600;border:var(--border-width-sm) solid var(--border-color)}.review-status.completed{background:var(--accent-green);color:var(--text-on-accent)}.review-status.pending{background:var(--accent-yellow);color:var(--text-primary)}.review-status.unreviewed{background:var(--bg-secondary);color:var(--text-muted)}.stat-cards{display:flex;gap:1rem;margin-bottom:1rem;flex-wrap:wrap}.stat-card{flex:1;min-width:100px;max-width:150px;padding:1rem;background:var(--bg-card);border:var(--border-width) solid var(--border-color);text-align:center;box-shadow:var(--shadow-offset) var(--shadow-offset) 0 var(--border-color)}.stat-card .stat-number{display:block;font-family:var(--font-heading);font-size:2rem;font-weight:700;color:var(--accent-blue);line-height:1}.stat-card .stat-label{display:block;font-size:.75rem;font-weight:600;color:var(--text-muted);margin-top:.25rem;text-transform:uppercase;letter-spacing:.5px}.stat-card.stat-warning .stat-number{color:var(--accent-yellow)}.stat-card.stat-danger .stat-number{color:var(--accent-red)}.review-section{background:var(--bg-card);border:var(--border-width) solid var(--border-color);padding:1.25rem;margin-bottom:1.5rem;box-shadow:var(--shadow-offset) var(--shadow-offset) 0 var(--border-color)}.section-title{font-family:var(--font-heading);font-size:1.125rem;font-weight:700;color:var(--text-primary);margin-bottom:1rem;padding-bottom:.5rem;border-bottom:var(--border-width-sm) solid var(--border-color)}.review-details{margin-top:.75rem}.review-details summary{cursor:pointer;font-weight:600;color:var(--text-secondary);padding:.5rem;background:var(--bg-secondary);border:var(--border-width-sm) solid var(--border-color);user-select:none}.review-details summary:hover{background:var(--bg-tertiary)}.review-details[open] summary{margin-bottom:.5rem}.review-event-list,.review-task-list{list-style:none;padding:0;margin:0}.review-event-item,.review-task-item{display:flex;align-items:center;gap:.75rem;padding:.5rem .75rem;border-bottom:1px solid var(--border-color)}.review-event-item:last-child,.review-task-item:last-child{border-bottom:none}.review-event-item .event-title,.review-task-item .task-description{flex:1;color:var(--text-primary)}.event-time{font-size:.875rem;font-weight:600;color:var(--text-muted);min-width:80px}.project-badge{font-size:.75rem;padding:.125rem .5rem;background:var(--bg-tertiary);border:1px solid var(--border-color);color:var(--text-secondary)}.due-badge{font-size:.75rem;padding:.125rem .5rem;background:var(--bg-secondary);border:1px solid var(--border-color);color:var(--text-secondary)}.due-badge.overdue{background:var(--accent-red);color:var(--text-on-accent);border-color:var(--accent-red)}.focus-section{background:linear-gradient(135deg,var(--bg-card) 0,color-mix(in srgb,var(--accent-yellow) 15%,var(--bg-card)) 100%)}.focus-task-list{list-style:none;padding:0;margin:0 0 1rem 0}.focus-task-list.available{opacity:.8}.focus-toggle{background:0 0;border:none;font-size:1.25rem;cursor:pointer;color:var(--text-muted);padding:0;line-height:1;transition:transform .15s ease}.focus-toggle:hover{transform:scale(1.2)}.focus-toggle.focused{color:var(--accent-yellow)}.review-task-item.focused{background:color-mix(in srgb,var(--accent-yellow) 10%,var(--bg-card))}.no-focus-message{color:var(--text-muted);font-style:italic;margin-bottom:1rem}.focused-projects{display:flex;gap:.5rem;flex-wrap:wrap;margin-bottom:1rem}.project-tag{background:var(--accent-blue);color:var(--text-on-accent);padding:.25rem .75rem;font-size:.875rem;font-weight:600;border:var(--border-width-sm) solid var(--border-color)}.notes-section{background:var(--bg-card)}.review-notes-input{width:100%;padding:.75rem;font-family:var(--font-mono);font-size:.9rem;border:var(--border-width-sm) solid var(--border-color);background:var(--bg-secondary);color:var(--text-primary);resize:vertical;min-height:100px}.review-notes-input:focus{outline:0;background:var(--bg-card);box-shadow:inset 0 0 0 2px var(--accent-blue)}.review-actions{margin-top:1rem;text-align:center}.tab-badge{display:inline-block;width:8px;height:8px;background:var(--accent-red);border-radius:var(--radius-full);margin-left:.5rem;vertical-align:middle;animation:pulse-badge 2s infinite}@keyframes pulse-badge{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.6;transform:scale(.8)}}.tab-status-dot{width:8px;height:8px;border-radius:50%;display:inline-block;margin-left:.5rem;vertical-align:middle;transition:background-color .3s ease}.tab-status-dot.status-none{display:none}.tab-status-dot.status-green{background-color:var(--accent-green)}.tab-status-dot.status-yellow{background-color:var(--accent-yellow);animation:pulse-badge 2s ease-in-out infinite}.tab-status-dot.status-red{background-color:var(--accent-red);animation:pulse-badge 1.5s ease-in-out infinite}.review-grid{display:grid;grid-template-columns:1fr 1fr;gap:1.5rem;max-width:1200px;margin:0 auto}.review-card{background:var(--bg-card);border:var(--border-width) solid var(--border-color);border-radius:var(--radius-md);padding:1.5rem;box-shadow:var(--shadow-offset) var(--shadow-offset) 0 var(--border-color)}.review-card .card-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;padding-bottom:.75rem;border-bottom:var(--border-width-sm) solid var(--bg-secondary)}.review-card .card-title{font-family:var(--font-heading);font-size:1.1rem;font-weight:700;display:flex;align-items:center;gap:.5rem}.review-card .card-icon{font-size:1.25rem}.review-card .card-badge{font-size:.8rem;padding:.25rem .75rem;border-radius:var(--radius-md);font-weight:600}.week-timeline{grid-column:1/-1}.timeline-visual{display:flex;gap:.5rem;margin-top:1rem}.timeline-day{flex:1;text-align:center;padding:.75rem .5rem;background:var(--bg-secondary);border-radius:var(--radius-md);border:1px solid var(--border-color);position:relative}.timeline-day.today{background:var(--accent-blue);color:var(--text-on-accent);border-width:2px;font-weight:700}.timeline-day.past{opacity:.7}.timeline-day.future{background:var(--bg-card)}.timeline-day .day-name{font-size:.7rem;font-weight:600;text-transform:uppercase;color:var(--text-muted)}.timeline-day .day-number{font-size:1.1rem;font-weight:700}.day-dots{display:flex;justify-content:center;gap:3px;margin-top:.5rem;min-height:8px}.day-dot{width:8px;height:8px;border-radius:var(--radius-full)}.day-dot.task{background:var(--accent-blue)}.day-dot.event{background:var(--accent-purple)}.day-dot.completed{background:var(--accent-green)}.day-dot.overdue{background:var(--accent-red)}.day-dot.vacation-off{background:var(--text-muted);opacity:.5;width:12px;height:4px;border-radius:2px}.day-events{display:flex;flex-direction:column;gap:2px;margin-top:.5rem;text-align:left}.day-event{font-size:.6rem;line-height:1.3;padding:1px 4px;border-left:2px solid var(--accent-purple);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--text-secondary)}.day-event .event-time{font-size:.55rem;font-weight:600;color:var(--accent-purple);margin-right:2px;min-width:auto}.day-event-more{font-size:.55rem;color:var(--text-muted);padding:1px 4px;font-style:italic}.week-timeline-events{grid-column:1/-1}.timeline-events-day{margin-bottom:.75rem}.timeline-events-day:last-child{margin-bottom:0}.timeline-events-day-label{font-family:var(--font-heading);font-size:.8rem;font-weight:700;color:var(--text-secondary);margin-bottom:.25rem;text-transform:uppercase}.vacation-toggles-section{margin-top:1rem;padding-top:1rem;border-top:2px solid var(--border-color)}.vacation-toggles-section h3{margin:0 0 .75rem 0;font-size:.9rem;font-family:var(--font-heading);font-weight:700}.vacation-toggles{display:flex;gap:.5rem}.vacation-toggle{width:2.5rem;height:2.5rem;border-radius:var(--radius-sm);border:var(--border-width) solid var(--border-color);background:var(--bg-secondary);font-family:var(--font-heading);font-weight:700;font-size:.8rem;cursor:pointer;transition:background var(--transition-fast),color var(--transition-fast),border-color var(--transition-fast);display:flex;align-items:center;justify-content:center}.vacation-toggle:hover{background:var(--bg-hover)}.vacation-toggle.active{background:var(--accent-purple);color:var(--text-on-accent);border-color:var(--accent-purple)}.timeline-day.vacation{opacity:.5}.timeline-day.vacation .day-name{text-decoration:line-through}.vacation-day-banner{text-align:center;padding:.5rem 1rem;background:color-mix(in srgb,var(--accent-purple) 15%,var(--bg-secondary));border:var(--border-width-sm) solid var(--accent-purple);border-radius:var(--radius-sm);font-family:var(--font-heading);font-weight:700;font-size:.85rem;color:var(--accent-purple);margin-bottom:.75rem}.stats-row{display:flex;gap:1rem;margin-bottom:1rem}.stat-box{flex:1;text-align:center;padding:1rem;background:var(--bg-secondary);border-radius:var(--radius-md)}.stat-box .stat-number{font-family:var(--font-heading);font-size:2rem;font-weight:800;line-height:1}.stat-box .stat-number.green{color:var(--accent-green)}.stat-box .stat-number.red{color:var(--accent-red)}.stat-box .stat-number.blue{color:var(--accent-blue)}.stat-box .stat-number.purple{color:var(--accent-purple)}.stat-box .stat-label{font-size:.75rem;text-transform:uppercase;color:var(--text-muted);font-weight:600;margin-top:.25rem}.task-list{list-style:none;max-height:200px;overflow-y:auto}.task-item{display:flex;align-items:center;gap:.75rem;padding:.75rem;margin-bottom:.5rem;background:var(--bg-secondary);border-radius:var(--radius-md);cursor:pointer;transition:background-color var(--transition-normal)}.task-item:hover{background:var(--accent-blue);color:var(--text-on-accent)}.task-item.completed{opacity:.6;text-decoration:line-through}.task-checkbox{width:20px;height:20px;border:2px solid var(--border-color);border-radius:var(--radius-xs);display:flex;align-items:center;justify-content:center;flex-shrink:0}.task-checkbox.checked{background:var(--accent-green);color:var(--text-on-accent)}.task-text{flex:1;font-size:.9rem}.task-project{font-size:.75rem;padding:.2rem .5rem;background:var(--bg-card);border-radius:var(--radius-xs);color:var(--text-muted)}.task-due{font-size:.75rem;color:var(--text-muted)}.task-due.overdue{color:var(--accent-red);font-weight:600}.focus-section.full-width{grid-column:1/-1}.scope-slots{display:grid;grid-template-columns:1fr 1fr 1fr;gap:1rem;margin-top:1rem}.scope-slot{padding:1.25rem;background:var(--bg-secondary);border:2px dashed var(--border-color);border-radius:var(--radius-md);min-height:100px;display:flex;flex-direction:column;gap:.5rem;transition:background .15s,border-color .15s}.scope-slot.filled{border-style:solid;background:var(--bg-card)}.scope-slot.empty{cursor:pointer}.scope-slot.empty:hover{border-color:var(--accent-primary);background:var(--bg-tertiary)}.scope-slot-label{font-size:.7rem;text-transform:uppercase;letter-spacing:.05em;color:var(--text-muted);font-weight:600}.scope-slot-title{font-weight:600;font-size:.95rem}.scope-slot-meta{font-size:.8rem;color:var(--text-secondary)}.scope-slot-empty{color:var(--text-muted);font-style:italic;font-size:.9rem}.focus-slot.primary{border-color:var(--accent-yellow);background:linear-gradient(135deg,var(--bg-card) 0,color-mix(in srgb,var(--accent-yellow) 10%,var(--bg-card)) 100%)}.projects-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:.75rem;margin-top:.5rem}.project-health{padding:.75rem;background:var(--bg-secondary);border-radius:var(--radius-md);border-left:4px solid var(--accent-blue)}.project-health.warning{border-left-color:var(--accent-yellow)}.project-health.danger{border-left-color:var(--accent-red)}.project-name{font-weight:600;font-size:.85rem;margin-bottom:.25rem}.project-stats{font-size:.75rem;color:var(--text-muted)}.reflection-section{grid-column:1/-1}.reflection-prompts{display:grid;grid-template-columns:1fr 1fr;gap:1rem;margin-top:1rem}.reflection-prompt{padding:1rem;background:var(--bg-secondary);border-radius:var(--radius-md)}.prompt-label{font-size:.8rem;font-weight:600;color:var(--text-secondary);margin-bottom:.5rem}.prompt-input,.reflection-textarea{width:100%;padding:.75rem;border:var(--border-width-sm) solid var(--border-color);border-radius:var(--radius-md);font-size:.9rem;font-family:inherit;resize:none;background:var(--bg-card)}.prompt-input:focus,.reflection-textarea:focus{outline:0;border-color:var(--accent-blue)}.review-actions-grid{grid-column:1/-1;display:flex;justify-content:flex-end;gap:1rem;padding-top:1rem}.event-item{display:flex;align-items:center;gap:.75rem;padding:.75rem;margin-bottom:.5rem;background:var(--bg-secondary);border-radius:var(--radius-md);border-left:3px solid var(--accent-purple)}.event-item .event-time{font-size:.8rem;font-weight:600;color:var(--accent-purple);min-width:100px}.event-item .event-title{flex:1;font-size:.9rem}.accomplishment-highlight{background:linear-gradient(135deg,color-mix(in srgb,var(--accent-green) 10%,var(--bg-card)) 0,color-mix(in srgb,var(--accent-green) 5%,var(--bg-card)) 100%);border:2px solid var(--accent-green);padding:1rem;border-radius:var(--radius-md);margin-bottom:1rem;display:flex;align-items:center;gap:1rem}.accomplishment-icon{font-size:2rem}.accomplishment-text{font-size:1rem}.accomplishment-text strong{color:var(--accent-green)}.task-list::-webkit-scrollbar{width:6px}.task-list::-webkit-scrollbar-track{background:var(--bg-secondary);border-radius:var(--radius-xs)}.task-list::-webkit-scrollbar-thumb{background:var(--border-color);border-radius:var(--radius-xs)}@media (max-width:900px){.review-grid{grid-template-columns:1fr}.focus-section.full-width,.reflection-section,.week-timeline,.week-timeline-events{grid-column:1}.focus-grid{grid-template-columns:1fr}.reflection-prompts{grid-template-columns:1fr}.projects-grid{grid-template-columns:1fr 1fr}}@media (max-width:600px){.stat-cards{flex-direction:column}.stat-card{max-width:none}.week-info{flex-direction:column;align-items:flex-start;gap:.5rem}.projects-grid{grid-template-columns:1fr}}.focus-slot{transition:background-color .2s ease-out,border-color .2s ease-out}.focus-slot.filled{animation:focusSlotFill .3s ease-out}@keyframes focusSlotFill{0%{transform:scale(.95);opacity:.7}100%{transform:scale(1);opacity:1}}.focus-slot:focus,.focus-slot:focus-within{outline:2px solid var(--accent-blue);outline-offset:2px}.focus-slot[tabindex]:focus{outline:2px solid var(--accent-blue);outline-offset:2px}.focus-section .btn{transition:transform .15s ease-out,opacity .15s ease-out}.focus-section .btn:active{transform:scale(.97)}@media print{.card-badge,.focus-section .btn,.focus-slot .btn,.header,.review-actions-grid,.sidebar,.tab-badge,.tab-nav,.tab-status-dot{display:none!important}.main-content,.weekly-review-content{margin:0;padding:0;width:100%;max-width:100%}.event-item,.focus-slot,.project-health,.reflection-prompt,.review-card,.weekly-review-content,body{background:#fff!important;color:#000!important;-webkit-print-color-adjust:exact;print-color-adjust:exact}.review-card{border:1px solid #ccc!important;box-shadow:none!important;page-break-inside:avoid;margin-bottom:1rem}.focus-slot{border:1px solid #999!important}.focus-slot.primary{border:2px solid #f7d154!important;background:#fffbea!important}.review-grid{display:block!important}.review-card{display:inline-block;vertical-align:top;width:48%;margin-right:2%}.focus-section.full-width,.reflection-section,.week-timeline,.week-timeline-events{width:100%!important;display:block!important}.weekly-review-header{border-bottom:2px solid #333;padding-bottom:1rem;margin-bottom:1.5rem}.week-dates{font-size:1.5rem;font-weight:700}.day-dot{-webkit-print-color-adjust:exact;print-color-adjust:exact}.day-dot.completed{background:#5cb85c!important}.day-dot.event{background:#9b59b6!important}.day-dot.overdue{background:#d9534f!important}.project-health{border-left:4px solid #337ab7!important}.project-health.warning{border-left-color:#f7d154!important}.project-health.danger{border-left-color:#d9534f!important}.focus-grid{display:flex!important;gap:1rem}.focus-slot{flex:1}.reflection-prompts{display:flex!important;gap:1rem}.reflection-prompt{flex:1}.prompt-input{border:1px solid #ccc!important;min-height:80px}.focus-section{page-break-before:auto}.reflection-section{page-break-before:always}}.monthly-review-nav,.weekly-review-nav{display:flex;align-items:center;gap:.5rem}.weekly-review-nav .week-dates{font-family:var(--font-heading);font-size:1.25rem;font-weight:700;color:var(--text-primary);margin-left:.5rem}.monthly-review-month-display{font-family:var(--font-heading);font-size:1.25rem;font-weight:700;color:var(--text-primary);margin-left:.5rem}.monthly-review-content{max-width:900px;margin:0 auto;padding:1rem}.month-heatmap{margin-bottom:1.5rem;border:var(--border-width-sm) solid var(--border-color);border-radius:var(--radius-md);padding:1rem;background:var(--bg-secondary)}.month-heatmap-header{display:grid;grid-template-columns:repeat(7,1fr);text-align:center;margin-bottom:.5rem}.month-heatmap-day-header{font-family:var(--font-heading);font-size:.75rem;font-weight:600;color:var(--text-secondary);text-transform:uppercase}.month-heatmap-grid{display:grid;grid-template-columns:repeat(7,1fr);gap:3px}.month-heatmap-cell{aspect-ratio:1;display:flex;flex-direction:column;align-items:center;justify-content:center;border-radius:var(--radius-xs);cursor:pointer;transition:transform .1s ease;border:var(--border-width-sm) solid transparent;position:relative;min-height:40px}.month-heatmap-cell:not(.empty):hover{transform:scale(1.1);border-color:var(--border-color);z-index:1}.month-heatmap-cell.empty{cursor:default;background:0 0}.month-heatmap-cell.intensity-0{background:var(--bg-primary)}.month-heatmap-cell.intensity-1{background:color-mix(in srgb,var(--accent-green) 20%,var(--bg-primary))}.month-heatmap-cell.intensity-2{background:color-mix(in srgb,var(--accent-green) 40%,var(--bg-primary))}.month-heatmap-cell.intensity-3{background:color-mix(in srgb,var(--accent-green) 60%,var(--bg-primary))}.month-heatmap-cell.vacation{background:var(--bg-tertiary);opacity:.6}.month-heatmap-cell.today{border-color:var(--accent-primary);border-width:2px}.month-heatmap-cell.past.intensity-0{background:var(--bg-tertiary)}.month-heatmap-day-number{font-family:var(--font-heading);font-size:.8rem;font-weight:600;color:var(--text-primary)}.month-heatmap-dots{display:flex;gap:2px;margin-top:2px}.month-dot{font-size:.6rem;font-weight:700;border-radius:var(--radius-xs);padding:0 3px;line-height:1.3}.month-dot.completed{color:var(--accent-green)}.month-dot.event{color:var(--accent-purple)}.monthly-review-cards{display:grid;grid-template-columns:1fr 1fr;gap:1rem}.review-card.month-stats-card{grid-column:span 1}.review-card.month-goals-card{margin-bottom:1rem}.review-card.month-patterns-card,.review-card.month-pulse-card{grid-column:span 1}.review-card.month-reflection-card{grid-column:1/-1}.review-card-title{font-family:var(--font-heading);font-size:1rem;font-weight:700;margin-bottom:.75rem;color:var(--text-primary)}.month-stats-grid{display:grid;grid-template-columns:1fr 1fr;gap:.5rem}.month-stat-item{display:flex;flex-direction:column;align-items:center;padding:.5rem;border-radius:var(--radius-xs);background:var(--bg-primary);border:var(--border-width-sm) solid var(--border-color)}.month-stat-value{font-family:var(--font-heading);font-size:1.5rem;font-weight:700;color:var(--text-primary)}.month-stat-label{font-size:.75rem;color:var(--text-secondary);text-transform:uppercase;font-weight:600}.month-stats-highlights{display:flex;gap:1rem;margin-top:.5rem;justify-content:center}.stat-highlight{font-size:.8rem;color:var(--text-secondary)}.month-pulse-list{display:flex;flex-direction:column;gap:.5rem}.month-pulse-item{display:flex;align-items:center;gap:.5rem;padding:.5rem;border-radius:var(--radius-xs);background:var(--bg-primary);border:var(--border-width-sm) solid var(--border-color)}.pulse-name{font-weight:600;flex:1;font-size:.875rem}.pulse-stats{font-size:.75rem;color:var(--text-secondary)}.pulse-arrow{font-size:1rem;font-weight:700}.month-pulse-item.positive .pulse-arrow{color:var(--accent-green)}.month-pulse-item.negative .pulse-arrow{color:var(--accent-red)}.month-pulse-item.neutral .pulse-arrow{color:var(--text-secondary)}.month-goal-body{display:flex;align-items:center;gap:.5rem;flex:1}.month-goal-item.done{opacity:.7}.month-goal-item.done .month-goal-text{text-decoration:line-through}.month-goal-item.abandoned{opacity:.5}.month-goal-item.abandoned .month-goal-text{text-decoration:line-through}.month-goal-status-btn{background:0 0;border:none;cursor:pointer;font-size:1rem;padding:0;color:var(--text-secondary);width:24px;text-align:center}.month-goal-item.done .month-goal-status-btn{color:var(--accent-green)}.month-goal-item.abandoned .month-goal-status-btn{color:var(--accent-red)}.month-goal-text{flex:1;font-size:.875rem}.month-goal-delete-btn{background:0 0;border:none;cursor:pointer;color:var(--text-tertiary);padding:0 4px;font-size:.75rem;opacity:0;transition:opacity .15s}.month-goal-item:hover .month-goal-delete-btn{opacity:1}.month-goal-placeholder{color:var(--text-tertiary);font-size:.875rem}.month-reflection-fields{display:flex;flex-direction:column;gap:.5rem}.month-reflection-label{font-size:.875rem;font-weight:600;color:var(--text-secondary)}.month-reflection-textarea{width:100%;padding:.5rem;border:var(--border-width-sm) solid var(--border-color);border-radius:var(--radius-xs);background:var(--bg-primary);color:var(--text-primary);font-family:var(--font-body);font-size:.875rem;resize:vertical}.month-reflection-textarea:focus{outline:2px solid var(--accent-primary);outline-offset:-1px}.month-patterns-list{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:.5rem}.month-pattern-item{font-size:.875rem;color:var(--text-secondary);padding:.5rem;background:var(--bg-primary);border-radius:var(--radius-xs);border:var(--border-width-sm) solid var(--border-color)}@media (max-width:640px){.monthly-review-cards{grid-template-columns:1fr}.review-card.month-goals-card,.review-card.month-patterns-card,.review-card.month-pulse-card,.review-card.month-stats-card{grid-column:span 1}.month-heatmap-cell{min-height:32px}.month-heatmap-day-number{font-size:.7rem}.month-heatmap-dots{display:none}}.import-wizard{display:flex;flex-direction:column;gap:1.5rem}.import-step{padding:1rem;background:var(--bg-secondary);border:var(--border-width) solid var(--border-color);border-radius:var(--radius-md)}.import-step h3{margin:0 0 1rem 0;font-size:var(--font-size-md);font-weight:600}.plugin-selector{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:.75rem}.plugin-option{display:flex;flex-direction:column;align-items:flex-start;padding:.75rem 1rem;background:var(--bg-card);border:var(--border-width) solid var(--border-color);border-radius:var(--radius-sm);cursor:pointer;text-align:left;transition:border-color var(--transition-fast),background var(--transition-fast)}.plugin-option:hover{border-color:var(--accent-primary);background:var(--bg-hover)}.plugin-option.selected{border-color:var(--accent-primary);background:color-mix(in srgb,var(--accent-primary) 10%,var(--bg-card));box-shadow:0 0 0 2px color-mix(in srgb,var(--accent-primary) 30%,transparent)}.plugin-option .plugin-name{font-weight:600;margin-bottom:.25rem}.plugin-option .plugin-meta{display:flex;gap:.5rem;font-size:var(--font-size-sm);color:var(--text-muted);margin-bottom:.25rem}.plugin-option .plugin-extensions{color:var(--accent-cyan)}.plugin-option .plugin-types{color:var(--text-secondary)}.plugin-option .plugin-description{font-size:var(--font-size-sm);color:var(--text-secondary);line-height:1.4}.file-selector{display:flex;align-items:center;gap:1rem}.selected-file-name{color:var(--text-secondary);font-family:monospace;font-size:var(--font-size-sm)}.import-preview-container{min-height:100px}.import-preview-table-wrapper{max-height:300px;overflow:auto;border:1px solid var(--border-color);border-radius:var(--radius-sm)}.import-preview-table{font-size:var(--font-size-sm);margin:0}.import-preview-table th{position:sticky;top:0;background:var(--bg-secondary);z-index:1}.import-preview-table td{max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.import-summary{margin:0 0 .75rem 0;color:var(--text-primary)}.import-more{margin:.5rem 0 0 0;color:var(--text-muted);font-style:italic;font-size:var(--font-size-sm)}.import-empty,.import-error{padding:2rem;text-align:center;color:var(--text-muted)}.import-error{color:var(--accent-red)}.import-warnings{margin-top:1rem;padding:.75rem;background:color-mix(in srgb,var(--accent-yellow) 10%,var(--bg-card));border:1px solid var(--accent-yellow);border-radius:var(--radius-sm);font-size:var(--font-size-sm)}.import-warnings ul{margin:.5rem 0 0 1.25rem;padding:0}.import-warnings li{margin-bottom:.25rem}.import-external-types{display:flex;gap:1rem;margin-bottom:1.5rem}.import-type-card{flex:1;display:flex;flex-direction:column;align-items:center;gap:.5rem;padding:1.5rem 1rem;background:var(--bg-card);border:2px solid var(--border-color);border-radius:var(--radius-md);cursor:pointer;transition:border-color .15s,background .15s}.import-type-card:hover{border-color:var(--accent-primary);background:var(--bg-secondary)}.import-type-icon{font-size:2rem}.import-type-label{font-weight:600;color:var(--text-primary)}.import-type-desc{font-size:var(--font-size-sm);color:var(--text-muted)}.plugin-list{display:flex;flex-direction:column;gap:.75rem}.plugin-item{display:flex;justify-content:space-between;align-items:center;padding:1rem;background:var(--bg-card);border:var(--border-width) solid var(--border-color);border-radius:var(--radius-md)}.plugin-item .plugin-info{flex:1}.plugin-item .plugin-name{font-weight:600}.plugin-item .plugin-version{color:var(--text-muted);font-size:var(--font-size-sm);margin-left:.5rem}.plugin-item .plugin-description{margin:.25rem 0;color:var(--text-secondary);font-size:var(--font-size-sm)}.plugin-item .plugin-extensions{font-size:var(--font-size-xs);color:var(--text-muted)}.plugin-item .plugin-actions{margin-left:1rem}.toggle-switch{position:relative;display:inline-block;width:44px;height:24px}.toggle-switch input{opacity:0;width:0;height:0}.toggle-slider{position:absolute;cursor:pointer;top:0;left:0;right:0;bottom:0;background-color:var(--bg-tertiary);border:2px solid var(--border-color);border-radius:var(--radius-xl);transition:background-color var(--transition-fast),border-color var(--transition-fast)}.toggle-slider:before{position:absolute;content:"";height:16px;width:16px;left:2px;bottom:2px;background-color:var(--text-muted);border-radius:var(--radius-full);transition:transform var(--transition-fast),background-color var(--transition-fast)}.toggle-switch input:checked+.toggle-slider{background-color:var(--accent-primary);border-color:var(--accent-primary)}.toggle-switch input:checked+.toggle-slider:before{transform:translateX(20px);background-color:var(--bg-card)}.toggle-switch input:focus+.toggle-slider{box-shadow:0 0 0 2px color-mix(in srgb,var(--accent-primary) 30%,transparent)}.milestones-section{margin-bottom:1.5rem}.milestones-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;padding-bottom:.5rem;border-bottom:2px solid var(--border-color)}.milestones-header h3{margin:0;font-size:1rem;font-family:var(--font-heading);font-weight:700}.milestone-card{background:var(--bg-card);border:var(--border-width) solid var(--border-color);border-radius:var(--radius-md);padding:1rem;margin-bottom:.75rem;transition:background-color .1s;box-shadow:var(--shadow-offset) var(--shadow-offset) 0 var(--border-color)}.milestone-card:hover{background:var(--bg-secondary)}.milestone-card-header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:.5rem}.milestone-card-header h4{margin:0;font-size:.95rem;font-family:var(--font-heading);font-weight:700}.milestone-card-header .milestone-status{font-size:.7rem;font-weight:700;text-transform:uppercase;padding:.15rem .4rem;border-radius:var(--radius-sm);background:var(--bg-secondary);color:var(--text-muted)}.milestone-card-header .milestone-status.completed{background:color-mix(in srgb,var(--accent-green) 15%,var(--bg-secondary));color:var(--accent-green)}.milestone-meta{display:flex;gap:1rem;font-size:.8rem;color:var(--text-muted);margin-bottom:.5rem}.milestone-progress{height:6px;background:var(--bg-secondary);border-radius:var(--radius-full);overflow:hidden;border:var(--border-width-sm) solid var(--border-color)}.milestone-progress-fill{height:100%;background:var(--accent-green);border-radius:var(--radius-full);transition:width var(--transition-fast)}.milestone-actions{display:flex;gap:.5rem;margin-top:.5rem}.milestone-actions button{font-size:.75rem;padding:.2rem .5rem;background:var(--bg-secondary);border:var(--border-width-sm) solid var(--border-color);border-radius:var(--radius-sm);cursor:pointer;color:var(--text-secondary);transition:background var(--transition-fast)}.milestone-actions button:hover{background:var(--bg-hover)}.milestone-actions button.danger:hover{background:color-mix(in srgb,var(--accent-red) 15%,var(--bg-secondary));color:var(--accent-red)}button.milestone-reorder-btn.btn{font-size:.65rem;padding:.15rem .35rem;line-height:1;min-width:1.5rem;text-align:center}.milestones-completed-section{margin-top:.75rem}.milestones-completed-toggle{font-size:.8rem;color:var(--text-secondary);padding:.25rem 0}.milestone-card-summary{padding:.5rem .75rem;opacity:.7}.milestone-card-summary .milestone-info{display:flex;align-items:center;gap:.5rem}.milestone-complete-badge{font-size:.7rem;font-weight:700;padding:.1rem .4rem;border-radius:var(--radius-sm);background:color-mix(in srgb,var(--accent-green) 15%,var(--bg-secondary));color:var(--accent-green)}.mobile-tab-bar{display:none;position:fixed;bottom:0;left:0;right:0;z-index:1100;background:var(--bg-card);border-top:var(--border-width) solid var(--border-color);padding-bottom:env(safe-area-inset-bottom,0);height:calc(52px + env(safe-area-inset-bottom,0px))}.mobile-tab{flex:1;display:flex;align-items:center;justify-content:center;height:52px;background:0 0;border:none;color:var(--text-muted);font-size:.7rem;font-weight:700;font-family:var(--font-sans);text-transform:uppercase;letter-spacing:.05em;cursor:pointer;-webkit-tap-highlight-color:transparent;transition:color .15s ease}.mobile-tab.active{color:var(--accent-blue)}.mobile-tab:active{background:var(--bg-secondary)}.mobile-tab-create{font-size:1.4rem;font-weight:400;color:var(--accent-green);letter-spacing:0;text-transform:none}.mobile-more-popover{display:none;position:fixed;bottom:calc(52px + env(safe-area-inset-bottom,0px));right:0;background:var(--bg-card);border:var(--border-width) solid var(--border-color);border-radius:var(--radius-md);padding:.25rem 0;z-index:1101;min-width:160px;box-shadow:0 -2px 8px rgba(0,0,0,.1)}.mobile-more-popover.visible{display:block}.mobile-more-popover button{display:block;width:100%;padding:.75rem 1rem;background:0 0;border:none;text-align:left;font-size:var(--font-size-sm);font-weight:600;color:var(--text-primary);cursor:pointer}.mobile-more-popover button:active{background:var(--bg-secondary)}.action-sheet{position:fixed;inset:0;z-index:10001;display:flex;flex-direction:column;justify-content:flex-end}.action-sheet.hidden{display:none}.action-sheet-backdrop{position:absolute;inset:0;background:rgba(0,0,0,.4)}.action-sheet-container{position:relative;background:var(--bg-card);border-top:var(--border-width) solid var(--border-color);border-radius:var(--radius-lg) var(--radius-lg) 0 0;padding:.5rem 1rem calc(.5rem + env(safe-area-inset-bottom,0px));max-height:60vh;overflow-y:auto;animation:sheetSlideUp .25s ease-out}.action-sheet-handle{width:36px;height:4px;border-radius:2px;background:var(--text-muted);margin:0 auto .75rem;opacity:.4}.action-sheet-content button{display:flex;align-items:center;gap:.75rem;width:100%;padding:.875rem .5rem;background:0 0;border:none;border-bottom:1px solid var(--bg-secondary);font-size:var(--font-size-base);font-weight:600;color:var(--text-primary);text-align:left;cursor:pointer}.action-sheet-content button:last-child{border-bottom:none}.action-sheet-content button:active{background:var(--bg-secondary)}.action-sheet-content button.danger{color:var(--accent-red)}.action-sheet-cancel{display:block;width:100%;padding:.875rem;margin-top:.5rem;background:var(--bg-secondary);border:var(--border-width-sm) solid var(--border-color);border-radius:var(--radius-md);font-size:var(--font-size-base);font-weight:700;color:var(--text-primary);text-align:center;cursor:pointer}.action-sheet-cancel:active{background:var(--bg-tertiary)}.modal-drag-handle{display:none;width:36px;height:4px;border-radius:2px;background:var(--text-muted);margin:.5rem auto 0;opacity:.4}.mobile-sort-bar{display:none;gap:.5rem;padding:.5rem 0;align-items:center}.mobile-sort-bar select{flex:1;font-size:var(--font-size-sm)}.mobile-filter-toggle{display:none}.swipe-actions-container{position:relative;overflow:hidden}.swipe-actions-bg{position:absolute;top:0;bottom:0;display:flex;align-items:center;padding:0 1rem;font-weight:700;font-size:var(--font-size-sm);color:var(--text-on-accent)}.swipe-actions-bg.swipe-left{right:0;background:var(--accent-green)}.swipe-actions-bg.swipe-right{left:0;background:var(--accent-red)}.swipe-content{position:relative;background:var(--bg-card);transition:transform .15s ease}.pull-to-refresh-indicator{display:none;text-align:center;padding:.75rem;font-size:var(--font-size-sm);color:var(--text-muted);font-weight:600}.pull-to-refresh-indicator.visible{display:block}.event-date-group-header{display:none}.day-plan-sidebar-toggle{display:none}@keyframes sheetSlideUp{from{transform:translateY(100%)}to{transform:translateY(0)}}@keyframes sheetSlideDown{from{transform:translateY(0)}to{transform:translateY(100%)}}@keyframes dialFadeIn{from{opacity:0}to{opacity:1}}@media (max-width:768px){body,html{overflow-x:hidden;max-width:100vw;touch-action:pan-y;overscroll-behavior-x:none}body{padding-top:env(safe-area-inset-top,0);padding-bottom:calc(52px + env(safe-area-inset-bottom,0px));padding-left:env(safe-area-inset-left,0);padding-right:env(safe-area-inset-right,0)}.pill-nav{touch-action:pan-x}.mobile-more-popover,.mobile-tab-bar,.timer-widget{padding-left:env(safe-area-inset-left,0);padding-right:env(safe-area-inset-right,0)}.mobile-tab-bar{display:flex}.tab-navigation{display:none!important}.app-header{display:none}.pill-nav{overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none;padding:var(--space-1) var(--space-3)}.tab-group>.subview>.page-header{position:static}.pill-nav::-webkit-scrollbar{display:none}.main-content{padding:.75rem}.page-header{flex-wrap:wrap;gap:.5rem}.page-header .btn-primary{display:none}.page-title{display:none}.modal-overlay{align-items:flex-end;bottom:calc(52px + env(safe-area-inset-bottom,0px))}.modal-container{width:100%!important;max-width:100%!important;max-height:calc(100vh - 52px - env(safe-area-inset-bottom,0px) - env(safe-area-inset-top,0px));border-radius:var(--radius-lg) var(--radius-lg) 0 0;margin:0;border-bottom:none;padding-bottom:0}.modal-container.modal-large{max-width:100%!important;width:100%!important;max-height:calc(100vh - 52px - env(safe-area-inset-bottom,0px) - env(safe-area-inset-top,0px));border-radius:var(--radius-lg) var(--radius-lg) 0 0}.modal-drag-handle{display:block}.modal-header{padding:.75rem 1rem}.modal-content{padding:1rem}@keyframes modalSlideIn{from{transform:translateY(100%)}to{transform:translateY(0)}}@keyframes modalSlideOut{from{transform:translateY(0)}to{transform:translateY(100%)}}.toast,.toast-undo{bottom:calc(env(safe-area-inset-bottom,0px) + 4.5rem)!important;left:1rem!important;right:1rem!important;max-width:none!important}.task-table{border:none;box-shadow:none;background:0 0}.task-header-row{display:none!important}.task-row{display:flex!important;flex-direction:column;gap:.25rem;padding:.75rem 1rem;margin-bottom:.5rem;background:var(--bg-card);border:var(--border-width-sm) solid var(--border-color);border-radius:var(--radius-md);border-left:4px solid var(--text-muted)}.task-row.task-pending{border-left-color:var(--text-muted)}.task-row .task-cell.priority-h~.task-cell:first-child,.task-row:has(.priority-h){border-left-color:var(--accent-red)}.task-row:has(.priority-m){border-left-color:var(--accent-yellow)}.task-row:has(.priority-l){border-left-color:var(--text-muted)}.task-row .task-cell{display:flex!important;overflow:visible;padding:0}.task-cell.task-description{font-weight:600;font-size:var(--font-size-base)}.task-cell.task-due,.task-cell.task-project{font-size:var(--font-size-sm);color:var(--text-secondary)}.task-row .task-cell.task-project::before{content:none}.task-cell.task-progress,.task-cell.task-recurrence,.task-row .task-cell:nth-child(3){display:none!important}.task-cell.task-project{order:2}.task-cell.task-due{order:3}.task-cell.task-description{order:1}.task-cell.task-actions-cell{order:4;justify-content:flex-end}.task-cell.task-progress:has(.progress-bar-container){display:flex!important;order:5}.task-actions-cell .bulk-checkbox{display:none}.kebab-btn{opacity:1}.mobile-sort-bar{display:flex}.mobile-filter-toggle{display:inline-flex;align-items:center;gap:.25rem;padding:.5rem .75rem;background:var(--bg-card);border:var(--border-width-sm) solid var(--border-color);border-radius:var(--radius-sm);font-size:var(--font-size-sm);font-weight:600;cursor:pointer}.filter-bar{display:none!important}.filter-bar.mobile-visible{display:flex!important;flex-direction:column;position:fixed;bottom:calc(52px + env(safe-area-inset-bottom,0px));left:0;right:0;background:var(--bg-card);border-top:var(--border-width) solid var(--border-color);border-radius:var(--radius-lg) var(--radius-lg) 0 0;padding:1rem;z-index:1050;box-shadow:0 -4px 12px rgba(0,0,0,.1)}.event-header-row{display:none!important}.event-row-virtual{display:flex!important;flex-direction:column;gap:.125rem;padding:.75rem 1rem;border-bottom:1px solid var(--bg-secondary)}.event-cell-date{font-weight:700;font-size:var(--font-size-sm);color:var(--text-secondary)}.event-cell-time{font-size:var(--font-size-sm);color:var(--text-muted)}.event-cell-title{font-weight:600;font-size:var(--font-size-base)}.event-cell-location{font-size:var(--font-size-sm);color:var(--text-secondary)}.event-date-group-header{display:flex;position:sticky;top:0;z-index:5;padding:.5rem 1rem;background:var(--bg-secondary);font-weight:700;font-size:var(--font-size-sm);text-transform:uppercase;letter-spacing:.05em;color:var(--text-primary);border-bottom:var(--border-width-sm) solid var(--border-color)}.email-item{padding:.625rem .75rem}.email-from{font-size:var(--font-size-sm)}.email-subject{font-size:var(--font-size-base)}.email-preview{display:none}.email-date{font-size:var(--font-size-xs)}.email-item .bulk-checkbox{display:none}.day-plan-content{flex-direction:column}.day-plan-sidebar{width:100%;max-height:none;border-top:var(--border-width-sm) solid var(--border-color);order:2}.day-plan-sidebar.collapsed .sidebar-task-list{display:none}.day-plan-sidebar-toggle{display:flex;align-items:center;justify-content:space-between;width:100%;padding:.625rem .75rem;background:var(--bg-secondary);border:none;border-bottom:1px solid var(--border-color);font-size:var(--font-size-sm);font-weight:700;cursor:pointer;color:var(--text-primary)}.day-plan-main{order:1}.day-plan-nav{flex-wrap:wrap;gap:.25rem}.weekly-review-content{padding:0}.monthly-review-content{padding:0}.month-reflection-textarea,.prompt-input{resize:none;overflow:hidden}.monthly-review-nav{flex-wrap:wrap;gap:.25rem}.monthly-review-month-display{font-size:1rem}.day-summary-sheet{padding:.5rem 0}.day-summary-date{font-size:1rem;font-weight:700;margin-bottom:.75rem;color:var(--text-primary)}.day-summary-stats{display:flex;gap:.5rem;margin-bottom:1rem}.day-summary-chip{padding:.25rem .75rem;background:var(--bg-secondary);border-radius:var(--radius-sm);font-size:var(--font-size-sm);font-weight:600;color:var(--text-secondary)}.day-summary-list{list-style:none;padding:0;margin:0 0 1rem 0}.day-summary-item{padding:.5rem 0;border-bottom:1px solid var(--bg-secondary);font-size:var(--font-size-sm);color:var(--text-primary)}.day-summary-time{font-weight:600;color:var(--text-secondary);margin-right:.5rem}.day-summary-more{color:var(--text-muted);font-style:italic}.day-summary-empty{color:var(--text-muted);font-size:var(--font-size-sm);margin:.5rem 0 1rem}.day-summary-go-btn{width:100%;margin-top:.5rem}.bulk-actions-bar{position:fixed;bottom:calc(52px + env(safe-area-inset-bottom,0px));left:0;right:0;z-index:1050;border-radius:var(--radius-lg) var(--radius-lg) 0 0;box-shadow:0 -4px 12px rgba(0,0,0,.15)}.pagination-controls{padding:.5rem}.pagination-controls .btn{padding:.5rem .75rem;font-size:var(--font-size-sm)}}@media (hover:none){.task-row:hover{background-color:transparent}.task-row-clickable:hover{background:0 0}.event-row-virtual:hover{background-color:transparent}.email-item:hover{background-color:transparent}.card:hover{background-color:var(--bg-card);transform:none;box-shadow:var(--shadow-offset) var(--shadow-offset) 0 var(--border-color)}.btn:hover{background:var(--bg-card)}.btn-primary:hover{background-color:var(--accent-blue)}.btn-danger:hover{background-color:var(--accent-red)}.dashboard-item:hover{background:var(--bg-card);transform:none;box-shadow:var(--shadow-offset) var(--shadow-offset) 0 var(--border-color)}.kanban-card:hover{background:var(--bg-card);transform:none;box-shadow:var(--shadow-offset) var(--shadow-offset) 0 var(--border-color)}.saved-view-item:hover{background:var(--bg-card);color:var(--text-primary);transform:none;box-shadow:var(--shadow-offset) var(--shadow-offset) 0 var(--border-color)}.context-menu-item:hover{background:0 0;color:var(--text-primary)}.modal-close:hover{background:var(--bg-card);color:var(--text-primary)}.month-heatmap-cell:hover{background:0 0;transform:none}.email-item .kebab-btn,.event-row-virtual .kebab-btn,.task-row .kebab-btn{opacity:1}.shortcut-hint-btn{display:none}.time-block-quick-options{grid-template-columns:1fr}.duration-preset,.time-block-quick-btn{min-height:44px;padding:.75rem;font-size:1rem}}body.is-touch .email-item .kebab-btn,body.is-touch .event-row-virtual .kebab-btn,body.is-touch .task-row .kebab-btn{opacity:1}body.is-touch .shortcut-hint-btn{display:none}.view-toggle{display:flex;gap:0;margin-left:auto}.view-toggle-btn{padding:.35rem .75rem;border:var(--border-width-sm) solid var(--border-color);background:var(--bg-secondary);font-family:var(--font-body);font-size:var(--font-size-md);cursor:pointer;transition:background var(--transition-fast),box-shadow var(--transition-fast)}.view-toggle-btn.active{background:var(--bg-card);font-weight:600}.view-toggle-btn:first-child{border-radius:var(--radius-xs) 0 0 var(--radius-xs)}.view-toggle-btn:last-child{border-radius:0 var(--radius-xs) var(--radius-xs) 0;border-left:none}.kanban-board{display:grid;grid-template-columns:repeat(3,1fr);gap:1rem;padding:.5rem 0;min-height:400px}.kanban-column{background:var(--bg-card);border:var(--border-width) solid var(--border-color);display:flex;flex-direction:column;min-height:300px;max-height:calc(100vh - 200px)}.kanban-column-header{padding:.75rem 1rem;border-bottom:2px solid var(--border-color);font-family:var(--font-heading);font-weight:700;display:flex;justify-content:space-between;align-items:center}.kanban-column-count{font-family:var(--font-body);font-size:var(--font-size-sm);color:var(--text-secondary)}.kanban-column-body{flex:1;overflow-y:auto;padding:.5rem;display:flex;flex-direction:column;gap:.5rem}.kanban-column.drag-over{background-color:var(--bg-tertiary)}.kanban-card{padding:.75rem;background:var(--bg-card);border:var(--border-width-sm) solid var(--border-color);cursor:grab;box-shadow:var(--shadow-offset) var(--shadow-offset) 0 var(--border-color);transition:transform .15s ease,box-shadow .15s ease,background-color .15s ease;border-left:4px solid transparent}.kanban-card:hover{background:var(--bg-secondary);transform:translate(-2px,-2px);box-shadow:calc(var(--shadow-offset) + 2px) calc(var(--shadow-offset) + 2px) 0 var(--border-color)}.kanban-card.dragging{opacity:.5;cursor:grabbing}.kanban-card.priority-high{border-left-color:var(--accent-red)}.kanban-card.priority-medium{border-left-color:var(--accent-yellow)}.kanban-card.priority-low{border-left-color:var(--accent-green)}.kanban-card-title{font-weight:600;margin-bottom:.25rem}.kanban-card-meta{font-size:var(--font-size-sm);color:var(--text-secondary);display:flex;gap:.5rem;flex-wrap:wrap}.kanban-card-due.overdue{color:var(--accent-red);font-weight:600}.progress-bar-mini{height:3px;background:var(--bg-tertiary);border-radius:2px;margin-top:.5rem}.progress-bar-mini .progress-fill{height:100%;background:var(--accent-green);border-radius:2px}@media (max-width:768px){.kanban-board{grid-template-columns:1fr}.kanban-column{max-height:none}}.timer-widget{position:fixed;bottom:0;left:0;right:0;z-index:900;background:var(--bg-primary);border-top:var(--border-width) solid var(--border-color);box-shadow:0 -2px 8px rgba(0,0,0,.1);padding:.5rem 1rem;transition:transform .2s ease}.timer-widget.hidden{transform:translateY(100%);pointer-events:none}.timer-widget-inner{display:flex;align-items:center;gap:1rem;max-width:800px;margin:0 auto}.timer-task-name{flex:1;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.timer-elapsed{font-family:var(--font-mono, monospace);font-size:1.125rem;font-weight:700;color:var(--accent-color);min-width:5rem;text-align:center}.timer-actions{display:flex;gap:.5rem}.focus-overlay{position:fixed;inset:0;z-index:1000;background:var(--bg-primary);display:flex;align-items:center;justify-content:center;transition:opacity .3s ease}.focus-overlay.hidden{opacity:0;pointer-events:none}.focus-overlay-content{text-align:center;max-width:400px;width:100%;padding:2rem}.focus-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:2rem}.focus-label{font-family:var(--font-heading);font-size:1.25rem;font-weight:700}.focus-presets{display:flex;gap:.5rem}.focus-preset-btn.active{background:var(--accent-color);color:var(--bg-primary);border-color:var(--accent-color)}.focus-countdown{font-family:var(--font-mono, monospace);font-size:4rem;font-weight:700;line-height:1;margin-bottom:1.5rem;color:var(--text-primary)}.focus-progress-bar{height:6px;background:var(--bg-tertiary);border-radius:3px;margin-bottom:1.5rem;overflow:hidden}.focus-progress-fill{height:100%;background:var(--accent-color);border-radius:3px;transition:width 1s linear}.focus-task-name{color:var(--text-secondary);margin-bottom:2rem;font-size:.9rem}.focus-actions{display:flex;gap:1rem;justify-content:center}.time-summary-section{margin-bottom:1rem}.time-summary-toggle{display:flex;align-items:center;gap:.5rem;width:100%;padding:.5rem;background:0 0;border:none;font-family:var(--font-heading);font-size:.875rem;font-weight:700;color:var(--text-primary);cursor:pointer;text-align:left}.time-summary-toggle:hover{color:var(--accent-color)}.time-summary-toggle-icon{font-size:.625rem;transition:transform .15s ease}.time-summary-body{padding:.5rem;overflow:hidden;transition:max-height .2s ease;max-height:500px}.time-summary-body.collapsed{max-height:0;padding:0 .5rem}.time-summary-today{display:flex;justify-content:space-between;align-items:center;padding:.5rem 0;border-bottom:1px solid var(--border-color);margin-bottom:.5rem}.time-summary-today-label{font-weight:600;font-size:.875rem}.time-summary-today-value{font-family:var(--font-mono, monospace);font-weight:700;font-size:1rem;color:var(--accent-color)}.time-summary-week-header{font-size:.75rem;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--text-secondary);margin-bottom:.5rem}.time-summary-project{margin-bottom:.5rem}.time-summary-project-info{display:flex;justify-content:space-between;align-items:center;font-size:.8125rem;margin-bottom:.25rem}.time-summary-project-name{color:var(--text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}.time-summary-project-time{font-family:var(--font-mono, monospace);font-weight:600;font-size:.75rem;color:var(--text-secondary);margin-left:.5rem;flex-shrink:0}.time-summary-bar{height:4px;background:var(--bg-tertiary);border-radius:2px;overflow:hidden}.time-summary-bar-fill{height:100%;background:var(--accent-color);border-radius:2px}.unscheduled-task-actions{display:flex;gap:.25rem;margin-top:.375rem}.unscheduled-task-actions .btn{font-size:.7rem;padding:.125rem .375rem;min-height:auto;line-height:1.4}.task-time-badge{display:inline-block;font-family:var(--font-mono, monospace);font-size:.7rem;font-weight:600;color:var(--text-secondary);background:var(--bg-secondary);border:var(--border-width-sm) solid var(--border-color);border-radius:var(--radius-sm);padding:.05rem .35rem;margin-left:.375rem;vertical-align:middle;white-space:nowrap}.task-time-badge.over-estimate{color:var(--accent-red);border-color:var(--accent-red)}.task-started-icon{display:inline-block;width:0;height:0;border-style:solid;border-width:5px 0 5px 8px;border-color:transparent transparent transparent var(--accent-green);margin-right:.375rem;vertical-align:middle;cursor:pointer;opacity:.8;flex-shrink:0}.task-started-icon:hover{opacity:1}.task-timer-active{display:inline-block;width:8px;height:8px;background:var(--accent-red);border-radius:var(--radius-full);margin-left:.375rem;vertical-align:middle;animation:timer-pulse 1.5s ease-in-out infinite}@keyframes timer-pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.4;transform:scale(.8)}}@media (max-width:768px){.timer-widget{bottom:calc(52px + env(safe-area-inset-bottom,0px))}.focus-countdown{font-size:3rem}}.timer-active-banner{display:flex;align-items:center;gap:1rem;padding:.875rem 1rem;background:var(--bg-secondary);border:var(--border-width) solid var(--accent-color);border-radius:var(--radius-md);margin-bottom:1.5rem}.timer-active-info{flex:1;min-width:0}.timer-active-label{display:block;font-size:.75rem;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--accent-color);margin-bottom:.125rem}.timer-active-task{display:block;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.timer-active-elapsed{font-family:var(--font-mono, monospace);font-size:1.25rem;font-weight:700;color:var(--accent-color);min-width:5rem;text-align:center}.timer-active-actions{display:flex;gap:.5rem}.timer-focus-split{display:flex;align-items:center;gap:.375rem;padding:.5rem 0;margin-bottom:.5rem}.timer-focus-split-label{font-size:.8125rem;color:var(--text-secondary);font-weight:600;margin-right:.25rem}.timer-split-input{width:3.5rem;padding:.25rem .375rem;font-size:.875rem;font-family:var(--font-mono, monospace);text-align:center;border:var(--border-width) solid var(--border-color);border-radius:var(--radius-sm);background:var(--bg-primary);color:var(--text-primary)}.timer-focus-split-sep{font-size:.8125rem;color:var(--text-secondary)}.timer-task-list{display:flex;flex-direction:column;gap:0}.timer-task-item{display:flex;align-items:center;gap:1rem;padding:.75rem .5rem;border-bottom:1px solid var(--border-color)}.timer-task-item:last-child{border-bottom:none}.timer-task-info{flex:1;min-width:0}.timer-task-desc{display:block;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.timer-task-meta{display:flex;gap:.5rem;flex-wrap:wrap;margin-top:.25rem;font-size:.8125rem;color:var(--text-secondary)}.timer-task-project{font-weight:600}.timer-task-priority{font-weight:600}.timer-task-priority.priority-h,.timer-task-priority.priority-high{color:var(--accent-red)}.timer-task-priority.priority-m,.timer-task-priority.priority-medium{color:var(--accent-yellow,var(--accent-color))}.timer-task-estimate,.timer-task-tracked{font-family:var(--font-mono, monospace);font-size:.75rem}.timer-task-actions{display:flex;gap:.375rem;flex-shrink:0}@media (max-width:768px){.timer-active-banner{flex-wrap:wrap}.timer-active-elapsed{font-size:1rem}.timer-task-item{flex-wrap:wrap}.timer-task-actions{width:100%;justify-content:flex-end}}.task-overview-section{margin-bottom:1.5rem;padding:1rem;background:var(--bg-card);border:var(--border-width) solid var(--border-color);box-shadow:var(--shadow-offset) var(--shadow-offset) 0 var(--border-color)}.task-overview-section-title{font-family:var(--font-heading);font-size:1rem;font-weight:700;margin-bottom:.75rem;display:flex;align-items:center;gap:.5rem}.task-overview-count{font-weight:400;font-size:.85rem;color:var(--text-secondary)}.task-overview-stats{display:grid;grid-template-columns:repeat(4,1fr);gap:.75rem;margin-bottom:1rem}.task-overview-stat{text-align:center;padding:.5rem;background:var(--bg-secondary);border:var(--border-width-sm) solid var(--border-color)}.task-overview-stat-value{font-family:var(--font-heading);font-size:1.25rem;font-weight:700}.task-overview-stat-label{font-size:.75rem;color:var(--text-secondary);margin-top:.25rem}.task-overview-heatmap-nav{display:flex;align-items:center;justify-content:center;gap:1rem;margin-bottom:.75rem;font-family:var(--font-heading);font-weight:700}.task-overview-meta{display:flex;flex-direction:column;gap:.5rem}.task-overview-badges{display:flex;flex-wrap:wrap;gap:.5rem;margin-bottom:.25rem}.task-overview-details{display:flex;flex-direction:column;gap:.25rem;color:var(--text-secondary);font-size:.9rem}.task-overview-subtask-list{display:flex;flex-direction:column;gap:.25rem;margin-bottom:.75rem}.task-overview-subtask{display:flex;align-items:center;gap:.5rem;padding:.25rem 0}.completed-text{text-decoration:line-through;color:var(--text-secondary)}.task-overview-add-form{display:flex;gap:.5rem;margin-top:.5rem}.task-overview-add-form .form-input{flex:1}.task-overview-sessions{display:flex;flex-direction:column;gap:.25rem;font-size:.9rem;color:var(--text-secondary)}.task-overview-session{padding:.25rem 0;border-bottom:var(--border-width-sm) solid var(--border-color)}.task-overview-annotations{display:flex;flex-direction:column;gap:.5rem;margin-bottom:.75rem}.task-overview-annotation{padding:.5rem;background:var(--bg-secondary);border:var(--border-width-sm) solid var(--border-color)}.task-overview-annotation-date{font-size:.75rem;color:var(--text-secondary);margin-bottom:.25rem}.task-overview-completion-list{margin-top:.75rem;font-size:.9rem;color:var(--text-secondary)}.task-overview-completion-item{padding:.25rem 0;border-bottom:var(--border-width-sm) solid var(--border-color)}.progress-bar{height:6px;background:var(--bg-secondary);border:var(--border-width-sm) solid var(--border-color);margin-bottom:.75rem;overflow:hidden}.progress-fill{height:100%;background:var(--accent-green);transition:width .3s ease}.progress-bar.over-estimate .progress-fill{background:var(--accent-red)}.badge-completed{background:var(--accent-green);color:var(--text-on-accent);padding:.1rem .5rem;font-size:.75rem}.badge-started{background:var(--accent-blue);color:var(--text-on-accent);padding:.1rem .5rem;font-size:.75rem}.badge-pending{background:var(--bg-secondary);padding:.1rem .5rem;font-size:.75rem}.badge-priority-h{background:var(--accent-red);color:var(--text-on-accent);padding:.1rem .5rem;font-size:.75rem}.badge-priority-m{background:var(--accent-yellow);padding:.1rem .5rem;font-size:.75rem}.badge-priority-l{background:var(--bg-secondary);padding:.1rem .5rem;font-size:.75rem}.badge-focus{background:var(--accent-primary);color:var(--text-on-accent);padding:.1rem .5rem;font-size:.75rem}.badge-overdue{background:var(--accent-red);color:var(--text-on-accent);padding:.1rem .5rem;font-size:.75rem}.badge-snoozed{background:var(--accent-yellow);padding:.1rem .5rem;font-size:.75rem}@media (max-width:600px){.task-overview-stats{grid-template-columns:repeat(2,1fr)}}.toggle-nudge-dot{display:inline-block;width:7px;height:7px;background:var(--accent-red);border-radius:var(--radius-full);margin-left:.35rem;vertical-align:middle;animation:pulse-badge 2s infinite}.view-toggle-btn{position:relative}.finish-review-bar{display:flex;justify-content:flex-end;padding:1.5rem 0 1rem;margin-top:1.5rem;border-top:1px dashed var(--border-color)}.finish-review-btn{position:relative;font-size:var(--font-size-lg);padding:.65rem 1.5rem}.finish-review-modal-content{display:flex;flex-direction:column;gap:1.25rem}.past-review-banner{padding:.75rem 1rem;border:var(--border-width-sm) dashed var(--border-color);border-radius:var(--radius-md);background:var(--bg-secondary);color:var(--text-secondary);font-size:.9rem}.day-accomplished-inline{margin-top:1.5rem}.day-accomplished-inline:empty{display:none}.day-accomplished-stats{font-size:.8rem;color:var(--text-secondary);margin-bottom:.5rem}.events-calendar-container{padding:0}.calendar-nav{display:flex;align-items:center;gap:.5rem;margin-bottom:1rem}.calendar-nav-label{font-family:var(--font-heading);font-size:1.1rem;font-weight:700;margin-left:.5rem}.cal-month-grid{border:var(--border-width) solid var(--border-color);overflow:hidden;background:var(--bg-card);box-shadow:var(--shadow-offset) var(--shadow-offset) 0 var(--border-color)}.cal-month-cells,.cal-month-header{display:grid;grid-template-columns:repeat(7,1fr)}.cal-month-day-header{font-family:var(--font-heading);font-size:.75rem;font-weight:600;text-align:center;padding:.5rem;text-transform:uppercase;background:var(--bg-secondary);border-bottom:var(--border-width-sm) solid var(--border-color)}.cal-month-cell{min-height:90px;border:var(--border-width-sm) solid var(--border-color);padding:.25rem;cursor:pointer;transition:background var(--transition-fast)}.cal-month-cell:hover{background:var(--bg-secondary)}.cal-month-cell.other-month{opacity:.4}.cal-month-cell.today{border-color:var(--accent-primary);border-width:2px}.cal-month-cell-header{margin-bottom:.15rem}.cal-day-number{font-family:var(--font-heading);font-size:.8rem;font-weight:600}.cal-event-chip{font-size:.7rem;padding:1px 4px;margin-top:2px;border-radius:var(--radius-xs);background:var(--accent-blue);color:var(--text-on-accent);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:pointer}.cal-event-chip:hover{opacity:.85}.cal-event-chip.block-focus{background:var(--accent-red)}.cal-event-chip.block-personal{background:var(--accent-yellow);color:var(--text-primary)}.cal-event-chip.block-free_time{background:var(--accent-green);color:var(--text-primary)}.cal-event-more{font-size:.65rem;color:var(--text-secondary);padding:1px 4px}.month-day-detail{margin-top:1rem;border:var(--border-width) solid var(--border-color);padding:1rem;background:var(--bg-card);box-shadow:var(--shadow-offset) var(--shadow-offset) 0 var(--border-color)}.cal-day-detail-event{display:flex;gap:.75rem;padding:.5rem 0;border-bottom:1px solid var(--border-color);cursor:pointer}.cal-day-detail-event:hover{background:var(--bg-secondary)}.cal-detail-time{font-weight:600;white-space:nowrap;min-width:100px}.cal-detail-location{color:var(--text-secondary);font-size:.85rem}.cal-week-grid{border:var(--border-width) solid var(--border-color);overflow:hidden;background:var(--bg-card);box-shadow:var(--shadow-offset) var(--shadow-offset) 0 var(--border-color)}.cal-week-header{display:grid;grid-template-columns:60px repeat(7,1fr);border-bottom:var(--border-width-sm) solid var(--border-color);background:var(--bg-secondary)}.cal-week-day-header{text-align:center;padding:.5rem;font-family:var(--font-heading)}.cal-week-day-header.today{background:color-mix(in srgb,var(--accent-primary) 15%,transparent)}.cal-week-day-name{font-size:.75rem;text-transform:uppercase;font-weight:600}.cal-week-day-num{font-size:1rem;font-weight:700;display:block}.cal-week-allday-row{display:grid;grid-template-columns:60px repeat(7,1fr);border-bottom:var(--border-width-sm) solid var(--border-color);min-height:28px}.cal-allday-label{font-size:.7rem;color:var(--text-secondary);display:flex;align-items:center;justify-content:center}.cal-week-allday-cell{padding:2px;border-left:1px solid var(--border-color)}.cal-week-body{display:grid;grid-template-columns:60px repeat(7,1fr);position:relative;overflow-y:auto;max-height:70vh}.cal-week-time-gutter{position:relative}.cal-week-hour-label{position:absolute;right:.5rem;font-size:.7rem;color:var(--text-secondary);transform:translateY(-.5em)}.cal-week-day-col{position:relative;border-left:1px solid var(--border-color)}.cal-week-day-col.today{background:color-mix(in srgb,var(--accent-primary) 5%,transparent)}.cal-week-hour-line{position:absolute;left:0;right:0;border-top:1px dashed color-mix(in srgb,var(--border-color) 50%,transparent)}.cal-week-event{position:absolute;left:2px;right:2px;border:var(--border-width-sm) solid var(--border-color);border-radius:var(--radius-sm);padding:2px 4px;background:var(--accent-blue);color:var(--text-on-accent);overflow:hidden;cursor:pointer;z-index:10;font-size:.7rem}.cal-week-event:hover{opacity:.85}.cal-week-event-title{font-weight:600}.cal-week-event-time{font-size:.65rem;opacity:.85}.cal-week-event.block-focus{background:var(--accent-red)}.cal-week-event.block-personal{background:var(--accent-yellow);color:var(--text-primary)}.cal-week-event.block-free_time{background:var(--accent-green);color:var(--text-primary)}@media (max-width:600px){:root{--timeline-slot-h:22px}.cal-month-cell{min-height:64px;padding:4px}.cal-day-number{font-size:.95rem;font-weight:600}.cal-event-chip{font-size:.65rem}.cal-week-body{max-height:60vh}.cal-mobile-day{display:flex;flex-direction:column;height:70vh;user-select:none}.cal-mobile-day-header{padding:.75rem;font-weight:600;text-align:center;border-bottom:1px solid var(--border-color)}.cal-mobile-day-header.today{color:var(--accent-primary)}.cal-mobile-allday{padding:.5rem;display:flex;flex-direction:column;gap:.25rem;border-bottom:1px solid var(--border-color)}.cal-mobile-day-body{position:relative;flex:1;overflow-y:auto;display:grid;grid-template-columns:48px 1fr}.cal-mobile-day-col{position:relative;border-left:1px solid var(--border-color)}}@media (max-width:768px){.settings-page-layout{flex-direction:column}.settings-sidebar{width:100%;border-right:none;border-bottom:1px solid var(--border-color);padding:.75rem;flex-direction:row;flex-wrap:wrap;align-items:center}.settings-back-btn{margin-bottom:0;margin-right:.5rem;padding:.5rem}.settings-nav-items{flex-direction:row;flex-wrap:wrap;gap:.25rem}.settings-nav-item{padding:.5rem .75rem;border-left:none;border-radius:var(--radius-sm)}.settings-nav-item.active{border-left-color:transparent}.settings-content{padding:1rem}}
1 < \ No newline at end of file
@@ -3,9 +3,17 @@
3 3 <head>
4 4 <meta charset="UTF-8">
5 5 <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
6 + <!-- Phase 7 Tier 3 #13 — iOS / PWA chrome integration.
7 + theme-color is updated by js/themes.js when the theme changes so the
8 + iOS status bar and Android browser chrome match the active surface. -->
9 + <meta name="theme-color" content="#E0E4FA" id="meta-theme-color">
10 + <meta name="apple-mobile-web-app-capable" content="yes">
11 + <meta name="apple-mobile-web-app-status-bar-style" content="default">
12 + <meta name="apple-mobile-web-app-title" content="GoingsOn">
6 13 <title>GoingsOn</title>
7 14 <link rel="icon" type="image/svg+xml" href="favicon.svg">
8 15 <link rel="icon" type="image/png" sizes="32x32" href="../icons/32x32.png">
16 + <link rel="apple-touch-icon" href="../icons/128x128.png">
9 17 <link rel="stylesheet" href="css/styles.css">
10 18 </head>
11 19 <body>
@@ -30,6 +38,7 @@
30 38 <div class="header-actions">
31 39 <button class="sync-indicator" id="sync-indicator" onclick="GoingsOn.settings.openCloudSync()" title="Cloud Sync" aria-label="Cloud sync status" style="display:none;">
32 40 <span class="sync-dot" id="sync-dot"></span>
41 + <span class="sync-label" id="sync-label"></span>
33 42 </button>
34 43 <button class="settings-btn" onclick="GoingsOn.search.open()" title="Search (Cmd+K)" aria-label="Search">Search <kbd class="kbd-hint">&#8984;K</kbd></button>
35 44 <button class="settings-btn" onclick="GoingsOn.settings.open()" title="Settings" aria-label="Open settings">Settings</button>
@@ -106,6 +115,7 @@
106 115 Waiting Only
107 116 </label>
108 117 <div class="filter-actions">
118 + <span id="task-count" class="filter-count" aria-live="polite"></span>
109 119 <button class="btn btn-link" onclick="GoingsOn.tasks.clearFilters()">Clear filters</button>
110 120 </div>
111 121 </div>
@@ -171,7 +181,7 @@
171 181 </div>
172 182 <button class="btn btn-secondary" id="edit-project-btn" onclick="GoingsOn.projects.editCurrent()">Edit Project</button>
173 183 </div>
174 - <p id="project-dashboard-description" style="color: var(--text-secondary); margin-bottom: 1rem;"></p>
184 + <p id="project-dashboard-description" class="project-dashboard-desc"></p>
175 185 <div id="project-milestones-section" class="milestones-section"></div>
176 186 <div class="project-dashboard-grid">
177 187 <div class="dashboard-column">
@@ -207,7 +217,7 @@
207 217 <button class="btn btn-sm btn-primary" id="project-attach-btn">+ Attach</button>
208 218 </div>
209 219 <div id="project-attachments-list" class="dashboard-list">
210 - <div class="empty-dashboard-list">No attachments</div>
220 + <div class="empty-state empty-state--dashboard">No attachments</div>
211 221 </div>
212 222 </div>
213 223 </div>
@@ -251,13 +261,9 @@
251 261 <input type="date" id="day-plan-date" class="day-plan-date-picker" onchange="GoingsOn.dayPlan.onDatePickerChange()">
252 262 <span id="day-plan-date-display" class="day-plan-date-display"></span>
253 263 </div>
254 - <div class="view-toggle" id="day-plan-review-toggle" data-plan-pane="day-plan-pane" data-review-pane="day-review-pane">
255 - <button class="view-toggle-btn active" data-mode="plan" onclick="GoingsOn.planReviewToggle.setMode('day', 'plan')" title="Schedule your day">Plan</button>
256 - <button class="view-toggle-btn" data-mode="review" onclick="GoingsOn.planReviewToggle.setMode('day', 'review')" title="Reflect on your day">Review</button>
257 - </div>
264 + <span id="day-review-status-badge" class="review-status hidden"></span>
258 265 </div>
259 - <!-- Plan pane -->
260 - <div id="day-plan-pane" class="day-plan-content">
266 + <div class="day-plan-content">
261 267 <div class="day-plan-main" id="day-plan-container">
262 268 <p class="timeline-hint">Click and drag across time slots to schedule blocks</p>
263 269 <div class="timeline-container" id="timeline-container">
@@ -280,13 +286,13 @@
280 286 <div id="unscheduled-tasks" class="sidebar-task-list" role="list">
281 287 <!-- Virtual scroller renders items here -->
282 288 </div>
289 + <div id="day-accomplished-inline" class="day-accomplished-inline"></div>
283 290 </div>
284 291 </div>
285 - <!-- Review pane -->
286 - <div id="day-review-pane" class="hidden">
287 - <div id="day-review-content" class="day-review-inline">
288 - <p style="color: var(--text-secondary);">Loading...</p>
289 - </div>
292 + <div class="finish-review-bar">
293 + <button class="btn btn-primary finish-review-btn" id="day-finish-review-btn" onclick="GoingsOn.dayPlanSchedule.openFinishReviewModal()">
294 + Finish &amp; Review
295 + </button>
290 296 </div>
291 297 </div>
292 298
@@ -294,10 +300,6 @@
294 300 <div id="weekly-review-view" class="subview hidden" role="tabpanel" aria-labelledby="weekly-review-tab">
295 301 <div class="page-header">
296 302 <h2 class="page-title">Weekly Review</h2>
297 - <div class="view-toggle" id="week-plan-review-toggle" data-plan-pane="week-plan-pane" data-review-pane="week-review-pane">
298 - <button class="view-toggle-btn active" data-mode="plan" onclick="GoingsOn.planReviewToggle.setMode('week', 'plan')" title="Schedule your week">Plan</button>
299 - <button class="view-toggle-btn" data-mode="review" onclick="GoingsOn.planReviewToggle.setMode('week', 'review')" title="Reflect on your week">Review</button>
300 - </div>
301 303 </div>
302 304 <div id="weekly-review-content" class="weekly-review-content">
303 305 <div class="skeleton-shimmer" aria-label="Loading weekly review">
@@ -317,10 +319,7 @@
317 319 <button class="btn btn-secondary" onclick="GoingsOn.monthlyReview.nextMonth()" title="Next month">&rarr;</button>
318 320 <span id="monthly-review-month-display" class="monthly-review-month-display"></span>
319 321 </div>
320 - <div class="view-toggle" id="month-plan-review-toggle" data-plan-pane="month-plan-pane" data-review-pane="month-review-pane">
321 - <button class="view-toggle-btn active" data-mode="plan" onclick="GoingsOn.planReviewToggle.setMode('month', 'plan')" title="Set goals for the month">Plan</button>
322 - <button class="view-toggle-btn" data-mode="review" onclick="GoingsOn.planReviewToggle.setMode('month', 'review')" title="Reflect on your month">Review</button>
323 - </div>
322 + <span id="month-review-status-badge" class="review-status hidden"></span>
324 323 </div>
325 324 <div id="monthly-review-content" class="monthly-review-content">
326 325 <div class="skeleton-shimmer" aria-label="Loading monthly review">
@@ -432,6 +431,7 @@
432 431 <select class="form-select" id="email-label-filter" onchange="GoingsOn.emails.filterByLabel(this.value)" style="width: auto; min-width: 120px;">
433 432 <option value="">All labels</option>
434 433 </select>
434 + <span id="email-count" class="filter-count" aria-live="polite"></span>
435 435 </div>
436 436 <div id="email-bulk-actions" class="bulk-actions-bar hidden" role="toolbar" aria-label="Bulk email actions">
437 437 <span id="email-bulk-count" class="bulk-count">0 selected</span>
@@ -506,10 +506,17 @@
506 506 </div><!-- /messages-view -->
507 507
508 508 <!-- Settings View (standalone, not a tab group) -->
509 - <div id="settings-view" class="view hidden">
509 + </main>
510 + </div><!-- /app-body -->
511 +
512 + <!-- Settings Overlay (Phase 7 Tier 6 — settings as modeless drawer) -->
513 + <div id="settings-overlay" class="settings-overlay hidden" role="dialog" aria-modal="true" aria-labelledby="settings-overlay-title" aria-hidden="true">
514 + <div class="settings-overlay-backdrop" onclick="GoingsOn.settings.goBack()" aria-hidden="true"></div>
515 + <div id="settings-view" class="view settings-overlay-card">
510 516 <div class="settings-page-layout">
511 517 <nav class="settings-sidebar">
512 - <button class="settings-back-btn" onclick="GoingsOn.settings.goBack()" aria-label="Go back">&larr; Back</button>
518 + <button class="settings-back-btn" onclick="GoingsOn.settings.goBack()" aria-label="Close settings">&larr; Close</button>
519 + <h2 class="visually-hidden" id="settings-overlay-title">Settings</h2>
513 520 <div class="settings-nav-items">
514 521 <button class="settings-nav-item active" data-section="appearance" onclick="GoingsOn.settings.showSection('appearance')">Appearance</button>
515 522 <button class="settings-nav-item" data-section="notifications" onclick="GoingsOn.settings.showSection('notifications')">Notifications</button>
@@ -517,16 +524,15 @@
517 524 <button class="settings-nav-item" data-section="plugins" onclick="GoingsOn.settings.showSection('plugins')">Plugins</button>
518 525 <button class="settings-nav-item" data-section="sync" onclick="GoingsOn.settings.showSection('sync')">Cloud Sync</button>
519 526 <button class="settings-nav-item" data-section="data" onclick="GoingsOn.settings.showSection('data')">Import &amp; Export</button>
527 + <button class="settings-nav-item" data-section="about" onclick="GoingsOn.settings.showSection('about')">About</button>
520 528 </div>
521 529 </nav>
522 530 <div class="settings-content" id="settings-content">
523 531 <!-- Populated by JS -->
524 532 </div>
525 533 </div>
526 - </div><!-- /settings-view -->
527 -
528 - </main>
529 - </div><!-- /app-body -->
534 + </div>
535 + </div><!-- /settings-overlay -->
530 536
531 537 <!-- Modal -->
532 538 <div id="modal-overlay" class="modal-overlay hidden" role="dialog" aria-modal="true" aria-labelledby="modal-title">
@@ -545,6 +551,21 @@
545 551 <!-- Content populated dynamically by JS -->
546 552 </div>
547 553
554 + <!-- Task Detail Drawer (Phase 7 Tier 6 — side-drawer task detail) -->
555 + <aside id="task-detail-drawer" class="task-drawer" role="dialog" aria-modal="false" aria-labelledby="task-drawer-title" aria-hidden="true">
556 + <div class="task-drawer-header">
557 + <button class="btn btn-sm btn-secondary task-drawer-close" onclick="GoingsOn.taskOverview.close()" title="Close (Esc)" aria-label="Close task detail">x</button>
558 + <h2 class="task-drawer-title" id="task-drawer-title">Task</h2>
559 + <div class="task-drawer-actions" id="task-drawer-actions"></div>
560 + </div>
561 + <div class="task-drawer-content" id="task-drawer-content">
562 + <div class="skeleton-shimmer" aria-label="Loading task detail">
563 + <div class="skeleton-row"><div class="skeleton-lines"><div class="skeleton-line long"></div><div class="skeleton-line medium"></div></div></div>
564 + <div class="skeleton-row"><div class="skeleton-lines"><div class="skeleton-line medium"></div><div class="skeleton-line short"></div></div></div>
565 + </div>
566 + </div>
567 + </aside>
568 +
548 569 <!-- Mobile Bottom Tab Bar (hidden on desktop) -->
549 570 <nav id="mobile-tab-bar" class="mobile-tab-bar" role="tablist" aria-label="Mobile navigation">
550 571 <button class="mobile-tab active" data-view="work" role="tab" aria-selected="true">
@@ -575,11 +596,6 @@
575 596 </div>
576 597 </div>
577 598
578 - <!-- Third-party libraries -->
579 - <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"
580 - integrity="sha512-BNaRQnYJYiPSqHHDb58B0yaPfCu+Wgds8Gp/gU33kqBtgNS4tSPHuGibyoeqMV/TJlSKda6FXzoEyYGjTReTEA=="
581 - crossorigin="anonymous"
582 - referrerpolicy="no-referrer"></script>
583 599
584 600 <!-- Namespace (must load first) -->
585 601 <script src="js/goingson.js"></script>
@@ -592,6 +608,7 @@
592 608 <script src="js/components.js"></script>
593 609 <script src="js/form-modal.js"></script>
594 610 <script src="js/themes.js"></script>
611 + <script src="js/query-state.js"></script>
595 612
596 613 <!-- Utility Managers -->
597 614 <script src="js/cache.js"></script>
@@ -622,6 +639,7 @@
622 639 <script src="js/attachments.js"></script>
623 640 <script src="js/autocomplete.js"></script>
624 641 <script src="js/address-highlight.js"></script>
642 + <script src="js/compose-form.js"></script>
625 643 <script src="js/tasks.js"></script>
626 644 <script src="js/task-overview.js"></script>
627 645 <script src="js/events.js"></script>