diff --git a/README.md b/README.md
index 0217c70..a2a0d5e 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,13 @@
-# P2P Poll App
\ No newline at end of file
+# P2P Poll App
+
+A decentralized polling app built with SvelteKit + PeerJS. Mobile-first, peer-to-peer.
+
+## UI Guidelines
+
+All UI code must follow mobile-first principles:
+
+- **Touch targets**: β₯ 44px
+- **Layout**: Single-column on mobile, expand on larger screens
+- **Navigation**: Bottom tab bar (Home, Create, Profile)
+- **Dark mode**: Respect `prefers-color-scheme`
+- **Tailwind**: Mobile breakpoints are the default; use `md:` / `lg:` to scale up
\ No newline at end of file
diff --git a/specs/001-project-setup/README.md b/specs/001-project-setup/README.md
new file mode 100644
index 0000000..939300c
--- /dev/null
+++ b/specs/001-project-setup/README.md
@@ -0,0 +1,72 @@
+---
+status: planned
+created: '2026-03-16'
+tags:
+ - infra
+ - setup
+priority: high
+created_at: '2026-03-16T07:51:47.401Z'
+---
+
+# Project Setup & Architecture
+
+> **Status**: π
Planned Β· **Priority**: High Β· **Created**: 2026-03-16
+
+## Overview
+
+Scaffold the SvelteKit project with PeerJS for WebRTC-based P2P communication. This is the foundation all other specs build on. The app is a decentralized polling toolβno central backend for poll data.
+
+## Design
+
+- **Framework**: SvelteKit (SSR for public sharing pages, SPA for the app itself)
+- **P2P**: PeerJS (WebRTC data channels for peer communication)
+- **Signaling**: PeerJS Cloud (free tier to start; can self-host later)
+- **State**: Svelte stores + IndexedDB (via `idb-keyval` or similar) for local persistence
+- **Styling**: Tailwind CSS (mobile-first utility classes)
+- **Build/Deploy**: Vercel or Cloudflare Pages (static adapter for SPA routes, SSR for public pages)
+
+### Mobile-First UI Principles (applies to ALL UI work)
+
+- Touch targets β₯ 44px
+- Single-column layout on mobile; expand on larger screens
+- Bottom navigation bar (thumb-friendly): Home, Create (+), Profile
+- Minimal chromeβcontent first
+- Dark mode support (`prefers-color-scheme`)
+- System font stack for performance
+- Tailwind mobile breakpoints as default (design small β scale up)
+
+### Architecture
+
+```
+βββββββββββββββββββββββββββββββββββββββ
+β SvelteKit App (runs in browser) β
+β βββ Svelte Stores (reactive state) β
+β βββ IndexedDB (persistence) β
+β βββ PeerJS (WebRTC data channels) β
+β βββ Crypto (identity keypairs) β
+ββββββββββββ¬βββββββββββββββββββββββββββ
+ β WebRTC
+ βββββββ΄ββββββ
+ β Other Peersβ
+ βββββββββββββ
+```
+
+## Plan
+
+- [ ] Init SvelteKit project with TypeScript
+- [ ] Install dependencies: peerjs, tailwindcss, idb-keyval
+- [ ] Set up Tailwind with mobile-first config
+- [ ] Create basic app layout (shell, navigation)
+- [ ] Set up IndexedDB persistence layer
+- [ ] Configure SvelteKit adapter (adapter-auto for Vercel/CF, SSR mode)
+
+## Test
+
+- [ ] `npm run dev` starts without errors
+- [ ] Tailwind classes render correctly
+- [ ] IndexedDB read/write works in browser
+
+## Notes
+
+- PeerJS Cloud has rate limitsβfine for development, may need self-hosted signaling for production
+- No server-side database; all poll data lives on peers' devices
diff --git a/specs/002-p2p-networking/README.md b/specs/002-p2p-networking/README.md
new file mode 100644
index 0000000..39dd04f
--- /dev/null
+++ b/specs/002-p2p-networking/README.md
@@ -0,0 +1,81 @@
+---
+status: planned
+created: '2026-03-16'
+tags:
+ - p2p
+ - core
+priority: high
+created_at: '2026-03-16T07:51:47.888Z'
+depends_on:
+ - 001-project-setup
+updated_at: '2026-03-16T07:52:03.104Z'
+---
+
+# P2P Networking Layer
+
+> **Status**: ποΈ Planned Β· **Priority**: High Β· **Created**: 2026-03-16 Β· **Tags**: p2p, core
+
+## Overview
+
+Build the P2P networking layer using PeerJS. This handles connection lifecycle, message passing, and data synchronization between peers. Every poll operates as a "room" where the owner's peer ID is the room identifier.
+
+## Design
+
+- **Connection model**: Star topology per pollβowner acts as relay hub; participants connect to owner
+- **Peer ID**: Derived from user's public key (deterministic, resumable)
+- **Messages**: JSON-based protocol over PeerJS data channels
+- **Reconnection**: Auto-reconnect with exponential backoff
+- **Sync**: On connect, owner sends full poll state snapshot; subsequent changes are incremental messages
+
+### Message Protocol
+
+```typescript
+type Message =
+ | { type: 'poll:state'; payload: Poll }
+ | { type: 'poll:vote'; payload: Vote }
+ | { type: 'poll:option:add'; payload: Option }
+ | { type: 'poll:role:update'; payload: RoleUpdate }
+ | { type: 'user:profile'; payload: UserProfile }
+ | { type: 'peer:discovery'; payload: PeerInfo[] }
+ | { type: 'ack'; payload: { commandId: string; revision: number } }
+ | { type: 'error'; payload: { commandId: string; message: string } }
+ | { type: 'sync:request'; payload: { pollId: string } }
+```
+
+### Offline Behavior
+
+When the poll owner is offline, the poll is unreachable. Participants can view their local cached copy but cannot submit new votes until the owner reconnects.
+
+### Outbox & Acknowledgment
+
+- Every mutation message includes a `commandId` (UUID) for deduplication
+- Sender persists the command in a local IndexedDB outbox before sending
+- Owner responds with `ack { commandId, revision }` on success or `error { commandId, message }` on failure
+- On reconnect, client resends any unacked commands from the outbox
+- Owner deduplicates by `commandId`
+- UI shows "Pending sync" indicator for unacked mutations
+
+## Plan
+
+- [ ] Create PeerJS service (singleton, manages connection lifecycle)
+- [ ] Implement message send/receive with typed protocol
+- [ ] Add connection state management (connecting, connected, disconnected)
+- [ ] Implement auto-reconnect with backoff
+- [ ] Add "room" conceptβjoin a poll by connecting to owner's peer ID
+- [ ] Handle peer disconnect/cleanup
+- [ ] Implement outbox (IndexedDB-backed pending command queue)
+- [ ] Implement ack/error response handling
+- [ ] Implement resend-on-reconnect for unacked commands
+
+## Test
+
+- [ ] Two browser tabs can establish a PeerJS connection
+- [ ] Messages round-trip correctly
+- [ ] Reconnection works after simulated disconnect
+- [ ] Unacked commands are resent after reconnect
+- [ ] Owner deduplicates commands by commandId
+
+## Notes
+
+- Star topology keeps it simpleβowner must be online for live interaction
+- Could explore gossip/mesh topology later for resilience, but adds complexity
diff --git a/specs/003-user-identity-profiles/README.md b/specs/003-user-identity-profiles/README.md
new file mode 100644
index 0000000..7b662ee
--- /dev/null
+++ b/specs/003-user-identity-profiles/README.md
@@ -0,0 +1,65 @@
+---
+status: planned
+created: '2026-03-16'
+tags:
+ - identity
+ - profiles
+priority: high
+created_at: '2026-03-16T07:51:48.340Z'
+depends_on:
+ - 001-project-setup
+updated_at: '2026-03-16T07:52:03.509Z'
+---
+
+# User Identity & Profiles
+
+> **Status**: ποΈ Planned Β· **Priority**: High Β· **Created**: 2026-03-16 Β· **Tags**: identity, profiles
+
+## Overview
+
+Users need a persistent identity without a centralized account system. Each user generates a cryptographic keypair locally. Their public key serves as their unique ID. Users can edit their profile (name, bio, tags).
+
+## Design
+
+- **Identity**: Ed25519 keypair generated via Web Crypto API, stored in IndexedDB
+- **User ID**: Base58-encoded public key (short, URL-safe)
+- **Profile fields**: `name` (display name), `bio` (short text), `tags` (array of categorized tags)
+- **Tags structure**: `{ category: string, value: string }` β e.g. `{ category: "location", value: "Berlin" }`, `{ category: "expertise", value: "UX Design" }`
+- **Tag categories**: location, interest, expertise (extensible)
+- **Signing**: Profile updates are signed with private key (used for server writes; P2P messages use connection identity)
+- **Storage**: Profile stored locally in IndexedDB; shared with peers on connect
+- **Discovery**: Deferred. Tags are stored locally and exchanged with peers, but there is no directory server for searching users by tag yet. When added, profiles will already have the right structure (see archived spec 007)
+
+### Profile Schema
+
+```typescript
+interface UserProfile {
+ id: string; // base58(publicKey)
+ name: string;
+ bio: string;
+ tags: Tag[];
+ updatedAt: number; // timestamp
+ signature: string; // signed(hash(profile), privateKey)
+}
+
+interface Tag {
+ category: 'location' | 'interest' | 'expertise' | string;
+ value: string;
+}
+```
+
+## Plan
+
+- [ ] Implement keypair generation + storage in IndexedDB
+- [ ] Create profile store (Svelte store backed by IndexedDB)
+- [ ] Build profile edit page (name, bio, tag management)
+- [ ] Implement tag CRUD with category selector
+- [ ] Add profile signing/verification utilities
+- [ ] Profile exchange on peer connect
+
+## Test
+
+- [ ] Keypair persists across page reloads
+- [ ] Profile updates are saved and signed
+- [ ] Tags can be added/removed by category
+- [ ] Profile signature verification works
diff --git a/specs/004-poll-data-model/README.md b/specs/004-poll-data-model/README.md
new file mode 100644
index 0000000..3eaef95
--- /dev/null
+++ b/specs/004-poll-data-model/README.md
@@ -0,0 +1,120 @@
+---
+status: planned
+created: '2026-03-16'
+tags:
+ - data
+ - core
+priority: high
+created_at: '2026-03-16T07:51:48.793Z'
+depends_on:
+ - 002-p2p-networking
+ - 003-user-identity-profiles
+updated_at: '2026-03-16T07:52:03.943Z'
+---
+
+# Poll Data Model & Sync
+
+> **Status**: ποΈ Planned Β· **Priority**: High Β· **Created**: 2026-03-16 Β· **Tags**: data, core
+
+## Overview
+
+Define the core data structures for polls, options, votes, and their sync behavior across peers. Polls are owned by the creating user and stored on their device (and cached by participants).
+
+## Design
+
+### Poll Schema
+
+```typescript
+interface Poll {
+ id: string; // uuid
+ ownerId: string; // creator's public key
+ title: string;
+ description: string;
+ anonymous: boolean; // set at creation, immutable
+ status: 'draft' | 'open' | 'closed';
+ visibility: 'private' | 'link' | 'public'; // who can view via URL
+ createdAt: number;
+ closedAt?: number;
+ options: Option[];
+ votes: Vote[];
+ roles: RoleAssignment[];
+}
+
+interface Option {
+ id: string;
+ text: string;
+ addedBy: string; // user ID
+ addedAt: number;
+}
+
+interface Vote {
+ optionId: string;
+ voterId: string | null; // null if poll.anonymous === true
+ timestamp: number;
+ signature: string; // proves vote authenticity
+}
+
+interface RoleAssignment {
+ userId: string;
+ role: 'viewer' | 'participant' | 'moderator';
+}
+```
+
+### Anonymity
+
+- When `anonymous: true`, votes store `voterId: null`. The owner's device may transiently see the sender's peer ID during live submission, but the identity is **not persisted**. Anonymity means: hidden from other participants and public snapshots. It is not cryptographic anonymity from the poll owner.
+- When `anonymous: false`, `voterId` is the voter's public key
+- This flag is set at poll creation and **cannot be changed** after any votes are cast
+
+### Visibility
+
+- `private`: Only users with assigned roles can access. Poll link requires role assignment.
+- `link`: Anyone with the poll link can view as a viewer. No directory listing.
+- `public`: Poll snapshot is published to the server. Discoverable via direct link (no directory listing yet β can be added with future discovery feature).
+
+### Roles & Permissions
+
+| Action | Viewer | Participant | Moderator | Owner |
+|---|---|---|---|---|
+| View poll & results | β
| β
| β
| β
|
+| Add options | β | β
| β
| β
|
+| Vote | β | β
| β
| β
|
+| Add/remove users | β | β | β
| β
|
+| Start/stop poll | β | β | β
| β
|
+| Delete poll | β | β | β | β
|
+
+- Owner is implicit (`poll.ownerId === userId`); not stored in `roles[]`
+- `RoleAssignment` entries in `poll.roles[]` grant viewer, participant, or moderator access
+- Users without a role assignment who connect via link get `viewer` by default
+- Permission checks happen both client-side (UI gating) and owner-side on message receipt (owner validates before applying any mutation)
+- Role changes are broadcast to all connected peers
+
+### Sync Strategy
+
+- Owner is the source of truth
+- On connect: owner sends full poll snapshot
+- Changes (new vote, new option, role change) are sent as incremental messages
+- Participants cache poll locally for offline viewing
+
+## Plan
+
+- [ ] Define TypeScript interfaces for Poll, Option, Vote, RoleAssignment
+- [ ] Create poll store (Svelte store + IndexedDB)
+- [ ] Implement poll CRUD operations locally
+- [ ] Implement sync: snapshot on connect, incremental updates
+- [ ] Enforce anonymity invariant (voterId null when anonymous)
+- [ ] Create permission check utility (`canVote()`, `canAddOption()`, `canModerate()`, etc.)
+- [ ] Implement owner-side validation of incoming messages against roles
+- [ ] Implement role change broadcast over PeerJS
+
+## Test
+
+- [ ] Poll can be created, read, updated locally
+- [ ] Anonymous polls never store voter identity
+- [ ] Poll state syncs correctly between two peers
+- [ ] Incremental updates apply correctly to cached state
+- [ ] Viewer cannot vote or add options
+- [ ] Participant can vote and add options
+- [ ] Moderator can add/remove users and start/stop poll
+- [ ] Only owner can delete
+- [ ] Unknown users connecting via link get viewer role
diff --git a/specs/005-poll-creation-management/README.md b/specs/005-poll-creation-management/README.md
new file mode 100644
index 0000000..ec0f944
--- /dev/null
+++ b/specs/005-poll-creation-management/README.md
@@ -0,0 +1,63 @@
+---
+status: planned
+created: '2026-03-16'
+tags:
+ - polls
+ - management
+priority: high
+created_at: '2026-03-16T07:51:49.209Z'
+depends_on:
+ - 004-poll-data-model
+updated_at: '2026-03-16T07:52:06.762Z'
+related:
+ - 011-mobile-first-ui
+---
+
+# Poll Creation & Management
+
+> **Status**: ποΈ Planned Β· **Priority**: High Β· **Created**: 2026-03-16 Β· **Tags**: polls, management
+
+## Overview
+
+UI and logic for creating, configuring, and managing polls. The poll creator (owner) can set title, description, anonymity mode, add initial options, manage the poll lifecycle (draft β open β closed), and delete the poll.
+
+## Design
+
+### Poll Creation Flow
+
+1. User taps "Create Poll"
+2. Fills in: title, description, anonymous (toggle), initial options (optional)
+3. Poll is created in `draft` status (only owner can see it)
+4. Owner shares poll link β users who join get `viewer` role; owner promotes to participant/moderator
+5. Owner (or moderator) starts the poll β status becomes `open`
+6. Owner (or moderator) stops the poll β status becomes `closed`
+
+### Owner Capabilities
+
+- **Delete poll**: Only the owner can permanently delete
+- **Start/stop**: Owner and moderators can transition `draftβopenβclosed`
+- **Re-open**: Owner can move `closedβopen` (but not change anonymity)
+
+### Pages
+
+- `/app/create` β poll creation form
+- `/app/poll/[id]` β poll view/management (adapts based on role)
+- `/app/polls` β list of user's polls (owned + participating)
+
+## Plan
+
+- [ ] Build poll creation form (title, description, anonymous toggle, initial options)
+- [ ] Implement poll lifecycle state machine (draft β open β closed)
+- [ ] Build poll list page (my polls, polls I participate in)
+- [ ] Build poll detail/management page
+- [ ] Add delete poll functionality (owner only)
+- [ ] Wire up to PeerJSβpoll becomes "live" when owner opens it
+- [ ] Build user/role management UI in poll detail page
+- [ ] Implement invite flow (shareable link β users join as viewer, owner promotes)
+
+## Test
+
+- [ ] Poll can be created with all fields
+- [ ] Anonymity toggle locks after first vote
+- [ ] Poll lifecycle transitions work correctly
+- [ ] Owner can delete poll; others cannot
diff --git a/specs/008-voting-system/README.md b/specs/008-voting-system/README.md
new file mode 100644
index 0000000..0dfebb8
--- /dev/null
+++ b/specs/008-voting-system/README.md
@@ -0,0 +1,74 @@
+---
+status: planned
+created: '2026-03-16'
+tags:
+ - voting
+ - core
+priority: high
+created_at: '2026-03-16T07:51:50.525Z'
+depends_on:
+ - 005-poll-creation-management
+updated_at: '2026-03-16T09:18:58.099Z'
+related:
+ - 011-mobile-first-ui
+---
+
+# Voting System & Anonymity
+
+> **Status**: ποΈ Planned Β· **Priority**: High Β· **Created**: 2026-03-16 Β· **Tags**: voting, core
+
+## Overview
+
+Core voting functionality: participants can add options to a poll and cast votes. Supports both anonymous and non-anonymous modes as configured at poll creation. Votes are sent to the poll owner via PeerJS.
+
+## Design
+
+### Adding Options
+
+- Participants and above can add options while the poll is `open`
+- Options have text only (keep it simple)
+- Owner validates the sender has permission before accepting
+
+### Casting Votes
+
+- V1: single-choice (one vote per participant). Data model uses `optionId: string` which can later be extended to `optionIds: string[]` for multi-choice or a ranked array for ranked-choice.
+- Vote is signed with voter's private key for authenticity
+- In anonymous mode: owner records the vote but strips `voterId` before storing
+- In non-anonymous mode: `voterId` is stored alongside the vote
+- Vote changes: a participant can change their vote while the poll is open (replaces previous)
+
+### Results
+
+- Results are visible to all roles (viewers included)
+- Show: option text, vote count, percentage
+- Non-anonymous: also show who voted for what
+- Results update in real-time via PeerJS messages
+
+### Vote Flow
+
+```
+Participant Owner (relay)
+ β β
+ ββββ poll:vote ββββββββββββββββΆβ validate permission
+ β β if anonymous: strip voterId
+ β β store vote
+ ββββ poll:state:update ββββββββ broadcast updated results
+```
+
+## Plan
+
+- [ ] Build "add option" UI and message handler
+- [ ] Build voting UI (option list with vote button)
+- [ ] Implement vote submission (sign + send to owner)
+- [ ] Owner-side vote processing (validate, anonymize if needed, store)
+- [ ] Build results display (bar chart or simple percentage view)
+- [ ] Implement vote change (replace previous vote)
+
+## Test
+
+- [ ] Participant can add an option
+- [ ] Participant can cast a vote
+- [ ] Anonymous vote does not leak voter identity
+- [ ] Results update in real-time across connected peers
+- [ ] Vote change replaces (not duplicates) previous vote
+- [ ] Viewers can see results but not vote
diff --git a/specs/009-public-sharing/README.md b/specs/009-public-sharing/README.md
new file mode 100644
index 0000000..96cbd23
--- /dev/null
+++ b/specs/009-public-sharing/README.md
@@ -0,0 +1,65 @@
+---
+status: planned
+created: '2026-03-16'
+tags:
+ - sharing
+ - web
+priority: medium
+created_at: '2026-03-16T07:51:51.015Z'
+depends_on:
+ - 005-poll-creation-management
+ - 008-voting-system
+ - 012-lightweight-server
+updated_at: '2026-03-16T07:57:41.176Z'
+related:
+ - 011-mobile-first-ui
+---
+
+# Public Sharing & Read-Only View
+
+> **Status**: ποΈ Planned Β· **Priority**: Medium Β· **Created**: 2026-03-16 Β· **Tags**: sharing, web
+
+## Overview
+
+Make polls shareable via URL to anyone on the webβeven non-users. A public link shows poll results in a read-only view. If the poll owner is online, results update live via WebRTC. If offline, the page shows a cached snapshot.
+
+## Design
+
+### Shareable URL
+
+- Format: `https://evocracy.app/p/[pollId]`
+- Anyone with the link can view (no identity required)
+- The page fetches the snapshot from the server (includes `ownerPeerId`), then attempts a PeerJS connection for live data
+- Falls back to a static snapshot if owner is offline
+
+### Snapshot Strategy
+
+- When a poll is shared publicly, the owner's client pushes a JSON snapshot to the lightweight server (same server as spec 007 directory)
+- Endpoint: `PUT /api/polls/:id/snapshot` (authenticated by owner's signature)
+- Public fetch: `GET /api/polls/:id/snapshot` (no auth required)
+- Snapshot is updated whenever poll state changes while owner is online
+- Snapshot includes: title, description, options, vote counts, status (no voter identities even for non-anonymous)
+
+### Public View Page
+
+- Read-only: title, description, options with vote counts/percentages
+- Visual bar chart of results
+- "Owner offline" indicator if can't connect
+- Open Graph meta tags for social media previews
+- Optional: "Join to vote" CTA linking to app
+
+## Plan
+
+- [ ] Create `/p/[id]` public route (SvelteKit SSR or client-side)
+- [ ] Implement PeerJS connection attempt for live data
+- [ ] Build read-only results view
+- [ ] Add Open Graph meta tags for link previews
+- [ ] Implement snapshot fallback (based on decision above)
+- [ ] Add "Share" button to poll management page (copy link)
+
+## Test
+
+- [ ] Public URL shows poll results without authentication
+- [ ] Live updates work when owner is online
+- [ ] Graceful fallback when owner is offline
+- [ ] Social media link preview shows poll title/description
diff --git a/specs/010-embeddable-widget/README.md b/specs/010-embeddable-widget/README.md
new file mode 100644
index 0000000..8286f2c
--- /dev/null
+++ b/specs/010-embeddable-widget/README.md
@@ -0,0 +1,71 @@
+---
+status: planned
+created: '2026-03-16'
+tags:
+ - embed
+ - widget
+priority: low
+created_at: '2026-03-16T07:51:51.430Z'
+depends_on:
+ - 009-public-sharing
+updated_at: '2026-03-16T07:52:06.356Z'
+---
+
+# Embeddable Poll Widget
+
+> **Status**: ποΈ Planned Β· **Priority**: Low Β· **Created**: 2026-03-16 Β· **Tags**: embed, widget
+
+## Overview
+
+Allow polls to be embedded on external websites and potentially in messaging apps. This extends the public sharing with an inline experience.
+
+## Design
+
+### Embed Options
+
+1. **iframe embed** (simplest, broadest support)
+ - ``
+ - Renders a compact read-only results view
+ - Self-contained, works anywhere iframes are supported
+ - Uses `postMessage` to communicate height for responsive sizing
+
+2. **oEmbed protocol** (for platforms that support itβNotion, WordPress, Medium, etc.)
+ - Endpoint: `https://evocracy.app/oembed?url=https://evocracy.app/poll/[pollId]`
+ - Returns iframe-based rich embed
+
+3. **Web Component** (deferred to v2)
+ - ``
+ - ``
+ - Shadow DOM for style isolation
+ - Deferred: iframe covers 90% of use cases
+
+### Messenger Compatibility
+
+- iMessage / WhatsApp / Telegram: Open Graph link previews (handled by spec 009)
+- Slack / Discord: oEmbed + Open Graph unfurling
+- Actual inline voting in messengers is not feasible without platform-specific bots
+
+### Embed Route
+
+- `/embed/[id]` β minimal chrome, compact layout, no navigation
+- Auto-resizes via `postMessage` to parent frame
+
+## Plan
+
+- [ ] Create `/embed/[id]` route with compact poll results view
+- [ ] Implement iframe auto-resize via postMessage
+- [ ] Add "Get embed code" UI to poll management page
+- [ ] Implement oEmbed endpoint (`/oembed`)
+- [ ] ~Build web component wrapper (`widget.js`)~ (deferred to v2)
+
+## Test
+
+- [ ] iframe embed renders correctly on a test HTML page
+- [ ] Auto-resize adjusts to content height
+- [ ] oEmbed endpoint returns valid JSON
+- [ ] Web component renders in isolation (Shadow DOM)
+
+## Notes
+
+- **DECIDED**: Web component deferred to v2βiframe alone covers 90% of use cases
+- Messenger "embeds" are really just link previews, not interactive widgets
diff --git a/specs/012-lightweight-server/README.md b/specs/012-lightweight-server/README.md
new file mode 100644
index 0000000..1052367
--- /dev/null
+++ b/specs/012-lightweight-server/README.md
@@ -0,0 +1,81 @@
+---
+status: planned
+created: '2026-03-16'
+tags:
+ - server
+ - infra
+priority: high
+created_at: '2026-03-16T07:57:36.544Z'
+depends_on:
+ - 001-project-setup
+updated_at: '2026-03-16T09:32:10.798Z'
+---
+
+# Poll Snapshot Server
+
+> **Status**: ποΈ Planned Β· **Priority**: High Β· **Created**: 2026-03-16 Β· **Tags**: server, infra
+
+## Overview
+
+A minimal server that stores poll snapshots for public/offline viewing. This is the only server-side component β intentionally thin. No user accounts, no directory, no auth beyond signature verification.
+
+## Design
+
+### Tech Stack
+
+- **Runtime**: Cloudflare Workers (or a simple Bun server if self-hosting)
+- **Storage**: Cloudflare KV (or SQLite for self-hosted)
+- **Auth**: Snapshot writes are signed with the poll owner's Ed25519 private key
+
+### API Endpoints
+
+- `PUT /api/polls/:id/snapshot` β Store/update snapshot (signed by poll owner)
+ - Body: `{ pollId, ownerId, ownerPeerId, title, description, options[], voteCounts, status, signature }`
+- `GET /api/polls/:id/snapshot` β Fetch snapshot (public, no auth)
+
+### Security
+
+- Write operations require a valid Ed25519 signature
+- Verify signature against `ownerId` in the request body; bind `pollId β ownerId` on first write; reject future writes if `ownerId` changes
+- No sessions, no cookies, no passwords
+- Rate limiting on writes to prevent abuse
+
+### Data Model
+
+```typescript
+// KV key: poll:{pollId}:snapshot β PollSnapshot
+
+interface PollSnapshot {
+ pollId: string;
+ ownerId: string;
+ ownerPeerId: string;
+ title: string;
+ description: string;
+ options: { id: string; text: string }[];
+ voteCounts: Record;
+ status: 'draft' | 'open' | 'closed';
+ updatedAt: number;
+}
+```
+
+## Plan
+
+- [ ] Set up Cloudflare Workers project (or simple Bun server)
+- [ ] Implement Ed25519 signature verification middleware
+- [ ] Implement poll snapshot store/fetch endpoints
+- [ ] Add rate limiting
+- [ ] Deploy
+
+## Test
+
+- [ ] Snapshot store with valid signature succeeds
+- [ ] Snapshot store with invalid signature is rejected
+- [ ] Snapshot fetch returns stored data (public, no auth)
+- [ ] Second write from different ownerId is rejected
+- [ ] Rate limiting works
+
+## Notes
+
+- The P2P app works without this server β it just loses public sharing and offline snapshot viewing
+- Future: user directory endpoints can be added here when peer discovery is implemented (see archived spec 007)
+- Consider a TTL on snapshots (e.g., auto-expire 90 days after last update)
diff --git a/specs/archived/006-role-permission-system/README.md b/specs/archived/006-role-permission-system/README.md
new file mode 100644
index 0000000..5217d4a
--- /dev/null
+++ b/specs/archived/006-role-permission-system/README.md
@@ -0,0 +1,67 @@
+---
+status: archived
+created: '2026-03-16'
+tags:
+ - auth
+ - roles
+priority: high
+created_at: '2026-03-16T07:51:49.636Z'
+depends_on:
+ - 004-poll-data-model
+updated_at: '2026-03-16T09:18:35.900Z'
+transitions:
+ - status: archived
+ at: '2026-03-16T09:18:35.900Z'
+---
+
+# Role & Permission System
+
+> **Status**: π¦ Archived Β· **Priority**: High Β· **Created**: 2026-03-16 Β· **Tags**: auth, roles
+
+## Overview
+
+Each poll has a role-based permission system. The owner assigns roles to users they discover or invite. Roles control what actions a user can perform on a poll.
+
+## Design
+
+### Roles & Permissions
+
+| Action | Viewer | Participant | Moderator | Owner |
+|---|---|---|---|---|
+| View poll & results | β
| β
| β
| β
|
+| Add options | β | β
| β
| β
|
+| Vote | β | β
| β
| β
|
+| Add/remove users | β | β | β
| β
|
+| Start/stop poll | β | β | β
| β
|
+| Delete poll | β | β | β | β
|
+
+### Implementation
+
+- Owner is implicit (poll.ownerId === userId)
+- Roles stored in `poll.roles[]` array
+- Role changes are broadcast to all connected peers
+- Moderators can invite users by peer ID (discovered via spec 007) or by sharing a poll link
+- Permission checks happen both client-side (UI) and on message receipt (owner validates)
+
+### Invite Flow
+
+1. Owner/moderator discovers a user (see spec 007) or has their peer ID
+2. Assigns them a role β updates `poll.roles[]`
+3. When that user connects, they receive the poll state including their role
+4. Users without a role who connect via link get `viewer` by default
+
+## Plan
+
+- [ ] Implement role assignment data model
+- [ ] Create permission check utility (`canVote()`, `canModerate()`, etc.)
+- [ ] Build user management UI in poll detail page
+- [ ] Implement role change broadcast over PeerJS
+- [ ] Owner-side validation of incoming messages against roles
+
+## Test
+
+- [ ] Viewer cannot vote or add options
+- [ ] Participant can vote and add options
+- [ ] Moderator can add/remove users and start/stop poll
+- [ ] Only owner can delete
+- [ ] Unknown users connecting via link get viewer role
diff --git a/specs/archived/007-peer-discovery/README.md b/specs/archived/007-peer-discovery/README.md
new file mode 100644
index 0000000..eac15dd
--- /dev/null
+++ b/specs/archived/007-peer-discovery/README.md
@@ -0,0 +1,65 @@
+---
+status: archived
+created: '2026-03-16'
+tags:
+ - p2p
+ - discovery
+priority: medium
+created_at: '2026-03-16T07:51:50.076Z'
+depends_on:
+ - 002-p2p-networking
+ - 003-user-identity-profiles
+ - 012-lightweight-server
+updated_at: '2026-03-16T09:31:19.812Z'
+transitions:
+ - status: archived
+ at: '2026-03-16T09:31:19.812Z'
+---
+
+# Peer Discovery by Tags
+
+> **Status**: π¦ Archived Β· **Priority**: Medium Β· **Created**: 2026-03-16 Β· **Tags**: p2p, discovery
+
+## Overview
+
+Enable poll owners to discover other users to invite, based on user tags (location, interests, expertise). This is P2Pβthere's no central user directory. Discovery works by asking connected peers for introductions.
+
+## Design
+
+### Discovery Mechanism
+
+1. **Direct invite**: Share a poll link or paste a peer ID manually
+2. **Directory server**: Lightweight server where users can register their profile for discoverability
+ - Users opt-in to being listed (toggle in profile)
+ - Server stores: peer ID, name, tags, `discoverable` flag
+ - Query endpoint: `GET /api/users?tag=location:Berlin&tag=expertise:UX`
+ - Minimal serverβjust a thin REST API over a key-value store (e.g., Cloudflare Workers + KV, or a simple SQLite API)
+3. **Peer-chain discovery** (secondary): Ask connected peers "who do you know matching these tags?"
+ - Supplements the directory for users who are connected but not listed
+
+### Privacy Controls
+
+- Users opt-in to being discoverable (setting in profile)
+- Directory only stores public profile data (name, tags)βno private keys, no bio
+- Users can remove themselves from directory at any time
+
+## Plan
+
+- [ ] Build lightweight directory server (REST API + KV store)
+- [ ] Add `discoverable` toggle to user profile
+- [ ] Implement directory registration (opt-in publish profile)
+- [ ] Implement directory search (query by tags)
+- [ ] Build discovery UI (search by tags, browse results)
+- [ ] Implement "invite from discovery" flow
+- [ ] Implement peer-chain discovery as fallback
+
+## Test
+
+- [ ] Discovery request returns matching peers from connected contacts
+- [ ] Non-discoverable users are not shared
+- [ ] Discovered peer can be invited to a poll
+
+## Notes
+
+- **DECIDED**: Adding a lightweight directory server for discoverability
+- Directory server can be reused for poll snapshot storage (see spec 009)
diff --git a/specs/archived/011-mobile-first-ui/README.md b/specs/archived/011-mobile-first-ui/README.md
new file mode 100644
index 0000000..490e00d
--- /dev/null
+++ b/specs/archived/011-mobile-first-ui/README.md
@@ -0,0 +1,74 @@
+---
+status: archived
+created: '2026-03-16'
+tags:
+ - ui
+ - mobile
+priority: high
+created_at: '2026-03-16T07:51:51.869Z'
+related:
+ - 005-poll-creation-management
+ - 008-voting-system
+ - 009-public-sharing
+updated_at: '2026-03-16T09:07:18.408Z'
+transitions:
+ - status: archived
+ at: '2026-03-16T09:07:18.408Z'
+---
+
+# Mobile-First UI Design
+
+> **Status**: π¦ Archived Β· **Priority**: High Β· **Created**: 2026-03-16 Β· **Tags**: ui, mobile
+
+## Overview
+
+Design all UI mobile-first. The primary use case is people voting on their phones. Desktop is a secondary concernβlayouts should scale up, not be shoehorned down.
+
+## Design
+
+### Principles
+
+- Touch targets β₯ 44px
+- Single-column layout on mobile; expand on larger screens
+- Bottom navigation bar (thumb-friendly)
+- Minimal chromeβcontent first
+- System font stack for performance
+
+### Key Screens
+
+1. **Home / Poll list**: Cards showing poll title, status, vote count
+2. **Create poll**: Simple form, large inputs, toggle for anonymity
+3. **Poll detail**: Results visualization, vote buttons, participant list
+4. **Profile**: Edit name, bio, tags
+5. **Poll management** (owner/mod): User list, role controls, start/stop
+
+### Navigation
+
+- Bottom tab bar: Home, Create (+), Profile
+- Poll detail accessed by tapping a poll card
+- Management accessed via gear icon within poll detail
+
+### Styling
+
+- Tailwind CSS with mobile breakpoints as default
+- Dark mode support (respects `prefers-color-scheme`)
+- CSS transitions for state changes (vote submitted, poll status change)
+
+## Plan
+
+- [ ] Design bottom tab navigation component
+- [ ] Build poll list (home) with card layout
+- [ ] Build poll creation form (mobile-optimized)
+- [ ] Build poll detail view with results visualization
+- [ ] Build profile edit page with tag management
+- [ ] Build poll management panel (users, roles, lifecycle)
+- [ ] Add dark mode toggle / system preference detection
+- [ ] Test on various mobile viewport sizes
+
+## Test
+
+- [ ] All touch targets meet 44px minimum
+- [ ] Layout works on 320pxβ428px width (small to large phones)
+- [ ] No horizontal scroll on any page
+- [ ] Dark mode renders correctly
+- [ ] Navigation is accessible via keyboard/screen reader