# Project Guidelines ## Finalizing Changes When making changes that affect published packages, create a changeset file: ```bash cat > .changeset/.md << 'EOF' --- "@gsxdsm/fusion": patch --- Short description of the change. EOF ``` Bump types: - **patch**: bug fixes, internal changes - **minor**: new features, new CLI commands, new tools - **major**: breaking changes Include the changeset file in the same commit as the code change. The filename should be a short kebab-case description (e.g., `fix-merge-conflict.md`, `add-retry-button.md`). Only create changesets for changes that affect the published `@gsxdsm/fusion` package — user-facing features, bug fixes, CLI changes, tool changes. Do NOT create changesets for internal docs (AGENTS.md, README), CI config, or refactors that don't change behavior. ## Package Structure - `@fusion/core` — domain model, task store (private, not published) - `@fusion/dashboard` — web UI + API server (private, not published) - `@fusion/engine` — AI agents: triage, executor, reviewer, merger, scheduler (private, not published) - `@gsxdsm/fusion` — CLI + pi extension (published to npm) Only `@gsxdsm/fusion` is published. The others are internal workspace packages. ## Storage Model Fusion uses a hybrid storage architecture: structured metadata lives in SQLite (`.fusion/fusion.db`) while large blob files (PROMPT.md, agent.log, attachments) remain on the filesystem under `.fusion/tasks/{ID}/`. The database runs in WAL mode for concurrent access. See [docs/storage.md](./docs/storage.md) for the full storage architecture documentation. ## Multi-Project Support Fusion supports multiple projects with a central registry at `~/.fusion/fusion-central.db`. Each project has its own SQLite database at `.fusion/fusion.db`. See [docs/multi-project.md](./docs/multi-project.md) for details on: - CentralCore API and project registration - Isolation modes (in-process, child-process) - Global concurrency management ## Testing ```bash pnpm test # run all tests pnpm build # build all packages ``` Tests are required. Typechecks and manual verification are not substitutes for real tests with assertions. ## Port 4040 is Reserved Port 4040 is the production dashboard port. A user's live dashboard session is typically running there. **Agents must NEVER:** - Run `kill`, `kill -9`, `pkill`, or `killall` against processes on port 4040 - Start a test server on port 4040 — always use `--port 0` for random free port ## Engine Process Rules The engine (`packages/engine`) runs the executor, merger, scheduler, IPC host, and dashboard-facing activity loop on a single Node event loop. **Blocking that loop stalls every task concurrently in-flight.** ### Never use `execSync` for User-Configured Commands `execSync` blocks the entire event loop until the child process exits. Any command from project settings — `testCommand`, `buildCommand`, `workflow step scripts`, etc. — **must** run via `promisify(exec)` with `timeout`. Never use `execSync` for user-configured commands. ```ts import { exec } from "node:child_process"; import { promisify } from "node:util"; const execAsync = promisify(exec); const { stdout, stderr } = await execAsync(command, { cwd: worktreePath, timeout: 120_000, maxBuffer: 10 * 1024 * 1024, }); ``` `execSync` is only acceptable for short, deterministic git plumbing (`git rev-parse`, `git branch -d`, `git worktree remove`, etc.). When in doubt, use async. ## Git Conventions - Commit messages: `feat(FN-XXX):`, `fix(FN-XXX):`, `test(FN-XXX):` - One commit per step (not per file change) - Always include the task ID prefix ## Node Dashboard Fusion has a Node Dashboard view for managing mesh network nodes. See [docs/architecture.md](./docs/architecture.md) for dashboard components and API endpoints. **Node Settings Sync API Endpoints:** | Method | Path | Description | |--------|------|-------------| | GET | `/api/nodes/:id/settings` | Fetch settings from a remote node | | POST | `/api/nodes/:id/settings/push` | Push local settings to a remote node | | POST | `/api/nodes/:id/settings/pull` | Pull settings from a remote node | | GET | `/api/nodes/:id/settings/sync-status` | Get sync status and diff summary | | POST | `/api/nodes/:id/auth/sync` | Sync model auth credentials | | POST | `/api/settings/sync-receive` | Receive pushed settings (inbound) | | POST | `/api/settings/auth-receive` | Receive auth credentials (inbound) | | GET | `/api/settings/auth-export` | Export local auth credentials | All remote node endpoints require the target node to have an `apiKey` configured. Inbound endpoints validate the `Authorization: Bearer ` header against the local node's apiKey. ## Pi Extension (`packages/cli/src/extension.ts`) The pi extension provides tools and a `/fn` command for interacting with fn from within a pi session. It ships as part of `@gsxdsm/fusion`. **Update the extension when:** - CLI commands change (behavior, flags, or output) - Task store / Agent store API changes (method signatures or behavior) - New user-facing features are added that chat agents should be able to use **Don't add tools for engine-internal operations** (move, step updates, logging, merge) — those are handled by the engine's own agents. The extension has no skills — tool descriptions give the LLM everything it needs. ## Agent Spawning (`spawn_agent` tool) The executor agent can spawn child agents that run in parallel. Each spawned agent: 1. Runs in its own git worktree (branched from the parent's worktree) 2. Receives a task prompt describing what to do 3. Executes autonomously until completion or termination 4. Reports status back to the parent via AgentStore ### Parameters | Parameter | Type | Description | |-----------|------|-------------| | `name` | `string` | Name for the child agent | | `role` | `string` | Role: `"triage"`, `"executor"`, `"reviewer"`, `"merger"`, `"engineer"`, or `"custom"` | | `task` | `string` | Task description for the child agent to execute | ### Settings - `maxSpawnedAgentsPerParent` (default: `5`) — Maximum children per parent agent - `maxSpawnedAgentsGlobal` (default: `20`) — Maximum total spawned agents per executor instance ### Lifecycle - Child agents are tracked in `AgentStore` with `reportsTo` set to the parent task ID - When the parent session ends, all spawned children are terminated - State transitions: `idle` → `active` → `running` → `active` (success) or `error` (failure) ### Error Handling - Per-parent and global limits are enforced with descriptive error messages - Failures during agent creation or worktree setup return error results - State update failures are non-blocking (logged but don't prevent execution) ## Agent Delegation Tools Two tools enable inter-agent delegation — discovering other agents and assigning work to them. ### `list_agents` Tool List all available agents in the system. Shows each agent's name, role, state, personality (soul), and current assignment. **Parameters:** | Parameter | Type | Description | |-----------|------|-------------| | `role` | `string` (optional) | Filter by agent role/capability (e.g., `"executor"`, `"reviewer"`) | | `state` | `string` (optional) | Filter by agent state (e.g., `"idle"`, `"active"`, `"running"`) | | `includeEphemeral` | `boolean` (optional) | Include ephemeral/runtime agents (default: `false`) | **Example usage:** ``` // Find all idle executor agents list_agents({ role: "executor", state: "idle" }) // See all agents including runtime task-workers list_agents({ includeEphemeral: true }) ``` ### `delegate_task` Tool Create a new task and assign it to a specific agent for execution. The task goes to `todo` and will be picked up by the target agent on their next heartbeat cycle. **Parameters:** | Parameter | Type | Description | |-----------|------|-------------| | `agent_id` | `string` (required) | The agent ID to delegate work to | | `description` | `string` (required) | What needs to be done | | `dependencies` | `string[]` (optional) | Task IDs this new task depends on | **Example workflow — CEO agent discovers QA agent and delegates testing:** ``` // 1. Discover available agents list_agents({ role: "qa" }) // → Returns QA agent with id "qa-agent-001" // 2. Delegate the testing task delegate_task({ agent_id: "qa-agent-001", description: "Run integration tests for the authentication module", dependencies: ["FN-100"] // depends on implementation being done }) // → Created FN-105: Delegated to QA Agent (qa-agent-001). // The task will be picked up on their next heartbeat cycle. ``` **Error cases:** - `"ERROR: Agent {agent_id} not found"` — The agent ID does not exist - `"ERROR: Cannot delegate to ephemeral/runtime agent {agent_id}"` — Cannot delegate to runtime task-worker agents (use `spawn_agent` for parallel worktree tasks instead) **Note:** The delegation tools are available to both executor agents and heartbeat agents when the agent store is configured. Use `list_agents` first to discover agent IDs and capabilities before delegating. ## Checkout Leasing Task ownership supports explicit checkout leases. Agents should be aware of: ### Conflict Semantics - Checkout conflicts return **409 Conflict** when another agent already holds the lease - Response shape: `{ error: "Task is already checked out", currentHolder, taskId }` - Clients **must not retry 409 automatically** — this is ownership contention, not a transient failure ### Heartbeat Enforcement `HeartbeatMonitor.executeHeartbeat()` validates checkout before work begins: - If `task.checkedOutBy` is set to another agent, the run exits with `reason: "checkout_conflict"` - Heartbeat execution does not auto-checkout — callers are responsible for obtaining checkout before starting work ## Per-Agent Heartbeat Configuration Each agent can override heartbeat behavior via `runtimeConfig`. Key settings: - `heartbeatIntervalMs` — How often heartbeats are triggered - `heartbeatTimeoutMs` — Time without heartbeat before agent is considered unresponsive - `maxConcurrentRuns` — Max concurrent heartbeat runs per agent See [docs/agents.md](./docs/agents.md) for the full configuration reference. ## Budget Governance Per-agent token budget tracking controls costs and prevents runaway AI spending. Budget enforcement happens at multiple points: - **HeartbeatMonitor.executeHeartbeat()** — Checks budget before creating sessions; skips when `isOverBudget: true` or `isOverThreshold: true` (for timer triggers) - **HeartbeatTriggerScheduler.onTimerTick()** — Skips timer ticks when budget is exceeded Agents can be paused by budget exhaustion. See [docs/agents.md](./docs/agents.md) for the full budget configuration reference. ## Heartbeat Trigger Scheduling `HeartbeatTriggerScheduler` manages three trigger mechanisms: - **Timer** — Periodic wakeup based on `heartbeatIntervalMs` - **Assignment** — Automatic wakeup when a task is assigned - **On-demand** — Manual trigger via `POST /api/agents/:id/runs` See [docs/agents.md](./docs/agents.md) for WakeContext and API details. ## Agent Performance Ratings Agent performance ratings allow users and agents to provide feedback that influences future behavior through system prompt injection. Ratings use a 1–5 scale with trend analysis (improving/declining/stable). See [docs/agents.md](./docs/agents.md) for the full API and dashboard configuration reference. ## Engine Diagnostic Logging The task executor, scheduler, and related subsystems use structured logging via `createLogger()` from `packages/engine/src/logger.ts`. All log lines are prefixed with the subsystem name. ### Key Diagnostic Points When debugging agent execution issues (agents stuck on "starting"), check these log points: 1. **`[executor] TaskExecutor constructed`** — Confirms the executor initialized with expected options 2. **`[executor] [event:task:moved] FN-XXX → in-progress`** — Confirms the scheduler moved the task 3. **`[executor] execute() called for FN-XXX`** — Confirms execute() was entered 4. **`[executor] FN-XXX: worktree ready at ...`** — Confirms worktree creation 5. **`[executor] FN-XXX: creating agent session`** — Confirms model resolution and session creation started 6. **`[pi] createKbAgent called`** — Confirms the agent factory was invoked 7. **`[pi] Session created successfully`** — Confirms the AI session was created 8. **`[executor] FN-XXX: calling promptWithFallback()...`** — Confirms the prompt was sent 9. **`[stuck-detector] Tracking task FN-XXX`** — Confirms heartbeat monitoring started ### Semaphore Resilience `AgentSemaphore` (`packages/engine/src/concurrency.ts`) has defensive guards: - `limit` getter returns minimum 1 (prevents indefinite blocking) - `availableCount` returns 0 for invalid limits (NaN, Infinity, ≤0) ## Headless Node Mode (`fn serve`) The `fn serve` command starts Fusion as a headless node (API server + AI engine, no frontend). It binds to `0.0.0.0` by default for remote accessibility. See [docs/architecture.md](./docs/architecture.md) for the full reference including health endpoint and startup banner. ## Settings fn uses a two-tier settings hierarchy: - **Global settings** — User preferences in `~/.fusion/settings.json` (theme, models, notifications) - **Project settings** — Project-specific settings in `.fusion/config.json` (concurrency, worktrees, commands) Project settings override global settings. Configure via the dashboard **Settings** modal or `fn settings` CLI. See [docs/settings-reference.md](./docs/settings-reference.md) for the complete settings reference. ### Settings Hierarchy for Model Selection **For Task Specification (Triage):** 1. Per-task `planningModelProvider`/`planningModelId` 2. Project `planningProvider`/`planningModelId` 3. Global `planningGlobalProvider`/`planningGlobalModelId` 4. Project `defaultProviderOverride`/`defaultModelIdOverride` 5. Global `defaultProvider`/`defaultModelId` 6. Automatic provider/model resolution **For Task Execution (Executor):** 1. Per-task `modelProvider`/`modelId` 2. Project `executionProvider`/`executionModelId` 3. Global `executionGlobalProvider`/`executionGlobalModelId` 4. Project `defaultProviderOverride`/`defaultModelIdOverride` 5. Global `defaultProvider`/`defaultModelId` 6. Automatic provider/model resolution **For Code/Spec Review (Reviewer):** 1. Per-task `validatorModelProvider`/`validatorModelId` 2. Project `validatorProvider`/`validatorModelId` 3. Global `validatorGlobalProvider`/`validatorGlobalModelId` 4. Project `defaultProviderOverride`/`defaultModelIdOverride` 5. Global `defaultProvider`/`defaultModelId` 6. Automatic provider/model resolution ## Per-Task Model Overrides Tasks can override project/global AI model settings on a per-task basis: - **Executor Model** — The model used to implement the task - **Validator Model** — The model used for code and plan review - **Planning Model** — The model used for task specification When both provider and modelId are set, the task override is used instead of global defaults. Set via the task detail modal's **Model** tab. ## Model Presets Model presets let teams standardize AI model choices. Each preset contains executor/validator model pairs. Presets can be auto-selected by task size (Small → Budget, Medium → Normal, Large → Complex). See [docs/settings-reference.md](./docs/settings-reference.md) for the full configuration reference. ## Mission Autopilot Missions can run in autopilot mode for autonomous progression. When enabled: - Autopilot watches task completion events - Automatically activates the next slice when the current one finishes - Progresses through: `inactive → watching → activating → completing` See [docs/missions.md](./docs/missions.md) for the full autopilot reference. ## Mission Planning Context When features are triaged to tasks, the system enriches descriptions with full mission hierarchy context (mission → milestone → slice → feature), giving implementation agents comprehensive context. See [docs/missions.md](./docs/missions.md) for the planning context system and interview flow documentation. ## Workflow Steps Workflow steps are reusable quality gates that run at configurable lifecycle phases: - **Pre-merge** — After task implementation, before merge (can block) - **Post-merge** — After successful merge (informational only) Steps can be defined as **prompt** (AI agent review) or **script** (deterministic command). See [docs/workflow-steps.md](./docs/workflow-steps.md) for the full reference including templates, API, and execution details. ## Run Audit The run-audit system records every mutation performed by the engine across three domains: - **Database** — task:create, task:update, task:move, etc. - **Git** — worktree:create, commit:create, merge:resolve, etc. - **Filesystem** — file:write, prompt:write, attachment:create, etc. Events are tied to specific run IDs for end-to-end traceability. See [docs/architecture.md](./docs/architecture.md) for the audit API reference. ## Archive Cleanup Archived tasks can be cleaned up from the filesystem while preserving metadata. Restored tasks keep all metadata but lose attachments and agent logs. See [docs/task-management.md](./docs/task-management.md) for the archive and restore reference. ## Dashboard UI Styling Guide This guide documents the dashboard's design system so that any AI agent or developer building new UI components follows established conventions automatically. All CSS lives in `packages/dashboard/app/styles.css` (≈60K lines). For deeper context on the theme system and known pitfalls, see `.fusion/memory/MEMORY.md`. --- ### Design Tokens All new CSS **must** use these token variables instead of hardcoded values. Tokens are defined at `:root` and adapted for light mode via `[data-theme="light"]`. | Token | Value | Purpose | |-------|-------|---------| | `--space-xs` | `4px` | Tight inline spacing | | `--space-sm` | `8px` | Small gaps | | `--space-md` | `12px` | Default gaps | | `--space-lg` | `16px` | Section padding | | `--space-xl` | `24px` | Large section padding | | `--space-2xl` | `32px` | Extra-large spacing | | `--radius-sm` | `4px` | Small corners | | `--radius-md` | `8px` | Default corners | | `--radius-lg` | `12px` | Card/modal corners | | `--radius-xl` | `16px` | Large corners | | `--radius-pill` | `10px` | Pill/badge corners | | `--shadow-sm` | `0 1px 2px rgba(0,0,0,0.1)` | Subtle lift | | `--shadow-md` | `0 4px 6px rgba(0,0,0,0.1)` | Standard lift | | `--shadow-lg` | `0 4px 24px rgba(0,0,0,0.4)` | Modals, dropdowns | | `--shadow-glow` | `0 0 8px rgba(88,166,255,0.3)` | Focus glow | | `--glow-success` | `0 0 8px rgba(46,160,67,0.3)` | Success state | | `--glow-danger` | `0 0 8px rgba(248,81,73,0.3)` | Danger state | | `--glow-warning` | `0 0 8px rgba(227,179,65,0.3)` | Warning state | | `--focus-ring` | `0 0 0 2px rgba(88,166,255,0.15)` | Subtle focus ring | | `--focus-ring-strong` | `0 0 0 2px rgba(88,166,255,0.3)` | Prominent focus ring | | `--transition-instant` | `0.1s ease` | Immediate | | `--transition-fast` | `0.15s ease` | Quick | | `--transition-normal` | `0.2s ease` | Default | | `--transition-slow` | `0.3s ease` | Smooth | | `--font-primary` | system font stack | Body font | | `--font-mono` | monospace stack | Code, IDs | | `--header-height` | `57px` | Fixed header height | | `--mobile-nav-height` | `44px` | Mobile nav bar | | `--standalone-bottom-gap` | `0px` / `8px` (PWA) | iOS home bar gap | | `--overlay-padding-top` | `10vh` | Modal vertical position | **Never** hardcode pixel values, colors, or durations in component CSS. Always reference a token. The sole exception is inside `:root` or theme blocks where tokens are *defined*. --- ### Color Variables Core palette (dark defaults at `:root`, overridden in `[data-theme="light"]`): | Variable | Purpose | |----------|---------| | `--bg` | Page background | | `--surface` | Elevated surface | | `--card` | Card background | | `--card-hover` | Card hover state | | `--border` | Borders and dividers | | `--text` | Primary text | | `--text-muted` | Secondary/muted text | | `--text-dim` | Tertiary/disabled text | Task status colors (semantic — consistent meaning across all 54 color themes): | Variable | Status | |----------|--------| | `--triage` | Triage | | `--todo` | Todo | | `--in-progress` | In Progress | | `--in-review` | In Review | | `--done` | Done | Semantic status colors: | Variable | Purpose | |----------|---------| | `--color-success` | Success green | | `--color-error` | Error red (light) | | `--color-error-dark` | Error red (dark) | | `--color-warning` | Warning amber | | `--color-info` | Info blue | | `--color-muted` | Muted gray | **Rule:** Never use raw hex or `rgba(...)` for colors in component styles. Use `var(--token)` or `color-mix(in srgb, var(--color) X%, transparent)` for translucent backgrounds. The only place hardcoded colors are acceptable is inside `:root` theme blocks defining tokens. Status background tokens already use `color-mix` for theme adaptability — reuse them rather than creating parallel variants: ``` --status-triage-bg, --status-todo-bg, --status-in-progress-bg, --status-in-review-bg, --status-done-bg, --status-archived-bg, --status-error-bg, --status-error-bg-deep ``` --- ### Theme System The dashboard supports **dark/light modes** (controlled by `data-theme` attribute) plus **54 color themes** (controlled by `data-color-theme` attribute). Theme blocks live in `packages/dashboard/app/public/theme-data.css` and are lazy-loaded when a non-default theme is active. **Token categories:** 1. **Base tokens** (`--bg`, `--surface`, `--text`, etc.) — redefined in every theme block (dark + light for each of the 54 themes). Components using these tokens adapt automatically. 2. **Semantic tokens** (tokens with consistent *meaning* across all themes, like `--autopilot-pulse`, `--event-error-text`, `--badge-mission-*`, `--fab-*`) — only need dark/light adaptation via `[data-theme="light"]`. They do NOT need per-color-theme overrides because the semantic meaning is always consistent. 3. **Status tokens** (`--triage`, `--todo`, `--in-progress`, etc.) — redefined per theme block to match each theme's palette. **Adding theme-aware CSS custom properties:** - For **base** tokens: add to `:root`, `[data-theme="light"]`, and all 54 theme blocks (dark + light variants) - For **semantic** tokens: add to `:root` and `[data-theme="light"]` only - For **status** tokens: add to `:root` and all theme blocks The automated test in `status-colors-theme.test.ts` iterates all theme blocks to catch regressions. **New components must use `var(--token)` references** so themes apply without any per-component dark/light handling. --- ### Component Classes Reuse these existing CSS classes rather than creating parallel styles. They live in `styles.css`. #### Buttons | Class | Purpose | |-------|---------| | `.btn` | Base button | | `.btn-primary` | Primary CTA (uses `--cta-bg`) | | `.btn-danger` | Destructive action (red) | | `.btn-warning` | Warning action (amber) | | `.btn-sm` | Compact button (4px 10px padding, 12px font) | | `.btn-icon` | Icon-only button (square, icon fills) | | `.btn-icon--active` | Active icon button state | | `.btn-badge` | Notification badge on button | All buttons use `--btn-padding`, `--btn-border-width`, `--transition-fast`, and `--radius-md` tokens. All have `:focus-visible` using `--focus-ring-strong` and `:active` using `transform: scale(0.97)`. #### Modals | Class | Purpose | |-------|---------| | `.modal-overlay` | Backdrop; use with `.open` class | | `.modal-overlay.open` | Visible overlay (uses `backdrop-filter: blur(4px)`) | | `.modal` | Default modal (480px wide, `--radius-lg`, `--shadow-lg`) | | `.modal-lg` | Large modal (640px wide) | | `.modal-header` | Modal title bar (flex, space-between) | | `.modal-close` | Close button (× icon, hover color change) | | `.modal-actions` | Action bar at modal bottom | | `.modal-actions-left` | Left-aligned actions (use `margin-right: auto`) | | `.modal-actions-right` | Right-aligned actions | The overlay uses `position: fixed; inset: 0; z-index: 100;`. Pad the top with `--overlay-padding-top` (default 10vh) to center vertically. #### Forms | Class | Purpose | |-------|---------| | `.form-group` | Field container with `--space-xl` horizontal padding | | `.form-group label` | Uppercase label (12px, 0.5px letter-spacing) | | `.input` | Global input (surface bg, `--radius-sm`) | | `.select` | Global select (same style as `.input`) | | `.checkbox-label` | Checkbox label (flex, gap, no uppercase) | | `.form-error` | Error box (uses `color-mix`, `--color-error`) | Inputs and selects in `.form-group` get focus styles (`border-color: var(--todo)` + `--focus-ring`). Global `.input` and `.select` classes apply independently. #### Cards | Class | Purpose | |-------|---------| | `.card` | Task card base (grab cursor, `--card-padding`) | | `.card-header` | Card top row (flex, gap: 6px) | | `.card-id` | Monospace task ID (11px, `--text-muted`) | | `.card-title` | Card title (13px, break-word) | | `.card-meta` | Meta row (flex, gap, `--space-sm`) | | `.card-status-badge` | Status badge (pill shape, status-bg/text colors) | | `.card-status-badge--triage` | Triage badge variant | | `.card-status-badge--todo` | Todo badge variant | | `.card-status-badge--in-progress` | In-Progress badge variant | | `.card-status-badge--in-review` | In-Review badge variant | | `.card-status-badge--done` | Done badge variant | | `.card-status-badge--archived` | Archived badge variant | Cards have `--focus-ring-strong` focus style and `--card-hover` background on hover. Use `.card-status-badge--{status}` classes for column-color badges. #### Utility | Class | Purpose | |-------|---------| | `.touch-target` | 44px minimum touch target (Apple HIG / WCAG 2.5.8) | | `.visually-hidden` | Screen-reader-only (clip rect) | --- ### Mobile Responsive **Breakpoints** (use `@media (max-width: N)`): | Breakpoint | Value | Use | |------------|-------|-----| | Mobile | `768px` | Primary mobile breakpoint | | Tablet | `1024px` | `min-width: 769px and max-width: 1024px` | | Small | `640px` | Compact mobile | | XSmall | `480px` | Very narrow devices | **Mobile CSS placement:** All mobile overrides go in `@media (max-width: 768px)` blocks at the **bottom** of `styles.css`, after their base styles. **Bottom spacing:** - `--mobile-nav-height` (`44px`) controls mobile nav bar height - `--standalone-bottom-gap` (`0px` default, `8px` in PWA `display-mode: standalone`) adds iOS home indicator breathing room - All bottom-positioned elements use `calc(..., var(--mobile-nav-height), env(safe-area-inset-bottom, 0px), var(--standalone-bottom-gap))` **Touch targets:** All interactive elements must be at least **36px** on mobile. Use `.touch-target` for elements below this threshold (which sets `44px` minimum). Inside mobile media queries, individual component touch targets may be `36px`. **Safe area:** Use `max(var(--space-md), env(safe-area-inset-left, 0px))` pattern for content respecting device notches on mobile. --- ### Adding New CSS 1. **Always use tokens** — `var(--space-md)`, `var(--text-muted)`, `var(--radius-md)`, `var(--transition-fast)`, etc. Never write `padding: 8px` or `color: #e6edf3` directly. 2. **Section headers** — Mark new component sections with `/* === ComponentName === */` in `styles.css` so they are discoverable. 3. **Reuse existing classes** — Don't create parallel button or form styles. Add states (`:hover`, `:focus-visible`, `:active`) to the existing `.btn`, `.card`, `.input` chains. 4. **Theme-aware backgrounds** — Use `color-mix(in srgb, var(--color) X%, transparent)` instead of `rgba(...)`. For example, error backgrounds: `color-mix(in srgb, var(--color-error) 10%, transparent)`. 5. **Accessibility** — Add `:focus-visible` styles using `var(--focus-ring-strong)` on every interactive component. Never suppress focus entirely. 6. **Test both themes** — Verify new styles look correct in both dark and light modes before committing. 7. **Mobile overrides** — Add mobile variants below the base styles, inside a `@media (max-width: 768px)` block. --- ### Common Pitfalls - **`--surface-hover` is undefined** — This token is referenced in several places but never defined in `:root` or theme blocks. Use a fallback: `var(--surface-hover, rgba(0,0,0,0.03))` or define the token explicitly. - **Hardcoded `rgba(...)` for error states** — Use `color-mix(in srgb, var(--color-error) 10%, transparent)` instead of `rgba(248, 81, 73, 0.1)`. - **`.form-error` style** — Should use `color-mix(in srgb, var(--color-error) 10%, transparent)` for the background, not hardcoded rgba. - **`lucide-react` icon changes** — When adding new icons, update test mocks (`vi.mock("lucide-react")`) immediately. Missing mock exports cascade into runtime failures. - **Light-theme overrides** — Components using `var(*)` tokens generally inherit correctly from `[data-theme="light"]` root redefinitions. Only add explicit `[data-theme="light"]` overrides where fine-tuning is needed (opacity, subtle shadows). - **CSS regex tests in test files** — When changing mobile CSS values (e.g., `min-height`), update both the CSS and the corresponding test assertions. Use non-greedy `[^}]*` patterns for block-scoped regex, not `[\s\S]*` which can bleed across block boundaries. - **BEM specificity conflicts** — When a container state class (`.quick-entry-box--expanded`) and an element modifier (`.quick-entry-input--expanded`) both target the same element, the container may win due to higher specificity. Use `:not(.modifier)` to scope container rules: `.quick-entry-box--expanded .quick-entry-input:not(.quick-entry-input--expanded)`. - **CSS in `@media` blocks** — Don't search backwards for the nearest `@media` to check if a rule is mobile-scoped. Track brace depth to confirm the line is inside the block. Many components are defined globally even if they only visually appear on mobile.