diff --git a/.gitignore b/.gitignore
index 0b758ae..8a48f7d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -101,3 +101,6 @@ graphify-out/
# Retrieval model files (rag/embed.py MODEL_CACHE), fetched at build time
backend/.models/
+.superpowers/
+frontend/test-results/
+frontend/playwright-report/
diff --git a/docs/CODEBASE-GUIDE.md b/docs/CODEBASE-GUIDE.md
index 2481949..aa8dd78 100644
--- a/docs/CODEBASE-GUIDE.md
+++ b/docs/CODEBASE-GUIDE.md
@@ -259,6 +259,19 @@ embeds the demo documents once per server and uploads the files in the backgroun
`globals.css`). `components/dashboard-shell.tsx` is just a page's content wrapper and optional header card.
- There is no root `loading.tsx`: the pages are static and prefetched, and a full-screen spinner made every click
look like a reload.
+- **Sky theme (UI revamp, `docs/superpowers/specs/2026-10-20-ui-revamp-design.md`).** `app/(app)/layout.tsx` wraps
+ the shell in `
`. In `globals.css`, `:root:has([data-app-theme="sky"])`
+ swaps every shadcn colour token to the light Sky palette, so portaled menus and dialogs match too. The
+ `dark:` variant is also switched off while the shell is mounted. Public pages keep the forced dark theme.
+ Extra tokens: `good`, `bad`, `accent-soft`, `--band`. Type roles: `font-serif` (Newsreader), `font-label` /
+ `.label-mono` (IBM Plex Mono labels).
+- **No layout shift from overlays.** `globals.css` sets `scrollbar-gutter: stable` and cancels Radix's scroll-lock
+ margin (`body[data-scroll-locked]`), so opening a menu or dialog never moves the page. Guarded by
+ `npm run test:e2e` (Playwright, local only), which drives the dev-only harness `app/dev/layout-shift` (404 in
+ production). Firefox is opt-in with `E2E_FIREFOX=1`.
+- **Motion:** use `motion/react` only (framer-motion was removed). Shared timings live in `lib/motion.ts`
+ (`DURATION`, `EASE_OUT`, `fadeUp`). Reduced motion is honoured globally by `MotionConfig reducedMotion="user"`
+ in the app layout and a CSS rule in `globals.css`.
## 7c. Ask Lumen screen (frontend, UI phase)
- `/chatbot` (`app/chatbot/chatbotContent.tsx`) is the one chat with the agent (the old `/agent` test page is gone).
diff --git a/docs/direction/STORY.md b/docs/direction/STORY.md
index 6239667..e71d065 100644
--- a/docs/direction/STORY.md
+++ b/docs/direction/STORY.md
@@ -311,3 +311,18 @@ with the numbers and the files that prove them. Newest last. Raw numbers live in
the old regime's slabs ("first ₹2.5 lakh at 0%"). Fixing that, and L05 (unused subscriptions) and L08 (savings
goal), needs a prompt/tool change and a new recording. Story point: a grader bug can hide a dishonest answer
as easily as it can fail a right one.
+
+## 27. Stopping overlays from nudging the page (UI revamp, Phase 0)
+- **S:** Opening the profile menu shifted the Ask Lumen chat column sideways. Every menu, dialog, select and sheet in
+ the app did the same, because Radix's scroll lock adds a margin equal to the scrollbar width.
+- **T:** Fix it once for the whole app, prove it stays fixed, and lay the light "Sky" theme over the signed-in app
+ without touching the dark public pages.
+- **A:** A dev-only harness page with one of each overlay and a Playwright check that measures the content column
+ before and after opening each one. The headless browser's hidden scrollbars first masked the bug, so the check
+ turns them back on. The fix is two CSS rules: a stable scrollbar gutter and cancelling the lock's margin. Sky tokens
+ are keyed on `:root:has([data-app-theme="sky"])`, so portaled menus match and the `dark:` variant switches off
+ in-app. One animation library instead of two, with reduced motion honoured globally.
+- **R:** Shift dropped from 7.5 px to 0 on 4 of the 5 overlays (the fifth, non-modal, was already fine).
+ 12 e2e checks pass, including portal colours, AA contrast for muted text (4.9:1), login staying dark and
+ reduced motion. framer-motion was removed. Story point: reproduce in a test first. Here the test environment
+ was hiding the very bug.
diff --git a/docs/superpowers/plans/2026-10-20-ui-revamp-phase0.md b/docs/superpowers/plans/2026-10-20-ui-revamp-phase0.md
new file mode 100644
index 0000000..62bcb56
--- /dev/null
+++ b/docs/superpowers/plans/2026-10-20-ui-revamp-phase0.md
@@ -0,0 +1,342 @@
+# UI Revamp Phase 0 (Foundations) Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Lay the ground every later revamp phase builds on: no layout shift from overlays, the Sky light theme scoped to the signed-in app, the new type roles, and one motion setup that honours reduced-motion.
+
+**Architecture:** One global CSS fix neutralises Radix's scroll-lock compensation, so every overlay stops nudging content. Sky tokens live in `globals.css` under `:root:has([data-app-theme="sky"])`. That puts them on `` whenever the app shell is mounted, so portaled menus and dialogs pick them up too. The public pages keep the forced dark theme. A dev-only harness page plus Playwright (Chromium and Firefox) proves all of it.
+
+**Tech Stack:** Next.js 14 app router, Tailwind v4 (`@theme inline`), shadcn/Radix, `next/font`, `motion` v12 (`motion/react`), `@playwright/test` (dev only).
+
+**Spec:** `docs/superpowers/specs/2026-10-20-ui-revamp-design.md` (sections 5, 6; phase 0 of section 10). Mockup: `docs/superpowers/specs/2026-10-20-ui-revamp/home-mockup.html`.
+
+## Global Constraints
+
+- Sky is light only and scoped to the signed-in `(app)` route group. Landing, login and onboarding stay dark, and the root layout's forced `dark` class remains.
+- Fonts: Funnel Sans (UI and numbers), Newsreader (serif headlines and brief), IBM Plex Mono (labels, 10.5px, 0.12em tracking, uppercase). Load all of them through `next/font` with no FOUT-driven shift.
+- Motion: 150–250ms, ease-out, opacity and transform only. Everything off under `prefers-reduced-motion`. Use the single library `motion` and remove `framer-motion`.
+- Zero layout-shift rule: an interaction in one region may not change the size or position of another region.
+- Playwright is a dev dependency and runs locally only. Do not add it to `.github/workflows/ci.yml`.
+- Git: branch from `refactor`. Do not use "claude" in branch names. Open one PR into `refactor` on `coderconnoisseur/Lumen`. Keep `docs/CODEBASE-GUIDE.md` current.
+
+## Review Focus
+
+1. **Portaled overlays outside the app wrapper** (menus, dialogs, selects render into ``): they must use Sky colours, not the dark theme. Task 2 test `portaled menu is sky`.
+2. **Firefox on Windows** (the owner's browser, where the bug was seen) has classic scrollbars. The shift test runs in Firefox as well as Chromium. Task 1 config.
+3. **Muted text contrast:** the mockup's `#7c7a74` is only 3.9:1 on the page background. The plan uses `#6b6963` (4.9:1 on bg, 4.7:1 on sidebar). Task 2 test `muted text meets AA`.
+4. **Public pages must stay dark:** Sky must not leak onto `/login`. Task 2 test `login stays dark`.
+5. **Reduced motion:** transitions must collapse to near zero. Task 3 test `reduced motion kills transitions`.
+
+## File map
+
+| File | Responsibility |
+|---|---|
+| `frontend/playwright.config.ts` (new) | Two projects (chromium, firefox), dev server on port 3100 with dummy public env |
+| `frontend/e2e/foundations.spec.ts` (new) | All Phase 0 checks |
+| `frontend/src/app/dev/layout-shift/page.tsx` (new) | Dev-only harness: Sky wrapper, centred column, tall content, one of each overlay. Returns `notFound()` in production |
+| `frontend/src/app/globals.css` | Scroll-lock neutraliser, Sky token block, label utility, reduced-motion rule, font theme vars |
+| `frontend/src/app/layout.tsx` | Add IBM Plex Mono via `next/font` |
+| `frontend/src/app/(app)/layout.tsx` | `data-app-theme="sky"` on the shell; `MotionConfig reducedMotion="user"` |
+| `frontend/src/lib/motion.ts` (new) | Shared durations and easing |
+| `frontend/src/components/ai-analytics/*.tsx` (7 files) | `framer-motion` → `motion/react` |
+| `frontend/package.json` | `+@playwright/test` (dev), `-framer-motion`, script `test:e2e` |
+| `docs/CODEBASE-GUIDE.md` | Note the theme scope, harness and e2e command |
+
+---
+
+### Task 1: Layout-shift guard (harness, failing test, global fix)
+
+**Files:**
+- Create: `frontend/playwright.config.ts`, `frontend/e2e/foundations.spec.ts`, `frontend/src/app/dev/layout-shift/page.tsx`
+- Modify: `frontend/src/app/globals.css` (inside `@layer base`), `frontend/package.json`
+
+**Interfaces:**
+- Produces: harness route `/dev/layout-shift` with test ids `probe` (the centred column), and `open-dropdown`, `open-dialog`, `open-select`, `open-popover`, `open-sheet` (triggers). It also has `transition-probe`, an element with `transition: opacity 200ms`, which Task 3 uses. The npm script `test:e2e` runs `playwright test`. Later tasks add tests to `e2e/foundations.spec.ts`.
+
+- [ ] **Step 1: Install and configure Playwright**
+
+ 1. Run `npm i -D @playwright/test` and then `npx playwright install chromium firefox`.
+ 2. Add `"test:e2e": "playwright test"` to `package.json` scripts.
+ 3. Write `playwright.config.ts` with these settings:
+ - `testDir: "e2e"`
+ - `use.baseURL: "http://localhost:3100"`
+ - `projects: [{name:"chromium",use:devices["Desktop Chrome"]},{name:"firefox",use:devices["Desktop Firefox"]}]`
+ - `webServer: { command: "npx next dev -p 3100", url: "http://localhost:3100/dev/layout-shift", reuseExistingServer: false, timeout: 180_000, env: { NEXT_PUBLIC_SUPABASE_URL: "http://127.0.0.1:54321", NEXT_PUBLIC_SUPABASE_ANON_KEY: "e2e-dummy", NEXT_PUBLIC_BACKEND_URL: "http://127.0.0.1:9" } }`
+
+ There is no `.env.local` in the repo. The dummy values only have to let `createClient` construct. If the Next dev error overlay still appears because a backend or Supabase call fails, make that call fail quietly at its source. Do not hide the overlay.
+
+- [ ] **Step 2: Write the harness page `src/app/dev/layout-shift/page.tsx`**
+
+ - Make it a client component that renders inside a wrapper `
`.
+ - At render time, if `process.env.NODE_ENV === "production"`, call `notFound()`.
+ - Inside the wrapper, add ``. Fill it with about 3000px of paragraphs so the page scrolls.
+ - Put one trigger of each kind at the top of `main`, using the existing wrappers in `@/components/ui/*`: DropdownMenu, Dialog, Select, Popover and Sheet, each with the test ids listed above.
+ - Add ``.
+
+- [ ] **Step 3: Write the failing test**
+
+```ts
+// e2e/foundations.spec.ts
+import { test, expect } from "@playwright/test";
+
+const overlays = [
+ ["open-dropdown", '[role="menu"]'],
+ ["open-dialog", '[role="dialog"]'],
+ ["open-select", '[role="listbox"]'],
+ ["open-popover", "[data-radix-popper-content-wrapper]"],
+ ["open-sheet", '[role="dialog"]'],
+] as const;
+
+for (const [trigger, surface] of overlays) {
+ test(`opening ${trigger} does not move the page`, async ({ page }) => {
+ await page.goto("/dev/layout-shift");
+ const probe = page.getByTestId("probe");
+ const before = await probe.boundingBox();
+ await page.getByTestId(trigger).click();
+ await expect(page.locator(surface).first()).toBeVisible();
+ const after = await probe.boundingBox();
+ expect(after!.x).toBeCloseTo(before!.x, 0);
+ expect(after!.width).toBeCloseTo(before!.width, 0);
+ });
+}
+```
+
+- [ ] **Step 4: Run it and confirm it fails**
+
+ Run `npm run test:e2e -- -g "does not move"`. The expected result is FAIL on at least the modal overlays (dropdown, dialog, select, sheet), where `x` or `width` differs by roughly the scrollbar width.
+
+ If nothing fails, the harness is not reproducing the bug. Check that the page really scrolls and that the overlays are modal before you go on. Do not skip this check.
+
+- [ ] **Step 5: Add the global fix in `globals.css` `@layer base`**
+
+```css
+html { scrollbar-gutter: stable; }
+/* Radix modals lock scroll via react-remove-scroll, which adds margin/padding equal to the scrollbar width and
+ shifts centred content. The gutter already reserves that space, so cancel the compensation. Higher specificity
+ than the runtime-injected `body[data-scroll-locked]` rule, which is appended after this sheet. */
+html body[data-scroll-locked] { margin-right: 0 !important; padding-right: 0 !important; }
+```
+
+- [ ] **Step 6: Run the tests and confirm they pass in both browsers**
+
+ Run `npm run test:e2e -- -g "does not move"`. The expected result is 10 passed (5 overlays × 2 browsers).
+
+ If any test still fails, debug it with superpowers:systematic-debugging before you change the approach. Do not patch individual components with `modal={false}` unless the root cause is component-specific.
+
+- [ ] **Step 7: Confirm by hand in the real app**
+
+ Run `npm run dev` with the real env, start a demo session, open Ask Lumen and open the profile menu. The chat column must not move. Check in Firefox.
+
+- [ ] **Step 8: Commit**
+
+```bash
+git add frontend/playwright.config.ts frontend/e2e frontend/src/app/dev frontend/src/app/globals.css frontend/package.json frontend/package-lock.json
+git commit -m "fix: overlays no longer shift page content; add layout-shift e2e guard"
+```
+
+---
+
+### Task 2: Sky visual system (tokens, fonts, label style)
+
+**Files:**
+- Modify: `frontend/src/app/globals.css`, `frontend/src/app/layout.tsx`, `frontend/src/app/(app)/layout.tsx`
+- Test: `frontend/e2e/foundations.spec.ts`
+
+**Interfaces:**
+- Consumes: the harness from Task 1, whose wrapper already carries `data-app-theme="sky"`.
+- Produces:
+ - The shadcn variables (`--background`, `--card`, …) resolve to Sky wherever `[data-app-theme="sky"]` is mounted.
+ - New tokens: `--good`, `--bad`, `--accent-soft`, `--band`.
+ - Tailwind utilities `bg-good`, `text-good`, `bg-bad`, `text-bad`, `bg-accent-soft`, `font-serif` (Newsreader), `font-label` (IBM Plex Mono), plus a `.label-mono` utility class.
+ - Later phases use only these names.
+
+- [ ] **Step 1: Write the failing tests**
+
+```ts
+// helper in foundations.spec.ts: any CSS colour -> [r,g,b] via canvas (works for oklch)
+async function rgb(page, css: string) {
+ return page.evaluate((c) => { const x = document.createElement("canvas").getContext("2d")!;
+ x.fillStyle = c; x.fillRect(0, 0, 1, 1); return Array.from(x.getImageData(0, 0, 1, 1).data.slice(0, 3)); }, css);
+}
+const ratio = (a: number[], b: number[]) => { const L = (c: number[]) => { const f = (v: number) => { v /= 255;
+ return v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4; }; return 0.2126*f(c[0])+0.7152*f(c[1])+0.0722*f(c[2]); };
+ const [h, l] = [L(a), L(b)].sort((x, y) => y - x); return (h + 0.05) / (l + 0.05); };
+
+test("sky tokens apply to page", async ({ page }) => {
+ await page.goto("/dev/layout-shift");
+ const bg = await page.evaluate(() => getComputedStyle(document.body).backgroundColor);
+ expect(await rgb(page, bg)).toEqual([244, 243, 239]);
+ expect(await page.evaluate(() => getComputedStyle(document.documentElement).colorScheme)).toBe("light");
+});
+test("portaled menu is sky", async ({ page }) => {
+ await page.goto("/dev/layout-shift");
+ await page.getByTestId("open-dropdown").click();
+ const bg = await page.locator('[role="menu"]').evaluate((e) => getComputedStyle(e).backgroundColor);
+ expect(await rgb(page, bg)).toEqual([255, 255, 255]);
+});
+test("muted text meets AA", async ({ page }) => {
+ await page.goto("/dev/layout-shift");
+ const v = await page.evaluate(() => { const s = getComputedStyle(document.documentElement);
+ return [s.getPropertyValue("--muted-foreground"), s.getPropertyValue("--background"), s.getPropertyValue("--sidebar")]; });
+ const [m, bg, side] = await Promise.all(v.map((c) => rgb(page, c)));
+ expect(ratio(m, bg)).toBeGreaterThanOrEqual(4.5);
+ expect(ratio(m, side)).toBeGreaterThanOrEqual(4.5);
+});
+test("login stays dark", async ({ page }) => {
+ await page.goto("/login");
+ const [r] = await rgb(page, await page.evaluate(() => getComputedStyle(document.body).backgroundColor));
+ expect(r).toBeLessThan(60);
+});
+test("type roles load", async ({ page }) => {
+ await page.goto("/dev/layout-shift");
+ expect(await page.evaluate(() => getComputedStyle(document.body).fontFamily)).toMatch(/Funnel/i);
+});
+```
+
+- [ ] **Step 2: Run them and confirm they fail**
+
+ Run `npm run test:e2e -- -g "sky|portaled|muted|login|type roles"`. The expected result is FAIL on `sky tokens`, `portaled`, `muted` and `type roles`. `login stays dark` should already pass.
+
+- [ ] **Step 3: Add the Sky block to `globals.css`, after the `.dark {}` block**
+
+ Use the selector `:root:has([data-app-theme="sky"])` and add `color-scheme: light !important;`. The `!important` is needed to beat the inline `style={{colorScheme:"dark"}}` on ``. Set these values (from the spec; `--muted-foreground` is darkened for contrast, see Review Focus 3):
+
+| Variable | Value |
+|---|---|
+| `--background` | `oklch(0.964 0.005 95)` (#f4f3ef) |
+| `--foreground`, `--card-foreground`, `--popover-foreground`, `--secondary-foreground`, `--accent-foreground`, `--sidebar-foreground`, `--sidebar-accent-foreground` | `oklch(0.222 0.002 107)` (#1b1b1a) |
+| `--card`, `--popover`, `--primary-foreground`, `--sidebar-primary-foreground` | `oklch(1 0 0)` |
+| `--primary`, `--ring`, `--sidebar-primary`, `--sidebar-ring`, `--chart-1` | `oklch(0.564 0.185 261)` (#2f6fe0) |
+| `--secondary`, `--muted`, `--accent`, `--input`, `--sidebar-accent` | `oklch(0.939 0.007 97)` (#ecebe6) |
+| `--muted-foreground` | `#6b6963` |
+| `--border`, `--sidebar-border` | `oklch(0.919 0.008 91)` (#e6e4de) |
+| `--sidebar` | `oklch(0.948 0.007 97)` (#efeee9) |
+| `--accent-soft` | `oklch(0.932 0.027 262)` (#dfe9fb) |
+| `--good` | `oklch(0.569 0.119 155)` (#2e8b57) |
+| `--bad`, `--destructive` | `oklch(0.588 0.150 40)` (#c4572f) |
+| `--band` | `linear-gradient(180deg, #cfe2fb 0%, #e9f1fb 55%, #f4f3ef 100%)` |
+
+ Leave `--chart-2` to `--chart-5` as they are. In the same block, set `font-family: var(--font-ui), ui-sans-serif, system-ui, sans-serif` on `body`. The `@layer base` body gradient is dark-tuned, so set `background-image: none` on the body for Sky.
+
+- [ ] **Step 4: Add the theme vars and the label utility**
+
+ In `@theme inline`, add:
+ - `--color-good: var(--good)`
+ - `--color-bad: var(--bad)`
+ - `--color-accent-soft: var(--accent-soft)`
+ - `--font-serif: var(--font-newsreader), Georgia, serif`
+ - `--font-label: var(--font-plex-mono), ui-monospace, monospace`
+
+ In `@layer utilities`, add `.label-mono { font-family: var(--font-plex-mono), ui-monospace, monospace; font-size: 10.5px; letter-spacing: 0.12em; text-transform: uppercase; font-weight: 500; }`.
+
+- [ ] **Step 5: Load IBM Plex Mono in `src/app/layout.tsx`**
+
+ Call `IBM_Plex_Mono({ subsets: ["latin"], weight: ["400", "500"], variable: "--font-plex-mono" })` and add its `.variable` to the `` className. Add `"500"` to Newsreader's weights, because the brief and headlines use 400 and 500.
+
+- [ ] **Step 6: Mark the app shell in `src/app/(app)/layout.tsx`**
+
+ Wrap the `` subtree in `
`. The `contents` class keeps it out of the layout.
+
+- [ ] **Step 7: Run the tests and confirm they pass**
+
+ Run `npm run test:e2e`. Everything should pass in both browsers. Then look at `/dashboard` in a demo session. Old dark-tuned components will look rough on light, and that is expected because Phases 1 to 5 replace them. But nothing may be unreadable: no white-on-white text. List any spots you find in the PR description.
+
+- [ ] **Step 8: Commit**
+
+```bash
+git add frontend/src/app/globals.css frontend/src/app/layout.tsx "frontend/src/app/(app)/layout.tsx" frontend/e2e/foundations.spec.ts
+git commit -m "feat: Sky light theme scoped to the app shell, label/serif type roles"
+```
+
+---
+
+### Task 3: Motion foundation (one library, shared timings, reduced motion)
+
+**Files:**
+- Create: `frontend/src/lib/motion.ts`
+- Modify: the 7 files in `frontend/src/components/ai-analytics/` that import `framer-motion`; `frontend/src/app/(app)/layout.tsx`; `frontend/src/app/globals.css`; `frontend/package.json`
+- Test: `frontend/e2e/foundations.spec.ts`
+
+**Interfaces:**
+- Produces, in `@/lib/motion`:
+ - `export const DURATION = { fast: 0.15, base: 0.2, slow: 0.25 } as const` (seconds, motion-style)
+ - `export const EASE_OUT = [0.22, 1, 0.36, 1] as const`
+ - `export const fadeUp = { initial: { opacity: 0, y: 6 }, animate: { opacity: 1, y: 0 }, transition: { duration: DURATION.base, ease: EASE_OUT } }`
+ - Later phases import only these.
+
+- [ ] **Step 1: Write the failing test**
+
+```ts
+test("reduced motion kills transitions", async ({ browser }) => {
+ const ctx = await browser.newContext({ reducedMotion: "reduce" });
+ const page = await ctx.newPage();
+ await page.goto("/dev/layout-shift");
+ const d = await page.getByTestId("transition-probe").evaluate((e) => parseFloat(getComputedStyle(e).transitionDuration));
+ expect(d).toBeLessThan(0.01);
+ await ctx.close();
+});
+```
+
+ Playwright's `browser` fixture has no `baseURL` in a new context. Pass `baseURL` to `newContext` from `test.info().project.use.baseURL`.
+
+- [ ] **Step 2: Run it and confirm it fails**
+
+ Run `npm run test:e2e -- -g "reduced motion"`. The expected result is FAIL, with a duration of 0.2.
+
+- [ ] **Step 3: Add the global reduced-motion rule to `globals.css`**
+
+```css
+@media (prefers-reduced-motion: reduce) {
+ *, *::before, *::after { animation-duration: .01ms !important; animation-iteration-count: 1 !important;
+ transition-duration: .01ms !important; scroll-behavior: auto !important; }
+}
+```
+
+- [ ] **Step 4: Create `src/lib/motion.ts` with the Interfaces exports**
+
+ In `(app)/layout.tsx`, wrap the shell children in ``, imported from `motion/react`.
+
+- [ ] **Step 5: Swap the library**
+
+ Replace `from "framer-motion"` with `from "motion/react"` in the 7 `ai-analytics` files. The API is the same. Then run `npm uninstall framer-motion`.
+
+- [ ] **Step 6: Verify**
+
+ Run each of these:
+ - `npm run test:e2e`: all tests pass.
+ - `npx tsc --noEmit`: no errors.
+ - `npm run lint`: clean.
+ - `npm run build`: succeeds, and `/dev/layout-shift` returns 404 in `npm start`.
+ - `grep -r "framer-motion" src package.json`: no output.
+
+- [ ] **Step 7: Update `docs/CODEBASE-GUIDE.md`, then commit**
+
+ Add three things to the guide:
+ - How the theme scope works: `data-app-theme="sky"` and `:root:has(...)`.
+ - The harness route and `npm run test:e2e`.
+ - The `@/lib/motion` constants.
+
+```bash
+git add frontend docs/CODEBASE-GUIDE.md
+git commit -m "feat: single motion library, shared timings, reduced-motion support"
+```
+
+---
+
+### Task 4: Ship Phase 0
+
+- [ ] **Step 1:** Push the branch, for example `ui-revamp/phase0-foundations` cut from `refactor`. Open a PR into `refactor` with the spec, this plan and the mockup files. In the description, list the tests and their output, plus the before and after of the manual profile-menu check.
+- [ ] **Step 2:** Add a STAR entry to `docs/direction/STORY.md`. Situation: overlays shifted content app-wide. Result: 10/10 overlay shift checks pass in two browsers.
+
+---
+
+## Later phases (each gets its own plan when we reach it)
+
+| Phase | Plan file (to write) | Depends on |
+|---|---|---|
+| 1 App shell: sidebar IA, icon-rail collapse + Ctrl/⌘B, avatar menu, redirects | `…-phase1-shell.md` | Phase 0 tokens and type |
+| 2 Home UI from the mockup, empty states for new cards | `…-phase2-home-ui.md` | Phase 1 shell |
+| 3 Home backend: summary + "Lumen found", activity, budget + `set_budget` proposals | `…-phase3-home-backend.md` | Phase 2 card contracts |
+| 4 Ask Lumen: chat threads (migration), fresh chat + history, handoff, "Next:" fix | `…-phase4-ask.md` | Phase 1 shell |
+| 5 Inner pages, with a mockup round each | per page | Phases 1–2 |
+| 6 Performance with measured before/after | `…-phase6-perf.md` | all |
diff --git a/docs/superpowers/specs/2026-10-20-ui-revamp-design.md b/docs/superpowers/specs/2026-10-20-ui-revamp-design.md
new file mode 100644
index 0000000..7b9de5b
--- /dev/null
+++ b/docs/superpowers/specs/2026-10-20-ui-revamp-design.md
@@ -0,0 +1,205 @@
+# Lumen inner-app UI/UX revamp: design
+
+Status: **DRAFT for owner review** · 2026-10-20 · supersedes the "no new design system" line in
+`docs/direction/specs/SPEC-UX.md` (its evidence-first chat and review-queue goals still stand).
+Reference mockup: [`2026-10-20-ui-revamp/home-mockup.html`](2026-10-20-ui-revamp/home-mockup.html)
+(screenshot: `home-mockup.png`). Use the **Sky** variant; the Ink variant is rejected.
+
+## 1. Why
+
+The signed-in app still carries its November 2025 skeleton: a stock shadcn sidebar ("Financial command
+center", "Workspace", "Compare"), tabs that no longer match what the app does (Analytics and Spending Views
+open the same page, "Review (test)", a separate Import page), and an Overview that looks like every
+AI-generated dashboard. The owner wants a full rethink (information architecture, layout, palette, motion),
+not a re-skin. Inspiration: Origin Financial's app (useorigin.com): dense, warm, every card says something
+specific, serif + mono-label typography, a sky motif that our landing page already uses.
+
+Success = opening Lumen feels like a calm, specific briefing on *your* money, not a template; nothing on screen
+moves unless you asked it to; and it stays light on the free Vercel tier.
+
+## 2. Goals / non-goals / anti-goals
+
+**Goals**
+- New information architecture and sidebar (section 3).
+- A new Home built from the mockup (section 4).
+- A design system: Sky palette, type scale, spacing, motion rules (sections 5–6), applied to every inner page.
+- Ask Lumen touch-up: fresh chat on open + history list; Home Ask bar hands off smoothly (section 7).
+- A **zero layout-shift rule** enforced app-wide, starting with the known bug (section 6.3).
+- Frontend performance pass (section 9).
+
+**Non-goals**
+- Bank/card linking, net worth, credit score, investments, financial-planner booking, paywalls. Every number
+ comes from imported statements, receipts and invoices already in Lumen.
+- Landing page, login and onboarding (already redesigned; only re-tint to share tokens if trivial).
+- The BYOK feature itself (another session builds it). This revamp only gives it a home: the avatar menu.
+
+**Anti-goals** (fail even if it works)
+- Generic KPI-card grid, gradient-blob "AI dashboard" look.
+- Any click on one region visibly nudging another region (the profile-menu bug class).
+- Invented numbers or scores. A card with no real data shows an honest empty state, not a placeholder figure.
+- Bouncy or slow animation; anything that ignores `prefers-reduced-motion`.
+
+## 3. Information architecture (owner-decided)
+
+```
+Lumen
+ Home
+TRACK
+ Spending ← merges /analytics (+weekly/monthly/yearly tabs) and /ai-analytics charts
+ Bills & subscriptions ← recurring charges, payment calendar, reminders
+PLAN
+ Budget (NEW)
+ Forecast (later; shown only once built)
+───────────
+ Receipts & statements 3 ← merges /documents, /import, /review; badge = items needing review
+ Ask Lumen ⌘K
+```
+
+- Group labels describe what the user does (owner rule: never name groups after backend concepts like
+ "AI" or "Data"). **Insights is not a tab**: its content becomes Home's "Lumen found" cards and the Spending
+ page.
+- **Profile, settings, API keys (BYOK), theme, log out → top-right avatar menu.** The sidebar is navigation
+ only.
+- **Collapse:** icon rail (~56px) with tooltips, toggled by a button and Ctrl/⌘+B, remembered across visits
+ (the existing shadcn sidebar cookie is fine). The content area animates width; content must not jump.
+- Mobile (<768px): the sidebar becomes a sheet (existing shadcn behaviour). Builder's call on adding a
+ bottom tab bar.
+- Old URLs (`/dashboard`, `/analytics`, `/ai-analytics`, `/chatbot`, `/documents`, `/import`, `/review`,
+ `/upload`) redirect to their new homes so bookmarks and the demo keep working. New route names are the
+ builder's call (suggested: `/home`, `/spending`, `/bills`, `/budget`, `/receipts`, `/ask`).
+ Until Phase 5 redesigns them, the new URLs render the existing page content (Spending = analytics +
+ ai-analytics stacked; Receipts = documents + import + review as tabs). Budget's nav item appears only once
+ its page exists (my call).
+
+## 4. Home (owner-approved content)
+
+Layout per the mockup: greeting row (left) + icon buttons and avatar (right); sub-tabs
+**Overview · This month · Year**; a one- or two-sentence brief in Newsreader; then a two-column grid
+(main ~1fr, side ~330px) over a sky-tinted header band that fades into the page.
+
+| Card | Content | Data |
+|---|---|---|
+| Brief | e.g. "October is running 9% lighter than September. The one thing worth a look: food delivery is up ₹2,140, and Netflix renews on Friday." | **Rule-based, no LLM per page load.** New backend: a summary endpoint picks the top fact or two from month-to-date vs last month, the biggest category change, and the next bill. |
+| Spent this month | Cumulative spend line for this month vs last month (dashed) plus a dotted forecast to month-end, with bill pins on the future part. Range chips 1W/1M/3M/YTD. | Existing `/api/analytics/dashboard` + `/forecast`; recurring bills for the pins. |
+| Lumen found | "N things worth ₹X this month", then up to 3 action cards: mono TAG, serif number-led headline, one line of why, a link. Kinds: unused/new subscriptions, category spikes, receipts needing review, duplicate charges. | New aggregation endpoint over the existing anomalies, insights and recurring detection and the review queue. ₹ "worth" only where a real saving exists; otherwise the headline carries no amount. |
+| Spending calendar + Recent transactions | Month heat-grid (darker = more spent, amount in cell) beside the last 5 transactions. | Existing invoice/transaction data. |
+| Budget **(NEW)** | "₹spent of ₹budget", progress bar, "₹left · N days · ~₹/day". Empty state → the draft-budget card. | New budget model + API (section 8). |
+| Let Lumen draft your budget **(NEW)** | Promo card; drafts per-category limits from 3 months of spending; user approves each line before save (Lumen's propose-then-approve rule). | Reuses the agent proposal/approve flow. |
+| Upcoming bills | Next 3 recurring charges with date chips. | Existing recurring detection / reminders. |
+| Activity | Imports, receipts read, new subscriptions spotted, with relative times. | New lightweight feed from created_at timestamps; no new event table unless needed. |
+
+Deferred: the "+" ask-about-this button on every card (owner: maybe later). The Ask bar lives in the sidebar
+("Ask anything ⌘K"); typing there and pressing Enter routes to Ask Lumen with the question already
+running (section 7).
+
+## 5. Visual system: Sky (owner-decided)
+
+Tokens from the mockup, to be re-expressed as `oklch` CSS variables in `globals.css` (replacing the current
+blue-grey dark theme):
+
+| Token | Value | Use |
+|---|---|---|
+| bg | `#f4f3ef` | page (warm paper) |
+| side | `#efeee9` | sidebar |
+| card | `#ffffff` | cards |
+| line | `#e6e4de` | borders, dividers |
+| text / mute | `#1b1b1a` / `#7c7a74` | body / secondary |
+| accent / accentSoft | `#2f6fe0` / `#dfe9fb` | links, chart line, selection / promo cards |
+| good / bad | `#2e8b57` / `#c4572f` | deltas, bill pins |
+| band | `linear-gradient(#cfe2fb → #e9f1fb → bg)` | sky header band, Home only (builder may reuse on other page headers) |
+
+Light theme only for this revamp, scoped to the signed-in `(app)` route group; the root layout's forced
+`dark` stays for landing/login/onboarding (my call). No "theme" item in the avatar menu yet. Ink (dark) is out; a later dark mode is possible because everything goes
+through tokens.
+
+Type: **Funnel Sans** (UI, numbers), **Newsreader** (brief, card headlines, italics for emphasis),
+**IBM Plex Mono** small caps-style labels (10.5px, 0.12em tracking) for card titles, tags and axis ticks.
+Load fonts via `next/font` (self-hosted, no layout shift). Radius 14px for cards, 8px for controls.
+
+## 6. Interaction and motion (owner-decided: quiet and quick)
+
+### 6.1 Motion rules
+- 150–250ms, ease-out; opacity/transform only (never animate layout properties except the sidebar width).
+- Route change: content fades/slides 4–8px; the sidebar and header never re-animate.
+- Numbers count up once on first load, not on every revisit.
+- Ask handoff: the sidebar Ask input morphs into the chat composer (shared element / View Transitions API
+ where supported; plain fade fallback).
+- All motion off under `prefers-reduced-motion`.
+- Use **one** animation library (`motion`); drop the duplicate `framer-motion` import path.
+
+### 6.2 Feedback states
+Every async card has a skeleton matching its final size (no jump when data arrives), an empty state with a
+next step, and an inline error with retry. Abstaining is a designed state (from SPEC-UX).
+
+### 6.3 Zero layout-shift rule (owner requirement)
+**Known bug:** opening the profile dropdown shifts the chat column left. Root cause: Radix menus/dialogs are
+modal by default; `react-remove-scroll` hides the page scrollbar and adds compensating padding, so centered
+content moves. This affects every DropdownMenu, Dialog, Select and Popover.
+**Fix once, globally:** `html { scrollbar-gutter: stable; }` plus neutralising the scroll-lock padding
+(`body[data-scroll-locked] { margin-right: 0 !important; padding-right: 0 !important; }`), and/or
+`modal={false}` on non-blocking menus in the shared `ui/` wrappers.
+**Rule going forward:** an interaction in one region may not change the size or position of another region.
+Overlays float; reserved space for async content; images and fonts sized ahead of time. Verify with a
+Playwright check that opens each menu/dialog and asserts that main-content bounding boxes don't change (CLS = 0).
+Playwright is a dev-only dependency; the check runs locally against the dev server with demo data and is
+**not** a CI gate for now (my call; promote to CI once a seeded signed-in CI app exists).
+
+## 7. Ask Lumen touch-up (owner-decided)
+
+- Opening Ask Lumen starts a **fresh chat** with suggested questions (drawn from the current "Lumen found"
+ items); past chats are in a collapsible history list on the side; one click resumes a chat.
+- **Real threads (owner-decided):** add a conversation/thread id to `ChatMessage` (migration) plus endpoints
+ to list threads, create one, and load one. Existing flat history becomes a single "Earlier chats" thread.
+ This makes Phase 4 full-stack.
+- From the sidebar Ask bar or ⌘K: Enter → navigate to Ask with the question already sent, so the answer
+ appears as the page settles (Google AI-Overview feel). **No streaming in this revamp (owner-decided):**
+ show a polished thinking state (work-trace steps animating in), then fade in the full answer. Token
+ streaming (SSE) is a separate follow-up task.
+- Restyle to Sky; keep evidence-first behaviour (citations, work trace, proposal cards) from SPEC-UX.
+- Fix the stray "Next:" artefact visible under answers (seen in the 2026-10-20 screenshot).
+
+## 8. New backend this implies
+
+1. **Home summary** endpoint: the brief sentences + "Lumen found" items, rule-based, cached per user per day
+ or invalidated on import.
+2. **Budget**: per-user monthly budget with per-category limits (table + CRUD); "draft budget" as an agent
+ proposal the user approves.
+3. **Activity feed**: recent events from existing timestamps.
+4. **Chat threads** (section 7).
+
+Each ships with tests in the existing mocked-LLM suite style.
+
+Calls made while writing this (not owner-decided; owner may override):
+- New endpoints go in FastAPI `backend/api/` (the newer pattern, where agent proposals live), reading the
+ analytics helpers as functions rather than calling the Flask routes.
+- Draft budget = **one proposal per category** using a new `set_budget` proposal type, so the existing
+ approve/reject-per-proposal flow and card UI work unchanged. "Approve all" is a convenience button. Fits the AI-engineering story: the draft
+budget is tool-calling + human approval.
+
+## 9. Performance pass (last phase)
+
+- Remove unused heavy dependencies from the app bundle: `three`, `postprocessing`, `lenis` (check landing-page
+ use first; lazy-load them there if still needed), `mathjs` if unused, the duplicate `framer-motion`.
+- Code-split charts (`recharts`) with `next/dynamic`; server-render static shells where possible.
+- Measure before/after (bundle size per route, Lighthouse LCP/CLS/INP) and log the numbers as a STAR entry
+ in `docs/direction/STORY.md`.
+
+## 10. Phased path (one PR per phase into `refactor`)
+
+| # | Phase | Contents |
+|---|---|---|
+| 0 | Foundations | Sky tokens, fonts via next/font, global layout-shift fix + Playwright CLS check, motion rules |
+| 1 | App shell | New sidebar IA, icon-rail collapse, top-right avatar menu (settings / BYOK slot / log out), route redirects |
+| 2 | Home UI | Mockup → components with real existing data; budget/brief/activity cards render empty states |
+| 3 | Home backend | Summary + "Lumen found" endpoint, activity feed, budget model + draft-budget proposal; wire into Home |
+| 4 | Ask Lumen | Chat threads (migration + endpoints), fresh chat + history list, Ask-bar handoff with thinking state, restyle, "Next:" fix |
+| 5 | Inner pages | Spending, Bills & subscriptions, Budget, Receipts & statements: each gets a short mockup round with the owner before build |
+| 6 | Performance | Section 9, with measured before/after |
+
+Keep `docs/CODEBASE-GUIDE.md` current in each PR.
+
+## 11. Left to the builder
+- Exact route names, component file layout, and whether Home sub-tabs "This month" / "Year" ship in phase 2 or later.
+- Chart implementation (keep recharts or hand-rolled SVG like the mockup), as long as it matches the look and passes the perf budget.
+- Mobile layout details beyond "sidebar becomes a sheet".
+- Copy for empty states and the brief's rule templates.
diff --git a/docs/superpowers/specs/2026-10-20-ui-revamp/home-mockup.html b/docs/superpowers/specs/2026-10-20-ui-revamp/home-mockup.html
new file mode 100644
index 0000000..3253f8b
--- /dev/null
+++ b/docs/superpowers/specs/2026-10-20-ui-revamp/home-mockup.html
@@ -0,0 +1,214 @@
+
+
+
+
+
+
+
Home v2: Origin's density, Lumen's data
+
Same screen in two colour moods. Sky = light paper + blue, close to Origin's app and our landing page sky. Ink = warm near-black + amber. Click one to pick; the full-size version is below. Cards marked NEW are features we'd build (UI then backend). Every + on a card opens Ask Lumen with that card as context. No bank linking anywhere: all numbers come from imported statements and receipts.
+
+
+
+
A · Sky — light, airy, Origin-like
+
B · Ink — dark, warm, private-ledger feel
+
+
Full size
+
+
+
+
+
+
Good evening, Nishant
+
↑?⚙N
+
OverviewThis monthYear
+
October is running 9% lighter than September. The one thing worth a look: food delivery is up ₹2,140, and Netflix renews on Friday.
+
+
+
+
+
SPENT THIS MONTH ›+
+
₹18,412
↓ ₹1,830 vs Sep at this point
+
— Oct ┄ Sep ┄ forecast
+
+
1W1M3MYTD
+
+
+
+
LUMEN FOUND +
+
3 things worth ₹3,240 this month
+
+
SUBSCRIPTIONS
Drop 2 unused subscriptions, save ₹1,148
Hotstar and Audible charged since July with no other activity.
SEE WHY →
+
SPENDING
Food delivery is up 40%
₹7,480 on Swiggy + Zomato vs ₹5,340 last month. Weekends drive most of it.
BREAK IT DOWN →
+
RECEIPTS
3 receipts need a glance
Two look like duplicates of the Croma charge on 14 Oct.
REVIEW →
+
+
+
+
+
+
SPENDING CALENDAR ›
+
+
+
+
RECENT TRANSACTIONS ›+
+
SwiggyFood · 20 Oct₹486
+
UberTravel · 19 Oct₹312
+
AmazonShopping · 18 Oct₹1,299
+
Airtel PostpaidBills · 17 Oct₹599
+
Blue TokaiFood · 17 Oct₹340
+
+
+
+
+
+
+
BUDGET · OCTOBER NEW+
+
₹18,412 of ₹28,000
+
+
₹9,588 left · 11 days · ~₹870/day
+
+
+
UPCOMING BILLS ›+
+
FRI24Netflix₹649
+
TUE28Spotify₹119
+
SAT01Rent₹15,000
+
+
+
Let Lumen draft your budget NEW
+
Built from 3 months of your actual spending. You approve every line before it's saved.