diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..bb1d8fc
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,53 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Project
+
+P2P Poll App — a peer-to-peer polling application for the Evocracy democratic coding research experiment (https://demcode.evocracy.org/).
+
+### Goal
+Build a simple P2P polling app where users can add poll options and vote. Data exchange works peer-to-peer between clients (no central server for data), though a signaling/sync server for connection establishment is acceptable.
+
+### Research Context
+- **Phase 1 (2 weeks):** Submit individual code proposal
+- **Phase 2:** Groups of 3 merge solutions into a working prototype
+- **Phase 3:** Representatives iteratively merge until one final solution remains
+- Code must be clean, explainable, and easy to merge with others' work
+- Final code published open-source under MIT license
+- Language: JavaScript/TypeScript
+
+## Tech Stack
+
+- **Runtime:** Bun
+- **Framework:** Waku (minimal React framework with RSC support)
+- **Styling:** Tailwind CSS v4
+- **P2P/Data sync:** Automerge with automerge-repo (CRDT-based)
+- **Networking:** `automerge-repo-network-websocket` (WebSocketClientAdapter) + `automerge-repo-network-broadcastchannel` (cross-tab sync)
+- **Storage:** `automerge-repo-storage-indexeddb` (client-side persistence)
+
+## Commands
+
+```bash
+bun install # Install dependencies
+bun run dev # Start dev server
+bun run build # Production build
+bun test # Run tests (Bun's built-in test runner)
+```
+
+## Architecture
+
+- Waku pages router: pages live in `src/pages/`, layouts in `_layout.tsx`
+- Client components use `'use client'` directive
+- Automerge Repo is initialized in a client-side provider component wrapping the app
+- The shared CRDT document holds the poll state (title, options, votes)
+- Peers sync via a lightweight WebSocket sync server (can use `automerge-repo` sync server or the public `wss://sync.automerge.org`)
+- `BroadcastChannelNetworkAdapter` enables cross-tab sync
+- `useDocument` hook from `@automerge/automerge-repo-react-hooks` for reactive document access
+- Every Waku page/layout must export `getConfig` specifying render mode (`'static'` or `'dynamic'`)
+
+## Key Design Principles
+
+- Keep it simple and merge-friendly for Phase 2 group work
+- Minimal file count, clear separation of concerns
+- No over-engineering — this is a research experiment, not production software
diff --git a/PLAN.md b/PLAN.md
new file mode 100644
index 0000000..07a8d7f
--- /dev/null
+++ b/PLAN.md
@@ -0,0 +1,267 @@
+# P2P Poll App — Implementation Plan
+
+A simple peer-to-peer polling app where users can add options and vote, built with no central server (beyond a public relay for WebSocket sync).
+
+---
+
+## 1. Project Setup
+
+```bash
+# Scaffold a Waku project with Bun
+bunx create-waku@latest . --template basic --package-manager bun
+
+# Install dependencies
+bun add @automerge/automerge @automerge/automerge-repo @automerge/automerge-repo-react-hooks @automerge/automerge-repo-network-websocket @automerge/automerge-repo-network-broadcastchannel @automerge/automerge-repo-storage-indexeddb
+
+# Tailwind CSS v4
+bun add tailwindcss @tailwindcss/vite
+```
+
+Waku uses Vite under the hood, so add `@tailwindcss/vite` to the Vite plugin list in `waku.config.ts` (see file structure below). Import Tailwind in a global CSS file with `@import "tailwindcss"`.
+
+---
+
+## 2. Data Model
+
+A single Automerge document represents one poll:
+
+```typescript
+interface Poll {
+ title: string;
+ options: PollOption[];
+}
+
+interface PollOption {
+ id: string; // nanoid or crypto.randomUUID()
+ text: string;
+ votes: string[]; // list of peer IDs who voted for this option
+}
+```
+
+- **votes as `string[]`** — storing peer IDs instead of a count lets us prevent double-voting and support vote retracting. The count is derived via `.length`.
+- Each peer generates a stable random ID on first visit (stored in `localStorage`).
+
+---
+
+## 3. File Structure
+
+```
+waku.config.ts — Waku + Vite config (Tailwind plugin)
+src/
+ pages/
+ _layout.tsx — root layout (server component), imports Providers + global CSS
+ index.tsx — landing page: create new poll or join via URL
+ poll/
+ [id].tsx — main poll view (resolved from Automerge document URL)
+ components/
+ Providers.tsx — 'use client': initializes Automerge Repo + RepoContext.Provider
+ PollView.tsx — displays poll title, options, votes; contains AddOption form and VoteButtons
+ ConnectionStatus.tsx — shows sync/connection state
+ lib/
+ repo.ts — creates and exports the Automerge Repo singleton
+ types.ts — Poll and PollOption type definitions
+ peer.ts — get-or-create local peer ID from localStorage
+ poll.ts — poll mutation logic (pure functions)
+ __tests__/
+ poll.test.ts — tests for poll logic
+ styles/
+ global.css — Tailwind import
+```
+
+**Total: ~12 files** (5 components/pages + 1 config + 4 lib modules + 1 test file + 1 CSS)
+
+---
+
+## 4. Implementation Steps
+
+### Step 1 — Scaffold and configure
+
+- Run the setup commands from Section 1.
+- Create `waku.config.ts`:
+ ```typescript
+ import { defineConfig } from 'waku/config';
+ import tailwindcss from '@tailwindcss/vite';
+
+ export default defineConfig({
+ vite: {
+ plugins: [tailwindcss()],
+ },
+ });
+ ```
+- Create `src/styles/global.css` with `@import "tailwindcss"`.
+- Verify `bun run dev` starts without errors.
+
+### Step 2 — Define types, peer identity, and poll logic
+
+- Create `src/lib/types.ts` with the `Poll` and `PollOption` interfaces.
+- Create `src/lib/peer.ts`: read `localStorage.getItem("peerId")`, or generate one with `crypto.randomUUID()` and persist it.
+- Create `src/lib/poll.ts`: extract poll mutation logic into pure functions (`createPoll`, `addOption`, `vote`, `unvote`, `hasVoted`) so they are testable independently of Automerge and React.
+
+### Step 3 — Initialize Automerge Repo
+
+- Create `src/lib/repo.ts`:
+ ```typescript
+ import { Repo } from "@automerge/automerge-repo";
+ import { WebSocketClientAdapter } from "@automerge/automerge-repo-network-websocket";
+ import { BroadcastChannelNetworkAdapter } from "@automerge/automerge-repo-network-broadcastchannel";
+ import { IndexedDBStorageAdapter } from "@automerge/automerge-repo-storage-indexeddb";
+
+ export const repo = new Repo({
+ network: [
+ new WebSocketClientAdapter("wss://sync.automerge.org"),
+ new BroadcastChannelNetworkAdapter(),
+ ],
+ storage: new IndexedDBStorageAdapter("p2p-poll"),
+ });
+ ```
+
+> **Sync server note:** `wss://sync.automerge.org` is a free public relay provided by the Automerge team. For production use or better reliability, you can run your own sync server using `@automerge/automerge-repo-network-websocket`'s server adapter with a simple Node/Bun WebSocket server.
+
+### Step 4 — Providers (client component) and root layout (server component)
+
+Waku layouts are **server components** — they cannot use browser APIs or React context directly. Instead, create a client component wrapper:
+
+- Create `src/components/Providers.tsx`:
+ ```typescript
+ 'use client';
+
+ import { RepoContext } from "@automerge/automerge-repo-react-hooks";
+ import { repo } from "../lib/repo";
+
+ export const Providers = ({ children }: { children: React.ReactNode }) => {
+ return (
+
+ {children}
+
+ );
+ };
+ ```
+
+- Create `src/pages/_layout.tsx` (server component — no `'use client'`):
+ ```typescript
+ import '../styles/global.css';
+ import { Providers } from '../components/Providers';
+
+ export default async function RootLayout({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+ }
+ ```
+
+### Step 5 — Landing page (create / join)
+
+- `src/pages/index.tsx` (`'use client'`):
+ - "Create Poll" button: calls `repo.create()` to make a new Automerge doc, initializes it with a default title and empty options array, then navigates to `/poll/`.
+ - "Join Poll" input: paste a document ID or full URL, navigate to `/poll/`.
+ - Show a list of recently visited polls from `localStorage` for convenience.
+
+### Step 6 — Poll view page
+
+- `src/pages/poll/[id].tsx` (`'use client'`):
+ - Read `id` from the route segment prop (Waku passes dynamic segments as props automatically).
+ - Reconstruct the Automerge `DocumentUrl` from `id`.
+ - Use `useDocument(url)` from `@automerge/automerge-repo-react-hooks` to get `[doc, changeDoc]`.
+ - Wrap the document-dependent UI in `Loading poll...}>` — `useDocument` may suspend while the document is being fetched/synced.
+ - Render `` and ``.
+
+### Step 7 — PollView component
+
+- `src/components/PollView.tsx` (`'use client'`):
+ - Display poll title (editable inline).
+ - List each option with its text, vote count (`option.votes.length`), and a Vote/Unvote button.
+ - Vote button logic:
+ ```typescript
+ changeDoc(d => {
+ const opt = d.options.find(o => o.id === optionId);
+ if (!opt.votes.includes(peerId)) {
+ opt.votes.push(peerId);
+ }
+ });
+ ```
+ - Unvote: filter out the peer ID.
+ - "Add Option" form at the bottom: text input + submit button that pushes a new `PollOption` into `doc.options`.
+ - Show a shareable link/document ID so users can invite peers.
+
+### Step 8 — ConnectionStatus component
+
+- `src/components/ConnectionStatus.tsx` (`'use client'`):
+ - Simple indicator (green dot / grey dot) based on whether the WebSocket adapter is connected.
+ - Can use `repo.networkSubsystem.on("peer", ...)` to track connected peers.
+
+### Step 9 — Polish and test
+
+- Style with Tailwind: center layout, card-style poll, colored vote bars.
+- Test with two browser tabs (BroadcastChannel sync).
+- Test with two devices on the same network pointing at the sync server.
+- Handle loading state (doc is `undefined` until synced — use `` or a null check).
+- Handle invalid/missing document IDs gracefully.
+
+---
+
+## 5. UI Components Summary
+
+| Component | Responsibility |
+|---|---|
+| `_layout.tsx` | Server-component root layout, imports Providers + global styles |
+| `Providers.tsx` | Client component: Repo initialization, RepoContext provider |
+| `index.tsx` | Create new poll, join existing poll by ID |
+| `[id].tsx` | Load Automerge doc by URL, wrap in Suspense, pass to PollView |
+| `PollView.tsx` | Render poll title, options list, vote buttons, add-option form, share link |
+| `ConnectionStatus.tsx` | Show online/offline and peer count |
+
+---
+
+## 6. P2P Setup Details
+
+**How peers connect:**
+
+1. Creator makes a new poll — Automerge generates a unique document URL (e.g., `automerge:2Xp9Q3...`).
+2. The app extracts the document ID and puts it in the page URL: `https://localhost:3000/poll/2Xp9Q3...`.
+3. Creator shares that URL with others (copy button in UI).
+4. When another peer opens the URL, the app reconstructs the Automerge document URL and calls `useDocument(url)` — Automerge syncs the doc via the WebSocket relay automatically.
+5. Cross-tab sync happens instantly via BroadcastChannel (no network needed).
+6. IndexedDB ensures the poll persists across page reloads.
+
+**No server to deploy** — the public `wss://sync.automerge.org` relay handles document sync. For production, consider running your own sync server for reliability and control.
+
+---
+
+## 8. Tests
+
+Tests use **Bun's built-in test runner** (`bun test`) — no extra test dependencies needed. Bun provides `describe`, `it`, and `expect` out of the box.
+
+Tests target the pure functions in `src/lib/poll.ts`, not React components. Test file: `src/lib/__tests__/poll.test.ts`.
+
+**Pure functions under test:**
+
+- `createPoll(title: string): Poll`
+- `addOption(poll: Poll, text: string): Poll`
+- `vote(poll: Poll, optionId: string, peerId: string): Poll`
+- `unvote(poll: Poll, optionId: string, peerId: string): Poll`
+- `hasVoted(poll: Poll, optionId: string, peerId: string): boolean`
+
+**Test cases (6 total):**
+
+- **Peer ID management** — `getPeerId()` generates a UUID and returns the same value on subsequent calls (persisted in localStorage)
+- **Poll creation** — `createPoll("Lunch?")` returns a poll with the given title and an empty options array
+- **Adding an option** — `addOption(poll, "Pizza")` appends an option with the given text, a generated ID, and an empty votes array
+- **Voting** — `vote(poll, optionId, peerId)` adds the peer ID to that option's votes array
+- **Double-vote prevention** — calling `vote` twice with the same peer ID and option does not duplicate the entry
+- **Vote retracting** — `unvote(poll, optionId, peerId)` removes the peer ID from the votes array
+
+> Run with: `bun test`
+
+---
+
+## 9. Nice-to-Haves (if time permits)
+
+- **Poll title editing** — inline editable title field (trivial with `changeDoc`).
+- **Vote retracting** — toggle vote on/off (already supported by the data model).
+- **Peer count display** — show how many peers are currently connected to this document.
+- **Option reordering** — drag to reorder or sort by vote count.
+- **Results visualization** — simple bar chart showing vote distribution using Tailwind width utilities.
+- **Multiple polls** — list/manage multiple polls from the landing page.
+- **Poll closing** — creator can lock a poll to prevent further votes.
diff --git a/TASKS.md b/TASKS.md
new file mode 100644
index 0000000..ce702d5
--- /dev/null
+++ b/TASKS.md
@@ -0,0 +1,49 @@
+# TASKS.md
+
+Implementation progress tracker for the P2P Poll App.
+
+## Setup
+- [ ] Scaffold Waku project with `bunx create-waku@latest`
+- [ ] Install Automerge dependencies
+- [ ] Install Tailwind CSS v4 and `@tailwindcss/vite`
+- [ ] Create `waku.config.ts` with Tailwind Vite plugin
+- [ ] Create `src/styles/global.css` with Tailwind import
+- [ ] Verify `bun run dev` starts without errors
+
+## Core Logic
+- [ ] Create `src/lib/types.ts` with Poll and PollOption interfaces
+- [ ] Create `src/lib/peer.ts` with localStorage-backed peer ID generation
+- [ ] Implement `createPoll` in `src/lib/poll.ts`
+- [ ] Implement `addOption` in `src/lib/poll.ts`
+- [ ] Implement `vote` and `unvote` in `src/lib/poll.ts`
+- [ ] Implement `hasVoted` in `src/lib/poll.ts`
+
+## Automerge Integration
+- [ ] Create `src/lib/repo.ts` with Repo singleton (WebSocket, BroadcastChannel, IndexedDB)
+- [ ] Create `src/components/Providers.tsx` with RepoContext.Provider
+
+## Pages & Routing
+- [ ] Create `src/pages/_layout.tsx` (server component with Providers and global CSS)
+- [ ] Create `src/pages/index.tsx` with create-poll and join-poll UI
+- [ ] Create `src/pages/poll/[id].tsx` with useDocument and Suspense
+
+## UI Components
+- [ ] Build `src/components/PollView.tsx` (title, options list, vote/unvote buttons)
+- [ ] Add "Add Option" form to PollView
+- [ ] Add shareable link / copy button to PollView
+- [ ] Build `src/components/ConnectionStatus.tsx`
+
+## Tests
+- [ ] Test: getPeerId generates and persists a UUID
+- [ ] Test: createPoll returns poll with title and empty options
+- [ ] Test: addOption appends option with text, id, and empty votes
+- [ ] Test: vote adds peer ID to option's votes array
+- [ ] Test: double-vote prevention (no duplicate entries)
+- [ ] Test: unvote removes peer ID from votes array
+
+## Polish
+- [ ] Tailwind styling (centered layout, card-style poll, vote bars)
+- [ ] Loading states for document sync
+- [ ] Error handling for invalid/missing document IDs
+- [ ] Cross-tab sync testing (BroadcastChannel)
+- [ ] Multi-device sync testing