diff --git a/AGENT_HANDOFF.md b/AGENT_HANDOFF.md
new file mode 100644
index 0000000..1e2dc75
--- /dev/null
+++ b/AGENT_HANDOFF.md
@@ -0,0 +1,80 @@
+# Monthy Tracker Agent Handoff
+
+Use this file as the starting point for any new agent. It summarizes the repo state, the important files, and the non-obvious behavior so you can continue without re-discovering basics.
+
+## What this app is
+
+- Single-user, local-first monthly expense tracker.
+- Stack: Next.js App Router, Prisma, SQLite, Ollama for offline AI.
+- Main views: dashboard, add expense, income/paychecks.
+- Goal: track expenses and paychecks, then generate private monthly summaries and recommendations.
+
+## Current state
+
+- Dashboard data is loaded from `/dashboard?month=YYYY-MM`.
+- Monthly insight generation lives in `src/lib/insights.ts`.
+- The app is currently working with the local dev DB at `file:./prisma/dev.db`.
+- Seed data exists for February 2026 and March 2026.
+- The frontend shows the current month by default, so if data looks missing, check the active month selector first.
+
+## Important files
+
+- `src/components/home-dashboard.tsx` - dashboard UI, fetches dashboard and Ollama status.
+- `src/lib/dashboard.ts` - aggregates totals, breakdowns, recent expenses, and comparisons.
+- `src/lib/insights.ts` - builds the model prompt, calls Ollama, stores fallback/model insights.
+- `src/app/dashboard/route.ts` - dashboard API route.
+- `src/app/backup/database/route.ts` - SQLite backup endpoint.
+- `src/lib/date.ts` - month/date helpers; month filtering uses local date strings.
+- `prisma/schema.prisma` - data model source of truth.
+- `prisma/seed.ts` - local seed script.
+- `README.md` - setup and runtime notes.
+- `openspec/changes/monthly-expense-tracker-v1/` - design/spec/task source for the product.
+
+## Key behavior to know
+
+- Dashboard aggregates all records, then filters them to the selected month in memory.
+- `getCurrentMonthKey()` uses local machine time.
+- `generateMonthlyInsight()` in `src/lib/insights.ts`:
+ - builds a structured prompt from dashboard totals, category breakdown, recent expenses, and chart points
+ - expects strict JSON with `summary` and `recommendations`
+ - falls back when activity is sparse or Ollama is unavailable
+ - stores the final result in `MonthlyInsight`
+- The fallback message is intentionally safe and short when data is too sparse.
+
+## Known gotchas
+
+- If dashboard values appear missing, confirm the selected month matches the seeded data.
+- If Docker backup fails, check `DATABASE_URL`; the backup route reads the SQLite path from the env var.
+- Prisma migration ordering matters because migration folders are applied lexicographically.
+- In Docker Compose on Linux, `extra_hosts: host-gateway` is needed for host Ollama access.
+
+## Current enhancement target
+
+- The next useful improvement is making the generated monthly summary more helpful and more structured.
+- Best place to change it: `src/lib/insights.ts`.
+- Likely changes:
+ - strengthen the prompt with clearer sections or required observations
+ - improve the fallback logic for sparse months
+ - optionally store more structured fields in `MonthlyInsight`
+ - update the dashboard card rendering in `src/components/home-dashboard.tsx` if the output shape changes
+
+## Useful commands
+
+```bash
+npm run dev
+npx prisma migrate deploy
+npx prisma db seed
+npm test
+```
+
+If you need to verify the dashboard API directly:
+
+```bash
+curl "http://localhost:3000/dashboard?month=2026-03"
+```
+
+## Recommended next step for another agent
+
+1. Read `src/lib/insights.ts`, `src/lib/dashboard.ts`, and `prisma/schema.prisma`.
+2. Decide whether to improve the insight prompt, the stored data shape, or the dashboard rendering.
+3. Make the change, then verify with the dashboard API and a generated insight.
diff --git a/next-env.d.ts b/next-env.d.ts
index c4b7818..9edff1c 100644
--- a/next-env.d.ts
+++ b/next-env.d.ts
@@ -1,6 +1,6 @@
///
///
-import "./.next/dev/types/routes.d.ts";
+import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/openspec/changes/theming-dark-mode/.openspec.yaml b/openspec/changes/theming-dark-mode/.openspec.yaml
new file mode 100644
index 0000000..0a32546
--- /dev/null
+++ b/openspec/changes/theming-dark-mode/.openspec.yaml
@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-03-23
diff --git a/openspec/changes/theming-dark-mode/design.md b/openspec/changes/theming-dark-mode/design.md
new file mode 100644
index 0000000..04a1530
--- /dev/null
+++ b/openspec/changes/theming-dark-mode/design.md
@@ -0,0 +1,54 @@
+## Context
+
+The project uses Next.js with Tailwind CSS v4 and a warm stone/amber palette. All styling is done via Tailwind utility classes directly in JSX. There are no separate CSS modules for components. The root layout owns the `` and `
` tags, making it the natural place to manage the theme class and prevent flash-of-unstyled-content.
+
+## Goals / Non-Goals
+
+**Goals:**
+- Implement class-based dark mode that works with Tailwind v4's `@custom-variant` API.
+- Prevent theme flash on page load with an inline blocking script.
+- Persist user preference in `localStorage` under the key `theme`.
+- Fall back to system preference (`prefers-color-scheme`) when no preference is saved.
+- Keep the dark palette warm and consistent with the existing amber/stone design language.
+
+**Non-Goals:**
+- Server-side theme rendering or cookies (local-only app, no SSR theme concerns beyond flash prevention).
+- Per-page or per-component theme overrides.
+- Animated theme transitions beyond the existing `transition-duration: 180ms` on interactive elements.
+
+## Decisions
+
+### Use Tailwind v4 `@custom-variant` for class-based dark mode
+- Rationale: Tailwind v4's default dark mode is media-query-based. Adding `@custom-variant dark (&:where(.dark, .dark *))` in `globals.css` enables the `dark:` prefix to respond to a class on any ancestor, which is the standard pattern for toggle-able dark mode.
+- Alternative considered: Continue using media-query dark mode with no toggle. Rejected because users cannot override system preference.
+
+### Store theme preference in `localStorage`
+- Rationale: Simple, synchronous, requires no server or cookies. Reads correctly in the blocking inline script.
+- Alternative considered: Cookies for SSR. Rejected because this app is local-first and has no meaningful SSR theme benefit.
+
+### Inject an inline blocking script in `` to set the `dark` class before first paint
+- Rationale: Prevents the flash where the page briefly renders in light mode before React hydrates and reads `localStorage`. The script is small and runs synchronously.
+- Alternative considered: Set the class in a React `useEffect`. Rejected because `useEffect` runs after paint, causing a visible flash.
+
+### Warm deep-stone dark palette
+- Rationale: The light theme is built on warm stone and amber tones. The dark theme mirrors this with deep stone backgrounds (`stone-950`, `stone-900`, `stone-800`) and dimmed amber/emerald accents, keeping the visual identity coherent.
+- Alternative considered: Neutral dark greys. Rejected because they clash with the warm amber accents and feel disconnected from the brand.
+
+## Risks / Trade-offs
+
+- [Inline script adds a small amount of render-blocking HTML] → Acceptable; the script is under 200 bytes and only runs once per page load.
+- [LocalStorage is unavailable in some privacy modes] → The script wraps the read in a try/catch and falls back to system preference.
+- [Many components have hardcoded warm background strings like `bg-[#fffaf2]`] → These are replaced with equivalent Tailwind tokens plus `dark:` overrides so the mapping is explicit and maintainable.
+
+## Migration Plan
+
+1. Update `globals.css` with `@custom-variant dark` and dark-mode CSS variable overrides.
+2. Create the `ThemeToggle` client component.
+3. Update `layout.tsx` to add the blocking script and render `ThemeToggle` in the header.
+4. Update `site-nav.tsx` with dark-mode nav styles.
+5. Update `home-dashboard.tsx`, `expense-workspace.tsx`, `paycheck-workspace.tsx`, and `recurring-expense-manager.tsx` with `dark:` variants.
+6. Update page-level header text that uses hardcoded colour classes.
+
+## Open Questions
+
+- Should the toggle also expose a "System" option (three-way: Light / Dark / System) rather than a binary flip? Deferred to a follow-up; the initialisation script already handles system fallback on first visit.
diff --git a/openspec/changes/theming-dark-mode/proposal.md b/openspec/changes/theming-dark-mode/proposal.md
new file mode 100644
index 0000000..68efaf1
--- /dev/null
+++ b/openspec/changes/theming-dark-mode/proposal.md
@@ -0,0 +1,27 @@
+## Why
+
+The app currently ships with a warm light theme only. Adding a system-aware dark mode with a manual toggle removes eye strain in low-light conditions and meets baseline accessibility expectations for a personal finance tool used at any hour.
+
+## What Changes
+
+- Add a class-based dark mode variant using Tailwind v4 `@custom-variant` so all `dark:` utilities are controlled by a `dark` class on ``.
+- Add a `ThemeToggle` component that persists the user's preference in `localStorage` and initialises the correct class before first paint to prevent flash.
+- Update the root layout to inject the inline initialisation script and render the toggle inside the existing header.
+- Apply `dark:` variants to all UI components to produce a warm, deep-stone dark palette consistent with the existing amber/stone design language.
+
+## Capabilities
+
+### New Capabilities
+- `dark-mode`: Users can switch between light and dark themes with a toggle that persists across sessions. System preference is respected on first visit.
+
+### Modified Capabilities
+- `expense-tracking`: Expense workspace and recurring expense manager now render correctly in both themes.
+- `paycheck-tracking`: Paycheck workspace now renders correctly in both themes.
+- `monthly-dashboard`: Dashboard sections, insight cards, category bars, and stat cards now render correctly in both themes.
+
+## Impact
+
+- Affected code: `globals.css`, `layout.tsx`, all components under `src/components/`, and page header text in `src/app/`.
+- APIs: None.
+- Dependencies: None — uses Tailwind v4 built-ins and the Web Storage API.
+- Systems: No server-side changes required; theme state is fully client-side.
diff --git a/openspec/changes/theming-dark-mode/tasks.md b/openspec/changes/theming-dark-mode/tasks.md
new file mode 100644
index 0000000..ff9639d
--- /dev/null
+++ b/openspec/changes/theming-dark-mode/tasks.md
@@ -0,0 +1,25 @@
+## 1. CSS and variant setup
+
+- [x] 1.1 Add `@custom-variant dark (&:where(.dark, .dark *))` to `globals.css` and update `:root` / `@theme` blocks with dark-mode CSS variable overrides.
+
+## 2. Theme toggle infrastructure
+
+- [x] 2.1 Create `src/components/theme-toggle.tsx` — a client component that reads and writes `localStorage` theme preference, toggles the `dark` class on ``, and renders a sun/moon button.
+- [x] 2.2 Update `src/app/layout.tsx` to inject the inline blocking script in `` and render `ThemeToggle` in the header alongside `SiteNav`.
+
+## 3. Component dark-mode styles
+
+- [x] 3.1 Update `src/components/site-nav.tsx` with `dark:` variants for link backgrounds, borders, and text.
+- [x] 3.2 Update `src/components/home-dashboard.tsx` with `dark:` variants for all section cards, stat tiles, insight blocks, progress bars, and empty states.
+- [x] 3.3 Update `src/components/expense-workspace.tsx` with `dark:` variants for the form card, list card, inputs, and expense articles.
+- [x] 3.4 Update `src/components/paycheck-workspace.tsx` with `dark:` variants for the schedule panel, form, and paycheck list.
+- [x] 3.5 Update `src/components/recurring-expense-manager.tsx` with `dark:` variants for the panel, inline form, and definition articles.
+
+## 4. Page header text
+
+- [x] 4.1 Update hardcoded text colours in `src/app/add-expense/page.tsx` and `src/app/income/page.tsx` with `dark:` overrides.
+
+## 5. Verification
+
+- [ ] 5.1 Visually verify light and dark modes across the dashboard, add-expense, and income pages in the browser.
+- [ ] 5.2 Verify that theme preference persists across page refreshes and that there is no flash of the wrong theme on load.
diff --git a/src/app/add-expense/page.tsx b/src/app/add-expense/page.tsx
index a8d5f7a..130b6e2 100644
--- a/src/app/add-expense/page.tsx
+++ b/src/app/add-expense/page.tsx
@@ -10,9 +10,9 @@ export default function AddExpensePage() {
return (
diff --git a/src/app/globals.css b/src/app/globals.css
index 7d833db..379b125 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -1,10 +1,17 @@
@import "tailwindcss";
+@custom-variant dark (&:where(.dark, .dark *));
+
:root {
--background: #ffffff;
--foreground: #171717;
}
+.dark {
+ --background: #1a1714;
+ --foreground: #f5f0eb;
+}
+
@theme inline {
--color-background: #fbfaf7;
--color-foreground: #1c1917;
diff --git a/src/app/income/page.tsx b/src/app/income/page.tsx
index 388fe56..c699634 100644
--- a/src/app/income/page.tsx
+++ b/src/app/income/page.tsx
@@ -8,9 +8,9 @@ export default function IncomePage() {
return (
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 3701781..29fe304 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -2,6 +2,7 @@ import type { Metadata } from "next";
import { Fraunces, Manrope } from "next/font/google";
import { SiteNav } from "@/components/site-nav";
+import { ThemeToggle } from "@/components/theme-toggle";
import "./globals.css";
@@ -20,6 +21,18 @@ export const metadata: Metadata = {
description: "Local-first monthly expense tracking with AI insights.",
};
+const themeScript = `
+(function() {
+ try {
+ var saved = localStorage.getItem('theme');
+ var prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
+ if (saved === 'dark' || (!saved && prefersDark)) {
+ document.documentElement.classList.add('dark');
+ }
+ } catch(e) {}
+})();
+`;
+
export default function RootLayout({
children,
}: Readonly<{
@@ -30,14 +43,20 @@ export default function RootLayout({
lang="en"
className={`${headingFont.variable} ${bodyFont.variable} h-full antialiased`}
>
-
+
+
+
+
diff --git a/src/components/expense-workspace.tsx b/src/components/expense-workspace.tsx
index 10e2057..28ba62c 100644
--- a/src/components/expense-workspace.tsx
+++ b/src/components/expense-workspace.tsx
@@ -207,24 +207,24 @@ export function ExpenseWorkspace({ categoryOptions }: Props) {
return (
-
+
-
+
{editingId ? "Edit expense" : "Daily entry"}
-
+
{editingId ? "Update this entry" : "Log today\u2019s spend in seconds"}
-
-
Current list total
-
{formatCurrencyFromCents(totalSpent)}
+
+
Current list total
+
{formatCurrencyFromCents(totalSpent)}