diff --git a/.gitignore b/.gitignore
index 8b25bb5..10b99be 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,3 +11,4 @@ dist-ssr
.vinxi
__unconfig*
todos.json
+dudesky.db*
\ No newline at end of file
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..6861439
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,5 @@
+# Claude Instructions
+
+## Skill Mappings
+
+When working on TanStack-related tasks, read [AGENTS.md](AGENTS.md) and load the relevant SKILL.md files listed there before proceeding.
diff --git a/HANDOFF.md b/HANDOFF.md
new file mode 100644
index 0000000..11e15f9
--- /dev/null
+++ b/HANDOFF.md
@@ -0,0 +1,95 @@
+# Handoff: UI polish — feed, login, and shell
+
+## What this project is
+
+Dudesky is a Bluesky client built with TanStack Start (React, SSR, file-based routing). It uses ATProto OAuth to authenticate users via their Bluesky handle. Stack: TanStack Start + Vite, Tailwind CSS v4, `@atproto/oauth-client-node` for OAuth, `better-sqlite3` for persistence.
+
+## What was built last session
+
+### Database-backed OAuth stores (`src/lib/db.ts`, `src/lib/oauth-client.ts`)
+
+Replaced the in-memory `stateStore` / `sessionStore` with SQLite via `better-sqlite3`. DB file is `dudesky.db` in the project root (path overridable via `DB_PATH` env var). Two tables:
+
+- `oauth_state` — short-lived PKCE/DPoP state, expires after 10 minutes (TTL enforced on each `set`)
+- `oauth_session` — long-lived token store keyed by DID
+
+Both stores serialize ATProto state objects as JSON text.
+
+### DID cookie (`src/routes/callback.tsx`)
+
+After `client.callback()` succeeds, sets a `did` cookie via a mutable `new Response(null, { headers: { 'Set-Cookie': ..., 'Location': ... } })`. **Do not use `Response.redirect()` here** — it creates a response with immutable headers, which crashes TanStack Start's `mergeEventResponseHeaders` when it tries to attach queued cookies. The DID is not sensitive on its own; actual tokens live in `sessionStore`.
+
+### Feed loader (`src/routes/feed.tsx`)
+
+Uses `createServerFn` (not a raw loader) for all server-only work — `getCookie`, `client.restore()`, DB access via the OAuth client, and `agent.getTimeline()`. This is required because TanStack Start loaders are isomorphic (run on both client and server); `better-sqlite3` is Node-only and will crash in the browser if imported directly in a loader.
+
+The server function returns a mapped subset of the feed data (not raw ATProto types) to avoid a type incompatibility with `createServerFn`'s serialization constraints (`{ [x: string]: unknown }` vs `{ [x: string]: {} }`).
+
+## Current state of key files
+
+```
+src/lib/db.ts ← SQLite setup, creates oauth_state + oauth_session tables
+src/lib/oauth-client.ts ← NodeOAuthClient with DB-backed stateStore + sessionStore
+src/routes/callback.tsx ← sets DID cookie, redirects to /feed
+src/routes/feed.tsx ← createServerFn fetches timeline; basic card UI (needs polish)
+src/routes/login.tsx ← plain unstyled form; working, needs polish
+src/routes/__root.tsx ← shell with Header + Footer components, devtools, theme init script
+src/components/Header.tsx ← sticky nav with "Feed" link chip; uses design tokens
+src/components/Footer.tsx ← copyright + "Built with TanStack Start" line
+src/styles.css ← Tailwind v4 + custom design system (see below)
+```
+
+## What needs to be done: UI cleanup
+
+The auth flow and data loading all work end-to-end. The feed renders but looks rough. The next session is purely visual polish — no logic changes needed.
+
+### Design system already in place
+
+`src/styles.css` has a full set of CSS custom properties and utility classes to use:
+
+**Color tokens** (light + dark + `prefers-color-scheme` variants):
+- `--sea-ink` / `--sea-ink-soft` — primary text colors
+- `--lagoon` / `--lagoon-deep` — teal accent
+- `--palm` — green accent
+- `--sand` / `--foam` / `--bg-base` — background layers
+- `--surface` / `--surface-strong` — card/panel backgrounds
+- `--line` — borders
+- `--inset-glint` — inner highlight on cards
+- `--header-bg` / `--chip-bg` / `--chip-line` — header/chip-specific
+
+**Utility classes**:
+- `.page-wrap` — centered container, `min(1080px, calc(100% - 2rem))`
+- `.island-shell` — frosted glass card style (border + gradient bg + box-shadow + backdrop-blur)
+- `.island-kicker` — small-caps label style
+- `.nav-link` — link with animated underline
+- `.display-title` — Fraunces serif font
+- `.rise-in` — entrance animation (opacity + translateY, 700ms)
+- `.feature-card` — card with hover lift
+
+**Fonts**: Manrope (sans body), Fraunces (serif display)
+
+### Specific things to polish
+
+**`src/routes/feed.tsx` — the main job:**
+- Post cards are bare `border rounded-lg` — should use `.island-shell` and the design token colors
+- Avatar images have no fallback for missing avatars
+- No timestamp on posts (it's available: `(item.post.record as { createdAt?: string }).createdAt`)
+- The feed data shape currently mapped in `getTimeline` only extracts `uri`, `text`, `author.{handle,displayName,avatar}` — if you need `createdAt` or other fields, add them to the mapped return object in the server function
+- No loading/pending state (`pendingComponent` on the route)
+- No error state (`errorComponent` on the route)
+
+**`src/routes/login.tsx` — unstyled:**
+- The form (``, `