diff --git a/.github/workflows/go-backend-ci.yml b/.github/workflows/go-backend-ci.yml
new file mode 100644
index 0000000..67ae9bf
--- /dev/null
+++ b/.github/workflows/go-backend-ci.yml
@@ -0,0 +1,115 @@
+name: go-backend-ci
+
+on:
+ workflow_dispatch:
+ pull_request:
+ paths:
+ - "go-backend/**"
+ - ".github/workflows/go-backend-ci.yml"
+ push:
+ branches:
+ - main
+ - develop
+ paths:
+ - "go-backend/**"
+ - ".github/workflows/go-backend-ci.yml"
+
+concurrency:
+ group: go-backend-ci-${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ check-go-backend:
+ name: Check go-backend
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
+
+ services:
+ postgres:
+ image: postgres:16
+ env:
+ POSTGRES_DB: xtablo
+ POSTGRES_USER: xtablo
+ POSTGRES_PASSWORD: xtablo
+ ports:
+ - 5432:5432
+ options: >-
+ --health-cmd "pg_isready -U xtablo -d xtablo"
+ --health-interval 10s
+ --health-timeout 5s
+ --health-retries 5
+
+ defaults:
+ run:
+ working-directory: go-backend
+
+ env:
+ DATABASE_URL: postgres://xtablo:xtablo@127.0.0.1:5432/xtablo?sslmode=disable
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v6
+
+ - name: Set up Go
+ uses: actions/setup-go@v6
+ with:
+ go-version-file: go-backend/go.mod
+ cache-dependency-path: go-backend/go.sum
+
+ - name: Install PostgreSQL client
+ run: sudo apt-get update && sudo apt-get install -y postgresql-client
+
+ - name: Install code generation tools
+ run: |
+ go install github.com/a-h/templ/cmd/templ@v0.3.1001
+ go install github.com/sqlc-dev/sqlc/cmd/sqlc@v1.31.1
+
+ - name: Load database schema
+ run: psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f internal/db/schema.sql
+
+ - name: Seed database
+ run: psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f internal/db/seed.sql
+
+ - name: Generate templ and sqlc code
+ run: |
+ templ generate
+ sqlc generate
+
+ - name: Verify generated files are committed
+ run: git diff --exit-code -- internal/db/sqlc internal/web/views
+
+ - name: Run tests
+ run: go test ./...
+
+ - name: Build
+ run: go build ./...
+
+ - name: Smoke test app against Postgres
+ run: |
+ set -euo pipefail
+
+ go run . >/tmp/go-backend.log 2>&1 &
+ app_pid=$!
+ trap 'kill $app_pid || true; wait $app_pid || true' EXIT
+
+ for _ in $(seq 1 30); do
+ if curl -fsS http://127.0.0.1:3000/login >/tmp/go-backend-login.html; then
+ break
+ fi
+ sleep 1
+ done
+
+ grep -q "Se connecter à Xtablo" /tmp/go-backend-login.html
+
+ curl -fsS \
+ -D /tmp/go-backend-signup.headers \
+ -o /tmp/go-backend-signup.body \
+ -X POST \
+ -H 'Content-Type: application/x-www-form-urlencoded' \
+ --data 'email=ci-user@example.com&password=xtablo-secret' \
+ http://127.0.0.1:3000/signup
+
+ grep -qi '^Hx-Redirect: /' /tmp/go-backend-signup.headers
+ test "$(psql "$DATABASE_URL" -tA -c "select count(*) from auth.users where email = 'ci-user@example.com'")" = "1"
+ test "$(psql "$DATABASE_URL" -tA -c "select count(*) from public.users where email = 'ci-user@example.com'")" = "1"
+ test "$(psql "$DATABASE_URL" -tA -c "select count(*) from auth.sessions where user_id = (select id from auth.users where email = 'ci-user@example.com')")" = "1"
diff --git a/.gitignore b/.gitignore
index 3cfb0b0..a1c325c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -45,3 +45,14 @@ dist
# Supabase
supabase/.temp
supabase/.branches
+
+# Podman
+.podman-compose
+
+# AI
+.claude
+.codex
+
+# go built binaries
+backend/web
+backend/worker
diff --git a/.planning/MILESTONES.md b/.planning/MILESTONES.md
new file mode 100644
index 0000000..a1c1e65
--- /dev/null
+++ b/.planning/MILESTONES.md
@@ -0,0 +1,60 @@
+# Project Milestones: Xtablo
+
+## v3.0 Design System & Visual Polish (Shipped: 2026-05-17)
+
+**Delivered:** Full design system ported from go-backend and applied to every product surface — auth pages, app shell, tablo list, tablo detail, chat, and planning.
+
+**Phases completed:** 13-17 (5 phases, 16 plans)
+
+**Key accomplishments:**
+
+- Built a complete CSS token vocabulary and typed templ component library (11 types: button, badge, card, modal, empty-state, table, icon-button, input, textarea, select, form-field) with a build-tag-gated `/ui-catalog` visual verification page.
+- Restyled auth pages (login/signup) with gradient animated background, centered auth-card, brand logo, and Google Material Design sign-in button with Roboto font.
+- Implemented app shell: sidebar with brand section, icon nav items, tablo list section, and user footer; tablo list with project-card grid, color avatars, and action controls.
+- Restyled tablo detail: project-card-top header, tasks-section kanban, etapes section, and files table using `@ui.Table` with `@ui.EmptyState` fallback.
+- Added own-vs-others chat bubbles with brand-tint right-alignment and planning day-separator event list using overview-section layout.
+
+**Stats:**
+
+- 150 files changed, +23,505 / −993 lines across v3.0 git range
+- 5 phases, 16 plans
+- 2 days (2026-05-16 → 2026-05-17)
+- Go backend: ~18,600 non-test LOC + ~3,400 templ LOC
+
+**Git range:** `3b9bd62` → `fa48d81`
+
+**Known deferred items at close:** 6 acknowledged open artifact items (all pre-existing from v1.0/v2.0). See `.planning/STATE.md` Deferred Items.
+
+---
+
+Entries are listed newest first.
+
+## v2.0 Collaboration, Planning, and Social Sign-in (Shipped: 2026-05-16)
+
+**Delivered:** Google sign-in, one-level etapes, tablo-owned events, personal planning, and native realtime tablo discussions on the Go + HTMX stack.
+
+**Phases completed:** 8-12 (18 plans total)
+
+**Key accomplishments:**
+
+- Added Google social sign-in while preserving Xtablo-owned users and sessions.
+- Added one-level etapes for task organization without breaking the kanban model.
+- Added tablo-owned events and an authenticated personal planning agenda.
+- Added native persisted realtime discussions using Postgres and Go-owned SSE.
+- Kept collaboration and scheduling features inside the single Go + HTMX product architecture.
+
+**Stats:**
+
+- 148 files changed across the v2.0 git range
+- 17,704 insertions and 527 deletions across the v2.0 git range
+- 5 phases, 18 plans, approximately 39 implementation/verification tasks
+- 2 days from milestone start to close (2026-05-15 -> 2026-05-16)
+- Current backend source footprint: 29,101 lines across Go, templ, SQL, and CSS files
+
+**Git range:** `0d23d94` -> `58a9ce0`
+
+**Known deferred items at close:** 7 acknowledged open artifact items. See `.planning/STATE.md` Deferred Items and `.planning/milestones/v2.0-MILESTONE-AUDIT.md`.
+
+**What's next:** v3.0 Design System & Visual Polish is already planned in `.planning/ROADMAP.md`.
+
+---
diff --git a/.planning/PROJECT.md b/.planning/PROJECT.md
new file mode 100644
index 0000000..b26102a
--- /dev/null
+++ b/.planning/PROJECT.md
@@ -0,0 +1,147 @@
+# Xtablo — Go + HTMX Product
+
+## What This Is
+
+A Go + HTMX version of Xtablo centered on tablos as collaborative workspaces. v1 established the authenticated Tablos workflow (tasks, files, worker, deploy); v2 added collaboration, messaging, etapes, events, planning, and Google sign-in; v3 ported the full design system from the reference JS app — CSS token vocabulary, typed templ component library, and a visual reskin of every surface.
+
+Built for a developer who wants a simpler, durable product stack: one Go server, Postgres, server-rendered UI, and no managed chat provider.
+
+## Core Value
+
+**A user can sign in and run the Tablos workflow — organize work, attach files, discuss, and plan scheduled events — without a JS framework or managed chat provider.**
+
+If everything else fails, this must work end-to-end on a single Go binary backed by Postgres and an S3-compatible bucket.
+
+## Current Milestone: v4.0 Figma Design Parity
+
+**Goal:** Bring every product surface to match the Figma design — new task views (grid + roadmap), calendar rework, navigation refresh, and tablo list revamp.
+
+**Target features:**
+- Navigation bar + top header redesigned to match Figma
+- Tablo list: project card revamp + card/list view toggle
+- Tasks: restyle kanban + new grid view + new roadmap view
+- Calendar: restyle `/planning` as Google Calendar-style (month/week/day navigation, proper event blocks)
+- Tablo detail view restyled to match Figma
+
+## Shipped: v3.0 Design System & Visual Polish (2026-05-17)
+
+**Delivered:** Full CSS token vocabulary and typed templ component library (11 types) applied to every surface — auth pages, app shell sidebar, tablo list, tablo detail, chat, and planning. Build-tag-gated `/ui-catalog` visual verification page included.
+
+## Requirements
+
+### Validated
+
+
+
+- ✓ Server-managed sessions with email/password auth — Phase 2 (v1.0)
+- ✓ Tablos CRUD with ownership — Phase 3 (v1.0)
+- ✓ Tasks kanban with drag-and-drop reorder — Phase 4 (v1.0)
+- ✓ File attachments with orphan cleanup worker — Phases 5–6 (v1.0)
+- ✓ Single-binary production deploy with Caddy TLS — Phase 7 (v1.0)
+- ✓ Google sign-in (server-managed sessions) — Phase 8 (v2.0)
+- ✓ Etapes as one-level task wrappers — Phase 9 (v2.0)
+- ✓ Tablo events with CRUD — Phase 10 (v2.0)
+- ✓ Individual planning view (`/planning`) — Phase 11 (v2.0)
+- ✓ Native per-tablo chat with real-time SSE — Phase 12 (v2.0)
+- ✓ CSS design token vocabulary + typed templ component library (11 types) in `backend/internal/web/ui` — Phase 13 (v3.0)
+- ✓ Auth pages (login/signup) match JS app visual — Phase 14 (v3.0)
+- ✓ App shell sidebar + project-card tablo grid — Phase 15 (v3.0)
+- ✓ Tablo detail (tasks, etapes, files) restyled with design system components — Phase 16 (v3.0)
+- ✓ Chat and planning views visually consistent with app shell — Phase 17 (v3.0)
+
+### Active
+
+
+
+- [ ] Navigation bar + top header redesigned to match Figma
+- [ ] Tablo list project card revamped to match Figma
+- [ ] Tablo list card/list view toggle
+- [ ] Tasks kanban restyled to match Figma
+- [ ] Tasks grid view (new — brand new data model + UI)
+- [ ] Tasks roadmap view (new — brand new)
+- [ ] Planning page restyled as Google Calendar-style (month/week/day, event blocks)
+- [ ] Tablo detail view restyled to match Figma
+
+### Out of Scope
+
+
+
+- **Managed chat/messaging providers** — no Stream Chat, Ably, Pusher, Firebase Realtime Database, or equivalent for v2 chat
+- **Stripe / billing** — defer monetization until product loop is validated
+- **Public booking widget** — `apps/external` rewrite not in v1 (may return in a later milestone)
+- **Client portal** — `apps/clients` magic-link experience deferred
+- **Admin app** — `apps/admin` internal tooling deferred
+- **Notes / rich documents** inside a Tablo — not part of this milestone
+- **Managed auth platforms** — no Clerk/Auth0/Lucia; Google and Apple are identity providers only, with Xtablo still owning users and sessions
+- **Mobile / Expo app** — out of scope for this rewrite
+- **Supabase** as a runtime dependency — Postgres only; Supabase Auth / RLS replaced by Go-side authz
+- **The existing `go-backend/` directory** — treated as scratch; new code lives in a fresh `backend/` Go package
+
+## Context
+
+- The JS monorepo (this repository) is the source of truth for product behavior and is fully mapped in `.planning/codebase/` (ARCHITECTURE, STACK, STRUCTURE, INTEGRATIONS, CONVENTIONS, CONCERNS, TESTING). Use these to derive expected behavior — but the rewrite is free to simplify both schema and visuals.
+- The DB schema **will change** during the rewrite — the JS version's Supabase schema is a reference, not a constraint. The user wants to be in the loop on schema decisions for each domain (users/sessions, tablos, tasks, files).
+- v3.0 shipped the full design system and visual reskin. `backend/internal/web/ui` now has: CSS token vocabulary, 11 typed templ components, an app shell, and surface-specific CSS (auth, dashboard, tablo detail, chat, planning).
+- Current backend source footprint: ~18,600 non-test Go LOC + ~3,400 templ LOC.
+- Developer is comfortable in Go and wants a low-dependency, server-rendered stack going forward.
+- Known stubs shipped in v3.0: tablo status field hardcoded as "En cours", progress bar hardcoded at 0% — both require a DB schema extension to wire properly.
+
+## Constraints
+
+- **Tech stack**: Go (server + templates) + HTMX + Tailwind + Postgres + sqlc — no JS framework, no managed BaaS
+- **Auth**: Server-managed sessions remain authoritative; Google/Apple sign-in may verify external ID tokens but must end by issuing Xtablo's own session cookie
+- **Chat**: No third-party chat provider; messages and read state live in Postgres, with real-time delivery handled by the Go app
+- **Storage**: Files in S3-compatible object storage (Cloudflare R2 to start)
+- **Architecture**: One web server binary + one background worker (same repo, possibly same binary with subcommand)
+- **Deploy target**: Single VPS / container — no Kubernetes
+- **Planning**: Events belong to tablos, but each individual user can access their own planning view
+- **Etapes**: One level only — a task can have at most one parent/wrapper, and a parent cannot itself have a parent
+- **Scope discipline**: v2 adds collaboration and planning only; billing, booking, portal, admin, notes, and mobile remain deferred
+
+## Key Decisions
+
+| Decision | Rationale | Outcome |
+|----------|-----------|---------|
+| Rewrite in Go + HTMX (no SPA) | Simpler stack, developer preference, product pivot | ✓ Validated through v1-v2 |
+| Built-in sessions, no 3rd-party auth | Avoid vendor coupling; sessions are well-trodden ground | ✓ Validated through email/password and Google sign-in |
+| Drop Supabase (keep Postgres) | Owning the auth + RLS story in Go is simpler than maintaining the boundary | ✓ Validated through owned authz queries |
+| Fresh `backend/` Go package, set `go-backend/` aside | Existing scaffold has decisions to revisit; cleaner to start over | ✓ Validated |
+| v1 = Tablos workflow only (Tasks + Files), defer chat/billing/booking/portal/admin | Focus the rewrite around the load-bearing user loop first | ✓ Completed |
+| Single binary + background worker, single VPS deploy | Matches the "simpler stack" thesis; avoid orchestration cost early | ✓ Validated for current deploy target |
+| User-in-the-loop on Postgres schema for each domain | Schema is changing from JS version; explicit review before sqlc generation | — Pending |
+| v2 chat is native, not vendor-backed | User explicitly does not want third-party chat; Postgres + Go should be enough for the first real-time version | ✓ Validated with SSE receive + HTMX POST send |
+| Google/Apple are identity providers only | Preserve server-owned users/sessions while allowing social sign-in | ✓ Google validated; Apple disabled for now |
+| Etapes are one-level wrappers around tasks | Matches the requested mental model and avoids recursive planning complexity | ✓ Validated |
+| Planning is individual, events remain tablo-scoped | Lets users see their schedule while preserving tablos as the source of work context | ✓ Validated |
+| Design system lives in `backend/internal/web/ui` (not go-backend) | Keep all production code in `backend/`; go-backend is scratch/reference only | ✓ Validated — all 11 components + token CSS in backend/ |
+| Build-tag-gated `/ui-catalog` route for design system verification | Catalog must not ship in production; build tags are the idiomatic Go mechanism | ✓ Validated — production build passes, catalog isolated |
+| Tablo status + progress bar shipped as visual stubs | DB schema extension needed; unblocking is lower value than shipping the visual design | ⚠️ Revisit — stub needs wiring in v4.0 |
+| Air config extended to watch .css files and run Tailwind CLI on rebuild | CSS edits require Tailwind rebuild; air restarts on template changes but not CSS without this | ✓ Validated |
+
+## Evolution
+
+This document evolves at phase transitions and milestone boundaries.
+
+**After each phase transition** (via `/gsd-transition`):
+1. Requirements invalidated? → Move to Out of Scope with reason
+2. Requirements validated? → Move to Validated with phase reference
+3. New requirements emerged? → Add to Active
+4. Decisions to log? → Add to Key Decisions
+5. "What This Is" still accurate? → Update if drifted
+
+**After each milestone** (via `/gsd-complete-milestone`):
+1. Full review of all sections
+2. Core Value check — still the right priority?
+3. Audit Out of Scope — reasons still valid?
+4. Update Context with current state
+
+## Phase History
+
+- **Phase 1: Foundation** — Completed 2026-05-14. Fresh `backend/` Go package boots a web server, renders an HTMX-driven base layout, connects to local Postgres with goose migrations. FOUND-01..FOUND-05 satisfied; user-approved manual walkthrough. Two inline justfile/tailwind fixes during UAT (commit `fix(01): guard sqlc on empty queries and correct tailwind paths`).
+- **Phase 7: deploy-v1** — Completed 2026-05-15. Multi-stage Dockerfile (assets→builder→distroless nonroot), docker-compose.prod.yaml with 4 services (postgres/web/worker/caddy), Caddyfile with `{$DOMAIN}` TLS, README runbook covering first-deploy/rollback/incident. DEPLOY-01..DEPLOY-05 satisfied. 3 UAT items (Docker build, compose config, live smoke test) pending on a Docker-equipped machine.
+- **Milestone v2.0 started: Collaboration, planning, and social sign-in** — Started 2026-05-15. Scope: native per-tablo chat, one-level etapes, Google/Apple sign-in, and individual planning with tablo events.
+- **Milestone v2.0 shipped: Collaboration, planning, and social sign-in** — Shipped 2026-05-16. Delivered Google sign-in, etapes, events, individual planning, and native SSE-backed tablo discussions. Closed with 7 acknowledged deferred artifact items; see `.planning/milestones/v2.0-MILESTONE-AUDIT.md`.
+
+---
+---
+*Last updated: 2026-05-17 after v4.0 milestone start — Figma design parity milestone defined*
diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md
new file mode 100644
index 0000000..9270e91
--- /dev/null
+++ b/.planning/REQUIREMENTS.md
@@ -0,0 +1,82 @@
+# Requirements: Xtablo — Go + HTMX Product
+
+**Defined:** 2026-05-17
+**Core Value:** A user can sign in and run the Tablos workflow — organize work, attach files, discuss, and plan scheduled events — without a JS framework or managed chat provider.
+
+## v4.0 Requirements — Figma Design Parity
+
+Requirements for v4.0. Each maps to roadmap phases.
+
+### Navigation & Shell
+
+- [ ] **NAV-01**: User sees a redesigned sidebar/nav bar matching Figma (brand section, icon nav items, tablo list section, user footer)
+- [ ] **NAV-02**: User sees a per-page top header bar with page title and contextual action buttons matching Figma
+
+### Tablo List
+
+- [ ] **LIST-01**: Tablo project cards are revamped to match Figma with updated layout, typography, and progress bar wired to real task completion percentage
+- [ ] **LIST-02**: User can toggle between card grid view and list view on the tablos page
+- [ ] **LIST-03**: Tablos have an active/archived status field stored in DB, visible as a UI indicator on cards and list rows
+
+### Tasks
+
+- [x] **TASK-01**: Kanban board columns, task cards, and drag-and-drop are restyled to match Figma
+- [ ] **TASK-02**: User can switch to a grid/table view of tasks within a tablo (new view — data model + UI)
+- [ ] **TASK-03**: User can switch to a roadmap/timeline view of tasks within a tablo (new view — requires due date fields added to tasks schema)
+
+### Calendar (Planning)
+
+- [ ] **CAL-01**: User can view their planning in a month/week/day calendar grid, replacing the current flat agenda list
+- [ ] **CAL-02**: Events render as colored blocks inside calendar cells (not just text list items)
+- [ ] **CAL-03**: User can filter calendar events by tablo
+
+### Tablo Detail
+
+- [x] **DETAIL-01**: Tablo detail page (header, tasks section, etapes section, files table) is restyled to match Figma
+
+## v5.0 Candidates (Deferred)
+
+### Polish & Accessibility
+
+- **RESP-01**: Responsive layout — sidebar collapses on small screens, kanban scrolls horizontally on mobile
+- **DARK-01**: Dark mode — CSS token `prefers-color-scheme` counterparts or class-toggle variant
+
+## Out of Scope
+
+| Feature | Reason |
+|---------|--------|
+| Apple sign-in | Disabled in v2.0; remains out of scope |
+| Stripe / billing | Deferred until product loop validated |
+| Public booking widget | Deferred to a later milestone |
+| Client portal | Deferred |
+| Admin app | Deferred |
+| Mobile / Expo app | Out of scope for this rewrite |
+| Notes / rich documents | Not part of this milestone |
+
+## Traceability
+
+Which phases cover which requirements. Updated during roadmap creation.
+
+| Requirement | Phase | Status |
+|-------------|-------|--------|
+| NAV-01 | Phase 18 | Pending |
+| NAV-02 | Phase 18 | Pending |
+| LIST-01 | Phase 19 | Pending |
+| LIST-02 | Phase 19 | Pending |
+| LIST-03 | Phase 19 | Pending |
+| TASK-01 | Phase 20 | Complete |
+| DETAIL-01 | Phase 20 | Complete |
+| TASK-02 | Phase 21 | Pending |
+| TASK-03 | Phase 21 | Pending |
+| CAL-01 | Phase 22 | Pending |
+| CAL-02 | Phase 22 | Pending |
+| CAL-03 | Phase 22 | Pending |
+
+**Coverage:**
+- v4.0 requirements: 12 total
+- Mapped to phases: 12
+- Unmapped: 0 ✓
+
+---
+*Requirements defined: 2026-05-17*
+*Last updated: 2026-05-17 after initial definition*
diff --git a/.planning/RETROSPECTIVE.md b/.planning/RETROSPECTIVE.md
new file mode 100644
index 0000000..ccdf375
--- /dev/null
+++ b/.planning/RETROSPECTIVE.md
@@ -0,0 +1,126 @@
+# Project Retrospective
+
+A living document updated after each milestone. Lessons feed forward into future planning.
+
+## Milestone: v2.0 — Collaboration, Planning, and Social Sign-in
+
+**Shipped:** 2026-05-16
+**Phases:** 5 | **Plans:** 18 | **Sessions:** multiple GSD sessions
+
+### What Was Built
+
+- Google sign-in with Xtablo-owned user/session issuance and Apple sign-in disabled for the shipped scope.
+- One-level etapes for organizing tasks while preserving existing kanban behavior.
+- Tablo-owned scheduled events plus an authenticated personal planning agenda.
+- Native per-tablo discussion history, posting, unread state, and realtime SSE delivery.
+
+### What Worked
+
+- The Go + HTMX stack handled new auth, planning, and realtime surfaces without introducing a JS framework or managed chat provider.
+- DB-backed handler tests gave strong coverage for ownership, validation, and cross-tablo aggregation.
+- UAT feedback during Phase 12 caught real browser behavior around duplicate rows and composer reset.
+
+### What Was Inefficient
+
+- Milestone close happened after v3.0 was already initialized, so archival had to preserve current v3 planning files instead of using the standard current-milestone deletion path.
+- Some verification artifacts lagged behind shipped behavior, which made milestone audit status noisier than implementation state.
+- Earlier v1 UAT/verification records still carry human-needed or partial statuses and should be cleaned separately if strict historical reporting matters.
+
+### Patterns Established
+
+- Use owned Postgres queries plus handler tests as the primary guardrail for cross-user isolation.
+- Keep social identity providers as login inputs only; Xtablo sessions remain authoritative.
+- For v2 realtime, SSE receive plus HTMX POST send is sufficient and keeps infrastructure local.
+- Validation files should be closed immediately after phase execution to avoid stale milestone audits.
+
+### Key Lessons
+
+1. Archive a milestone before starting the next one, or make the close workflow explicitly support archival from a historical commit.
+2. Treat `*-VERIFICATION.md` as a required phase exit artifact, not a later audit cleanup.
+3. Browser UAT remains necessary for HTMX swap/reset behavior even when handler tests are green.
+
+### Cost Observations
+
+- Model mix: not tracked.
+- Sessions: multiple interactive GSD sessions.
+- Notable: most rework came from artifact hygiene and UAT-discovered browser behavior, not from core backend design.
+
+---
+
+## Milestone: v3.0 — Design System & Visual Polish
+
+**Shipped:** 2026-05-17
+**Phases:** 5 (13-17) | **Plans:** 16
+
+### What Was Built
+
+- Full CSS token vocabulary in `base.css` (colors, spacing, typography, shadows, gradients)
+- 11 typed templ components: button, badge, card, modal, empty-state, table, icon-button, input, textarea, select, form-field
+- Build-tag-gated `/ui-catalog` visual verification page for the design system
+- Auth pages (login/signup) with gradient animated background, auth-card layout, Google Material Design sign-in button
+- App shell sidebar with brand section, nav icons, tablo list section, user footer
+- Project-card tablo grid with color avatars, creation date, action controls
+- Tablo detail: project-card-top header, tasks-section kanban, etapes section, files `@ui.Table` with `@ui.EmptyState`
+- Chat view: own-vs-others message bubbles with brand-tint right-alignment
+- Planning view: day-separator event list using overview-section layout
+
+### What Worked
+
+- Building the design system foundation (Phase 13) before any surface work paid off — phases 14-17 moved fast because components were ready
+- Build-tag catalog (`just catalog`) made visual verification frictionless between phases
+- Air CSS watching (added in Phase 14) eliminated manual restart friction for the rest of the milestone
+- TDD for view model logic (planning view, chat IsOwn) caught integration bugs before browser verify
+
+### What Was Inefficient
+
+- REQUIREMENTS.md checkboxes for DASH-01/02/03 were not ticked even after Phase 15 completed — stale data discovered only at milestone close
+- Phase 13 summary one-liners were raw "Task 1 —" prefixes from the executor rather than human-readable accomplishments — cleaned manually at close
+- Some ROADMAP.md phase entries had stale "1/2 plans executed" counts that didn't reflect actual SUMMARY.md files
+
+### Patterns Established
+
+- Design system → auth surface → dashboard → detail → auxiliary (chat/planning) is the right reskin order
+- `@ui.Table` + `@ui.EmptyState` as a pair for any list view
+- `@AppLayout` wraps all authenticated pages; `@AuthLayout` wraps auth pages — switching is done by changing the outer template
+- Build tags (`//go:build catalog` + `//go:build !catalog` stub) as the idiom for dev-only routes
+
+### Key Lessons
+
+- Validate REQUIREMENTS.md checkboxes during phase completion, not at milestone close
+- Phase SUMMARY.md one-liners should be written by the executor in final form; raw task names create cleanup work at milestone close
+- CSS dev-watch in air is non-negotiable for any CSS-heavy phase — add it at project start, not mid-milestone
+- Catalog-first is worthwhile for design systems: visual inventory before any surface application prevents design drift
+
+### Cost Observations
+
+- 2-day milestone (2026-05-16 → 2026-05-17)
+- 150 files, +23,505/−993 lines across 128 commits
+- Most expensive phase: Phase 14 (auth pages) — auth_components + auth_layout from scratch took iteration
+- Fastest phase: Phase 13 Plan 05 (catalog) — 3 minutes once components existed
+
+---
+
+## Cross-Milestone Trends
+
+### Process Evolution
+
+| Milestone | Sessions | Phases | Key Change |
+|-----------|----------|--------|------------|
+| v2.0 | multiple | 5 | Collaboration, scheduling, and realtime work stayed inside Go + HTMX; artifact hygiene needs tighter close discipline. |
+| v3.0 | multiple | 5 | Design-system-first approach validated; surface reskins moved fast. Stale requirement checkboxes and raw summary one-liners are recurring close-time friction. |
+
+### Cumulative Quality
+
+| Milestone | Tests | Coverage | Zero-Dep Additions |
+|-----------|-------|----------|-------------------|
+| v2.0 | Go handler/DB suites plus browser UAT | Strong behavior coverage; some archive artifacts deferred | Native SSE chat, server-owned sessions, Postgres-backed planning |
+| v3.0 | TDD for view models (planning, chat IsOwn); browser verify checkpoints | Visual correctness tested via catalog + human UAT; unit coverage on behavior helpers | Design system (no new runtime deps); build-tag-gated catalog route |
+
+### Top Lessons
+
+1. Close phase validation and verification artifacts before running milestone audit.
+2. Keep milestone archival before next-milestone initialization when possible.
+3. Preserve browser UAT for HTMX interaction behavior.
+4. Check REQUIREMENTS.md checkboxes during phase execution, not at milestone close — stale checkboxes create friction.
+5. Design-system-first pays dividends: build the component library, verify in catalog, then apply to surfaces.
+6. Add CSS dev-watch (Air) at project start — it's always needed for CSS-heavy work.
diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md
new file mode 100644
index 0000000..760b6d3
--- /dev/null
+++ b/.planning/ROADMAP.md
@@ -0,0 +1,143 @@
+# Roadmap: Xtablo Go + HTMX Rewrite
+
+## Milestones
+
+- ✅ **v1.0 MVP** — Phases 1-7 (shipped 2026-05-15). Archive: [`milestones/v1.0-ROADMAP.md`](milestones/v1.0-ROADMAP.md)
+- ✅ **v2.0 Collaboration, Planning, and Social Sign-in** — Phases 8-12 (shipped 2026-05-16). Archive: [`milestones/v2.0-ROADMAP.md`](milestones/v2.0-ROADMAP.md)
+- ✅ **v3.0 Design System & Visual Polish** — Phases 13-17 (shipped 2026-05-17). Archive: [`milestones/v3.0-ROADMAP.md`](milestones/v3.0-ROADMAP.md)
+
+## v4.0 Active Phases
+
+- [x] Phase 18: App Shell & Navigation — sidebar redesign + top header bar (NAV-01, NAV-02) — completed 2026-05-17
+- [ ] Phase 19: Tablo List Revamp — card redesign, progress bar, status field, list/card toggle (LIST-01, LIST-02, LIST-03)
+- [ ] Phase 20: Tablo Detail & Kanban — detail page + kanban restyled to Figma (DETAIL-01, TASK-01)
+- [ ] Phase 21: Task Grid & Roadmap Views — new grid view + new roadmap view with date fields (TASK-02, TASK-03)
+- [ ] Phase 22: Calendar Rework — month/week/day grid, event blocks, tablo filter (CAL-01, CAL-02, CAL-03)
+
+## Phases
+
+
+✅ v1.0 MVP (Phases 1-7) — SHIPPED 2026-05-15
+
+- [x] Phase 1: Foundation — completed 2026-05-14
+- [x] Phase 2: Authentication — completed 2026-05-15
+- [x] Phase 3: Tablos CRUD — completed 2026-05-15
+- [x] Phase 4: Tasks Kanban — completed 2026-05-15
+- [x] Phase 5: File Attachments — completed 2026-05-15
+- [x] Phase 6: Background Worker — completed 2026-05-15
+- [x] Phase 7: Deploy v1 — completed 2026-05-15
+
+
+
+
+✅ v2.0 Collaboration, Planning, and Social Sign-in (Phases 8-12) — SHIPPED 2026-05-16
+
+- [x] Phase 8: Social Sign-in (5/5 plans) — completed 2026-05-15
+- [x] Phase 9: Etapes (4/4 plans) — completed 2026-05-15
+- [x] Phase 10: Events (3/3 plans) — completed 2026-05-16
+- [x] Phase 11: Individual Planning (2/2 plans) — completed 2026-05-16
+- [x] Phase 12: Native Tablo Chat (3/3 plans) — completed 2026-05-16
+
+
+
+
+✅ v3.0 Design System & Visual Polish (Phases 13-17) — SHIPPED 2026-05-17
+
+- [x] Phase 13: Design System Foundation (5/5 plans) — completed 2026-05-16
+- [x] Phase 14: Auth Pages (2/2 plans) — completed 2026-05-16
+- [x] Phase 15: Dashboard & Tablos (3/3 plans) — completed 2026-05-16
+- [x] Phase 16: Tablo Detail (4/4 plans) — completed 2026-05-17
+- [x] Phase 17: Chat & Planning (2/2 plans) — completed 2026-05-17
+
+
+
+## Phase Details — v4.0
+
+### Phase 18: App Shell & Navigation
+**Goal:** Redesign the sidebar and top header bar to match the Figma design.
+**Requirements:** NAV-01, NAV-02
+**Plans:** 3 plans
+**Success criteria:**
+1. Sidebar renders brand section, icon nav items, tablo list section, and user footer matching Figma
+2. Every authenticated page shows a top header bar with page title and contextual actions
+3. Existing navigation functionality (logout, tablo selection) is preserved
+
+Plans:
+- [ ] 18-01-PLAN.md — AppLayout signature extension + BreadcrumbItem struct + all call sites updated
+- [ ] 18-02-PLAN.md — Sidebar full HTML/CSS rebuild to Figma spec with collapse toggle
+- [ ] 18-03-PLAN.md — PageHeader component, avatar dropdown, /settings stub, tests updated
+
+### Phase 19: Tablo List Revamp
+**Goal:** Restyle the tablos page with revamped cards, real progress data, list/card toggle, and status field.
+**Requirements:** LIST-01, LIST-02, LIST-03
+**Plans:** 3 plans
+**Success criteria:**
+1. Tablo cards display with updated Figma layout including a progress bar showing real task completion %
+2. User can switch between card grid and list view; selection persists for the session
+3. Tablos have an active/archived status field in DB; a status indicator is visible on cards and list rows
+4. DB migration for status field is reversible
+
+Plans:
+- [ ] 19-01-PLAN.md — DB migration 0010, sqlc regen with status column, batch progress query, handler wiring
+- [ ] 19-02-PLAN.md — Revamped TabloProjectCard template (badge, initial, progress bar) + list row CSS
+- [ ] 19-03-PLAN.md — View toggle button + inline JS + tests for LIST-01/02/03
+
+### Phase 20: Tablo Detail & Kanban Restyle
+**Goal:** Restyle the tablo detail page and kanban board to match Figma.
+**Requirements:** DETAIL-01, TASK-01
+**Plans:** 3/3 plans complete
+**Success criteria:**
+1. Tablo detail header shows tablo name, status, and progress matching Figma
+2. Kanban columns and task cards are restyled to match Figma
+3. Drag-and-drop reorder continues to work after restyle
+4. Etapes section and files table match Figma layout
+
+Plans:
+- [x] 20-01-PLAN.md — Handler + view model + route: GET /tablos/{tabloID}, TabloDetailViewModel, test scaffold
+- [x] 20-02-PLAN.md — TabloDetailPage templ components: header, tab bar, kanban board, task cards
+- [x] 20-03-PLAN.md — CSS restyle: tablo detail header, kanban board layout, task card, progress bar, files table
+
+### Phase 21: Task Grid & Roadmap Views
+**Goal:** Add grid/table and roadmap/timeline views to the task section of a tablo.
+**Requirements:** TASK-02, TASK-03
+**Success criteria:**
+1. User can switch between kanban, grid, and roadmap views via a view switcher in the tablo detail
+2. Grid view shows all tasks as a sortable/filterable table with key columns (title, status, assignee, due date)
+3. Roadmap view shows tasks on a horizontal timeline grouped by status or date
+4. Tasks have an optional due_date field in DB (migration + sqlc); roadmap uses it for positioning
+
+### Phase 22: Calendar Rework
+**Goal:** Replace the flat agenda list on /planning with a full Google Calendar-style interface.
+**Requirements:** CAL-01, CAL-02, CAL-03
+**Success criteria:**
+1. /planning page shows a month/week/day calendar grid with navigation controls
+2. Events render as colored blocks inside calendar cells rather than plain text rows
+3. User can filter the calendar to show events from a specific tablo only
+4. Existing event CRUD (create, edit, delete) continues to work within the new calendar UI
+
+## Progress
+
+| Phase | Milestone | Plans Complete | Status | Completed |
+|-------|-----------|---------------|----------|------------|
+| 1. Foundation | v1.0 | — | Complete | 2026-05-14 |
+| 2. Authentication | v1.0 | 7/7 | Complete | 2026-05-15 |
+| 3. Tablos CRUD | v1.0 | 3/3 | Complete | 2026-05-15 |
+| 4. Tasks Kanban | v1.0 | 3/3 | Complete | 2026-05-15 |
+| 5. File Attachments | v1.0 | — | Complete | 2026-05-15 |
+| 6. Background Worker | v1.0 | 2/2 | Complete | 2026-05-15 |
+| 7. Deploy v1 | v1.0 | — | Complete | 2026-05-15 |
+| 8. Social Sign-in | v2.0 | 5/5 | Complete | 2026-05-15 |
+| 9. Etapes | v2.0 | 4/4 | Complete | 2026-05-15 |
+| 10. Events | v2.0 | 3/3 | Complete | 2026-05-16 |
+| 11. Individual Planning | v2.0 | 2/2 | Complete | 2026-05-16 |
+| 12. Native Tablo Chat | v2.0 | 3/3 | Complete | 2026-05-16 |
+| 13. Design System Foundation | v3.0 | 5/5 | Complete | 2026-05-16 |
+| 14. Auth Pages | v3.0 | 2/2 | Complete | 2026-05-16 |
+| 15. Dashboard & Tablos | v3.0 | 3/3 | Complete | 2026-05-16 |
+| 16. Tablo Detail | v3.0 | 4/4 | Complete | 2026-05-17 |
+| 17. Chat & Planning | v3.0 | 2/2 | Complete | 2026-05-17 |
+| 18. App Shell & Navigation | v4.0 | 0/3 | Pending | — |
+| 19. Tablo List Revamp | v4.0 | 0/3 | Pending | — |
+| 20. Tablo Detail & Kanban | v4.0 | 3/3 | Complete | 2026-05-18 |
+| 21. Task Grid & Roadmap Views | v4.0 | — | Pending | — |
+| 22. Calendar Rework | v4.0 | — | Pending | — |
diff --git a/.planning/STATE.md b/.planning/STATE.md
new file mode 100644
index 0000000..3c6eb13
--- /dev/null
+++ b/.planning/STATE.md
@@ -0,0 +1,209 @@
+---
+gsd_state_version: 1.0
+milestone: v4.0
+milestone_name: Figma Design Parity
+status: ready_to_plan
+last_updated: 2026-05-18T14:04:17.721Z
+last_activity: 2026-05-18 -- Phase 20 execution started
+progress:
+ total_phases: 5
+ completed_phases: 2
+ total_plans: 9
+ completed_plans: 9
+ percent: 40
+stopped_at: Phase 20 complete (3/3) — ready to discuss Phase 21
+---
+
+# STATE
+
+**Project:** Xtablo Go+HTMX Product
+**Milestone:** v3.0 — Design System & Visual Polish
+**Created:** 2026-05-14
+
+## Project Reference
+
+See: `.planning/PROJECT.md` (updated 2026-05-17)
+
+**Core value:** A user can sign in and run the Tablos workflow — organize work, attach files, discuss, and plan scheduled events — without a JS framework or managed chat provider.
+**Current focus:** Phase 21 — task grid & roadmap views
+
+## Current Position
+
+Phase: 21
+Plan: Not started
+Status: Ready to plan
+Last activity: 2026-05-18
+
+## Previous Milestone Status
+
+| # | Phase | Status |
+|---|-------|--------|
+| 8 | Social Sign-in | ✓ Complete |
+| 9 | Etapes | ✓ Complete |
+| 10 | Events | ✓ Complete |
+| 11 | Individual Planning | ✓ Complete |
+| 12 | Native Tablo Chat | ✓ Complete |
+
+## Verification Record
+
+- 2026-05-15: Phase 8 execution complete. Local verification passed with `go test ./... -count=1`; database-backed integration coverage skips unless `TEST_DATABASE_URL` is configured.
+- 2026-05-15: Phase 9 UAT complete. Five browser checkpoints passed after fixing the selected-etape create gap; backend verification passed with `TEST_DATABASE_URL='postgres://xtablo:xtablo@localhost:5432/xtablo?sslmode=disable' go test ./... -count=1`.
+- 2026-05-16: Phase 10 execution complete. Events UAT approved; backend verification passed with `TEST_DATABASE_URL='postgres://xtablo:xtablo@localhost:5432/xtablo?sslmode=disable' go test ./... -count=1`.
+- 2026-05-16: Phase 12 execution complete. Discussion realtime UAT approved after duplicate-row and composer-reset fixes; backend verification passed with `just generate` and `TEST_DATABASE_URL='postgres://xtablo:xtablo@localhost:5432/xtablo?sslmode=disable' go test ./... -count=1`.
+- 2026-05-16: Milestone v2.0 archived after acknowledging 7 legacy/open artifact items as deferred.
+
+## Deferred Items
+
+Items acknowledged and deferred at v3.0 milestone close on 2026-05-17 (all pre-existing from v1.0/v2.0):
+
+| Category | Item | Status |
+|----------|------|--------|
+| uat_gap | Phase 05: 05-HUMAN-UAT.md | partial; 6 pending scenarios |
+| uat_gap | Phase 07: 07-HUMAN-UAT.md | partial; 3 pending scenarios |
+| verification_gap | Phase 03: 03-VERIFICATION.md | human_needed |
+| verification_gap | Phase 04: 04-VERIFICATION.md | human_needed |
+| verification_gap | Phase 05: 05-VERIFICATION.md | human_needed |
+| verification_gap | Phase 07: 07-VERIFICATION.md | human_needed |
+
+## Decisions
+
+- **Consolidated internal/auth package** (not split with internal/session) — RESEARCH Open Question 3
+- **compose Postgres + schema isolation for tests** (not testcontainers-go) — RESEARCH Open Question 1
+- **goose.SetTableName per test-schema** prevents public goose_db_version table collision
+- **argon2id** over bcrypt for password hashing — D-08, confirmed by user
+- **Hand-rolled PHC encode/decode** (Pattern 1 verbatim) — no alexedwards/argon2id wrapper dep; keeps code lean and self-tested
+- **Store.now injectable field** (not method/interface) — simpler for single-test-clock use case in MaybeExtend threshold tests
+- **sha256.Sum256 inlined at Create + Lookup** (not in helper) — satisfies >= 2 grep acceptance criterion; makes D-05 hashing visible at usage sites
+- **SignupForm/SignupErrors in templates package** (not handlers or separate forms pkg) — avoids import cycle between templates and internal/web
+- **setupTestDB duplicated into web package test file** — Go prohibits importing _test.go files across packages; duplication chosen over non-test shared infra
+- **nil-Store guard in auth.ResolveSession** — enables Phase 1 unit tests to pass AuthDeps{} zero value without panic
+- **errInvalidCreds const** (not inline string) — satisfies D-20 single-source-of-truth grep gate; both credential failure paths use the constant
+- **NewLimiterStoreWithClock exported** — cross-package integration tests inject a frozen clock without a test-helper shim
+- **Status 401 for non-HTMX credential failures** — consistent with HTTP semantics; HTMX path returns 200+fragment
+- **Layout takes *auth.User explicitly** (not thread-through-ctx) — easier to test templates in isolation; no magic context values
+- **TestIndex_RendersHxGet rewritten to TestIndex_UnauthRedirects** — GET / is now protected; Phase 1 test was a false positive after this plan
+- **csrf.PlaintextHTTPRequest(r) in dev mode** — skips TLS Referer check in plain-HTTP local dev/httptest; production unaffected
+- **trustedOrigins variadic arg in auth.Mount + NewRouter** — test routers pass "localhost"; production passes none
+- **sqlc-generated files not committed** — backend/.gitignore excludes internal/db/sqlc/*.go; just generate reproduces them (03-01)
+- **NewRouter extended with TablosDeps parameter** — second explicit deps param before csrfKey; all call sites updated (03-01)
+- **index_templ.go deleted manually after emptying index.templ** — templ generates broken import when .templ has no components (03-02)
+- **Non-HTMX validation error re-fetches tablos and renders full dashboard** — no form state threading; simpler than threading form through full page (03-02)
+- **_zone hidden field (title|desc) in edit fragments** — unified POST /tablos/{id} handler reads _zone to select which display fragment to return; misuse only affects response body, not DB state (03-03)
+- **loadOwnedTablo helper for all parametric handlers** — uuid.Parse + GetTabloByID + ownership check factored into single function; 404 on any failure, 403 never used (D-04) (03-03)
+- **TabloDeleteButtonFragment canonical delete-zone** — TabloCard delegates to it; single source of truth for zone HTML across card and detail-page contexts (03-03)
+- **task_status ENUM order matches visual column order** (todo, in_progress, in_review, done) — ENUM ordinal sorting aligns with kanban column display order per Pitfall 6 (04-01)
+- **Down migration drops TABLE before TYPE** — tasks table dropped before task_status type per Pitfall 3; type cannot be dropped while still referenced (04-01)
+- **TasksDeps stub in test file** — declared in handlers_tasks_test.go; moved to handlers_tasks.go in Plan 02 to avoid file dependency before handlers exist (04-01)
+- **TaskColumns/TaskColumnLabels in templates package** — moved from web package to templates to avoid web↔templates import cycle; handlers reference via templates.TaskColumns (04-02)
+- **TabloDetailPage accepts tasks []sqlc.Task** — kanban board embedded below tablo header; TabloDetailHandler fetches tasks via ListTasksByTablo before rendering (04-02)
+- **Dual reorder payload** — TaskReorderHandler supports array form (task_id[]/task_col[]) and single-value form (task_id/status/position) for test scaffold + Sortable.js compatibility (04-03)
+- **GetTaskByID before UpdateTask in reorder** — preserves title+description (T-04-08), validates task-to-tablo ownership at fetch time (T-04-10) (04-03)
+- **fileQuerier interface in OrphanCleanupWorker** — enables mock injection for pure unit tests without real DB; pool field retained for production (06-01)
+- **river deps as // indirect until Plan 02** — cmd/worker wiring in Plan 02 will promote river to direct dependency; expected Go module behavior (06-01)
+- **signal.NotifyContext after rivermigrate and S3 init** — startup I/O uses context.Background(); signal context created after all I/O succeeds; prevents river.Client.Start receiving a pre-cancelled context (RESEARCH Pitfall 2) (06-02)
+- **pool.Close() explicit after StopAndCancel, not via defer** — StopAndCancel fully returns before pool is closed; matches PATTERNS.md pool close ordering (06-02)
+- **Social-only users use NULL password_hash** — email/password login rejects them through the generic invalid-credentials path while provider sign-in owns the session path (08-01)
+- **Verified provider identity links by provider subject first, then verified email** — prevents duplicate local users while keeping provider subject as the strongest identity key (08-02)
+- **Apple callback uses GET/query response mode for the first working version** — keeps the callback inside existing CSRF middleware boundaries; Apple dashboard configuration must match (08-03)
+- **Provider buttons degrade to disabled controls when config is missing** — auth pages remain deployable without provider credentials (08-04)
+- **Account providers page is read-only in Phase 8** — linked identity visibility shipped before unlink/add-password account management (08-05)
+- **Apple sign-in disabled after UAT scope change** — Apple controls are hidden, Apple auth routes are not mounted, and provider docs only cover Google for now (08-UAT)
+- **Google button width: max-content (not 100%)** — matches Google Material Design branding guidelines; pill button naturally sized, not stretched to card width (14-02)
+- **Roboto font loaded in auth_layout.templ only** — auth pages are the only gsi-material-button context; global stylesheet pollution avoided (14-02)
+- **AuthProviderButtonsBlock/AuthProviderButtonControl removed** — superseded by GoogleButton from auth_components.templ; single-provider design, components consolidated (14-02)
+- **Air dev .css watching added** — .css in include_ext + Tailwind in rebuild cmd; CSS edits now trigger reload without manual restart (14-02)
+
+## Performance Metrics
+
+| Phase | Plan | Duration | Tasks | Files |
+|-------|------|----------|-------|-------|
+| 02-authentication | 01 | ~10min | 3 | 9 |
+| 02-authentication | 02 | ~8min | 2 | 4 |
+| 02-authentication | 03 | ~15min | 2 | 5 |
+| 02-authentication | 04 | ~25min | 2 | 11 |
+| 02-authentication | 05 | ~7min | 2 | 9 |
+| 02-authentication | 06 | ~12min | 1 | 9 |
+| 02-authentication | 07 | ~25min | 1 | 18 |
+| 03-tablos-crud | 01 | ~15min | 3 | 10 |
+| 03-tablos-crud | 02 | ~4min | 3 | 8 |
+| 03-tablos-crud | 03 | ~30min | 3 | 5 |
+| 04-tasks-kanban | 01 | ~4min | 3 | 7 |
+| 04-tasks-kanban | 02 | ~20min | 3 | 12 |
+| Phase 04-tasks-kanban P03 | ~15min | 3 tasks | 3 files |
+| 06-background-worker | 01 | ~15min | 2 | 9 |
+| Phase 06-background-worker P01 | ~15min | 2 tasks | 9 files |
+| 06-background-worker | 02 | ~10min | 2 | 3 |
+| Phase 08 P01 | 25min | 2 tasks | 8 files |
+| Phase 08 P02 | 35min | 2 tasks | 9 files |
+| Phase 08 P03 | 30min | 2 tasks | 7 files |
+| Phase 08 P04 | 20min | 2 tasks | 8 files |
+| Phase 08 P05 | 20min | 2 tasks | 7 files |
+| Phase 11 P01 | ~15min | 2 tasks | 15 files |
+| Phase 11 P02 | ~20min | 2 tasks | 4 files |
+| Phase 12 P01 | ~25min | 3 tasks | 22 files |
+| Phase 12 P02 | ~25min | 2 tasks | 5 files |
+| Phase 12 P03 | ~45min | 4 tasks | 9 files |
+| Phase 14 P01 | 4min | 2 tasks | 5 files |
+| Phase 14 P02 | ~30min | 2 tasks | 4 files |
+| Phase 17-chat-planning P02 | 15min | 3 tasks | 6 files |
+
+## Notes
+
+- Existing `go-backend/` is set aside; new code lives in a fresh `backend/` Go package.
+- DB schema is changing from the JS/Supabase version — user is in the loop on every schema decision (Phases 2–5).
+- Phase 2 Plan 01 SUMMARY: `.planning/phases/02-authentication/02-01-SUMMARY.md`
+- Phase 2 Plan 02 SUMMARY: `.planning/phases/02-authentication/02-02-SUMMARY.md`
+- Phase 2 Plan 03 SUMMARY: `.planning/phases/02-authentication/02-03-SUMMARY.md`
+- Commits (02-01): 513044d (migration), 799c260 (sqlc), 2c84f42 (auth package + test harness)
+- Commits (02-02): 3bb3828 (RED tests), ee36a5c (GREEN implementation)
+- Commits (02-03): fd2301d (session store + cookie helpers), 1d07830 (middleware)
+- Commits (02-04): 73935ed (signup templates + smoke tests), efdc16b (handler + router + integration tests)
+- Phase 2 Plan 04 SUMMARY: `.planning/phases/02-authentication/02-04-SUMMARY.md`
+- Commits (02-05): b5c20c7 (LimiterStore + tests), 7d8c498 (login handler + integration tests)
+- Phase 2 Plan 05 SUMMARY: `.planning/phases/02-authentication/02-05-SUMMARY.md`
+- Commits (02-06): b5c3fc4 (RED tests), 8b54ff4 (logout + protected routes + layout)
+- Phase 2 Plan 06 SUMMARY: `.planning/phases/02-authentication/02-06-SUMMARY.md`
+- Commits (02-07): ae2d356 (RED tests), 389e1bc (GREEN csrf implementation)
+- Phase 2 Plan 07 SUMMARY: `.planning/phases/02-authentication/02-07-SUMMARY.md`
+- Phase 3 Plan 01 SUMMARY: `.planning/phases/03-tablos-crud/03-01-SUMMARY.md`
+- Commits (03-01): f1b8d6e (migration + queries), c8f44b1 (TablosDeps stub + test scaffold), 2c1b186 (button CSS variants)
+- Phase 3 Plan 02 SUMMARY: `.planning/phases/03-tablos-crud/03-02-SUMMARY.md`
+- Commits (03-02): 43ddf25 (tablos templates + layout footer), 5db9215 (tablo handlers + router wiring), c08da7f (delete retired index.templ)
+- 03-02 complete — all 3 tasks done including human-verify checkpoint approval
+- Phase 3 Plan 03 SUMMARY: `.planning/phases/03-tablos-crud/03-03-SUMMARY.md`
+- Commits (03-03): 6f167e2 (detail/edit/delete templates), ab6937c (handlers + router + all 10 TABLO tests green), b5fa318 (checkpoint approved)
+- **Phase 3 complete** — All 3 plans done; TABLO-01..06 closed; 10/10 TABLO tests green; full browser verify passed 2026-05-15
+- Phase 4 Plan 01 SUMMARY: `.planning/phases/04-tasks-kanban/04-01-SUMMARY.md`
+- Commits (04-01): c9c8262 (migration + sqlc queries), 8b9543d (RED test scaffold + form structs), 55fb32f (Sortable.js + soft-danger CSS)
+- Phase 4 Plan 02 SUMMARY: `.planning/phases/04-tasks-kanban/04-02-SUMMARY.md`
+- Commits (04-02): 181ae79 (handlers + router + main.go), 889164b (templates + tablos.templ + layout.templ), 92ebb5f (activate task tests)
+- Phase 4 Plan 03 SUMMARY: `.planning/phases/04-tasks-kanban/04-03-SUMMARY.md`
+- Commits (04-03): 2b299e2 (TaskEditHandler + TaskUpdateHandler + TaskEditFragment + Sortable.js init), 5f87d7e (TaskReorderHandler + reorder test skips removed), f6deb87 (TestTaskOrderPersists active — full suite green)
+- Phase 6 Plan 01 SUMMARY: `.planning/phases/06-background-worker/06-01-SUMMARY.md`
+- Commits (06-01): 62e5e3e (river dep + ListOrphanFiles sqlc query), a1c2828 (internal/jobs package + unit tests)
+- Phase 6 Plan 02 SUMMARY: `.planning/phases/06-background-worker/06-02-SUMMARY.md`
+- Commits (06-02): 6e70478 (cmd/worker full river wiring), e202ad3 (just worker target + README docs)
+- Phase 8 Plan 01 SUMMARY: `.planning/phases/08-social-sign-in/08-01-SUMMARY.md`
+- Phase 8 Plan 02 SUMMARY: `.planning/phases/08-social-sign-in/08-02-SUMMARY.md`
+- Phase 8 Plan 03 SUMMARY: `.planning/phases/08-social-sign-in/08-03-SUMMARY.md`
+- Phase 8 Plan 04 SUMMARY: `.planning/phases/08-social-sign-in/08-04-SUMMARY.md`
+- Phase 8 Plan 05 SUMMARY: `.planning/phases/08-social-sign-in/08-05-SUMMARY.md`
+- Commits (08): 2d004cd (social identity schema foundation), 6779663 (Google sign-in), a8b6a03 (Apple implementation later disabled), 59fd6b1 (auth page provider controls), 6e65836 (account providers view + docs)
+- Phase 9 Plan 01 SUMMARY: `.planning/phases/09-etapes/09-01-SUMMARY.md`
+- Phase 9 Plan 02 SUMMARY: `.planning/phases/09-etapes/09-02-SUMMARY.md`
+- Phase 9 Plan 03 SUMMARY: `.planning/phases/09-etapes/09-03-SUMMARY.md`
+- Phase 9 Plan 04 SUMMARY: `.planning/phases/09-etapes/09-04-SUMMARY.md`
+- Phase 9 UAT: `.planning/phases/09-etapes/09-UAT.md` — 5/5 checkpoints passed after selected-etape create fix.
+- Commits (09): a8a3e5f/565bb88 (first etape slice), 9b89282/4af623a (management), 9f6c7eb/b22d79d (assignment selector), 55263e4/3a3ecf5/cf07c29 (reorder hardening), 0c95049/ee62ff9/f9fc7a1 (UAT fixes)
+
+- Phase 14 Plan 01 SUMMARY: `.planning/phases/14-auth-pages/14-01-SUMMARY.md`
+- Phase 14 Plan 02 SUMMARY: `.planning/phases/14-auth-pages/14-02-SUMMARY.md`
+- Commits (14-01): cf116ff (logo assets + auth.css), e4d5f96 (auth_components.templ + auth_layout.templ)
+- Commits (14-02): 808eaec (auth_login.templ), 65e3dbf (auth_signup.templ), 4624fb3 (Roboto + icon fix), 70fe384 (button width fix), 6e64cfb (air CSS watch)
+
+---
+*Last updated: 2026-05-16 after Phase 14 Plan 02 execution complete*
+
+## Operator Next Steps
+
+- Start the next milestone with /gsd-new-milestone
diff --git a/.planning/codebase/ARCHITECTURE.md b/.planning/codebase/ARCHITECTURE.md
new file mode 100644
index 0000000..d237a89
--- /dev/null
+++ b/.planning/codebase/ARCHITECTURE.md
@@ -0,0 +1,162 @@
+# Architecture
+
+_Last updated: 2026-05-14_
+
+This document describes the high-level architecture of the `xtablo-source` monorepo: how apps, packages, and external services fit together, the dominant data-flow patterns, and where the key abstractions live.
+
+## High-Level Diagram
+
+```
+ +-----------------------------------------------------+
+ | Frontend Apps |
+ | |
+ | apps/main apps/external apps/clients |
+ | (dashboard) (booking widget) (client portal)|
+ | apps/admin |
+ | (internal admin) |
+ +------+----------------+--------------------+--------+
+ | | |
+ | shared packages (source-only) |
+ | @xtablo/shared @xtablo/ui |
+ | @xtablo/shared-types |
+ | @xtablo/auth-ui @xtablo/chat-ui |
+ | @xtablo/tablo-views |
+ | |
+ v v
+ +-----------------+ +---------------------+
+ | apps/api |<------------>| apps/chat-worker |
+ | (Hono REST) | | (CF Durable Obj) |
+ +--------+--------+ +----------+----------+
+ | |
+ +----------------+----------------+-----------------+
+ | | | |
+ v v v v
+ Supabase Stripe Cloudflare R2 Stream Chat
+ (Postgres+Auth) (payments) (file storage) (messaging)
+ |
+ + Datadog (RUM/APM)
+ + Google Secret Manager
+```
+
+## Application Layers
+
+- **Frontend dashboard** (`apps/main`): primary authenticated SPA. Tablos, planning, events, chat, notes, billing. Entry: `apps/main/src/main.tsx`, root component `apps/main/src/App.tsx`.
+- **Public booking widget** (`apps/external`): embeddable / floating booking widget. Entry: `apps/external/src/main.tsx`. Query params drive mode (`?mode=embed&eventTypeId=...`).
+- **Client portal** (`apps/clients`): public-facing client portal experience. Entry: `apps/clients/src/main.tsx`, routes in `apps/clients/src/routes.tsx`.
+- **Admin app** (`apps/admin`): internal admin tools. Entry: `apps/admin/src/main.tsx`, routes in `apps/admin/src/routes.tsx`.
+- **API** (`apps/api`): Hono-based REST API serving all frontends. Entry: `apps/api/src/index.ts` (compiled to Node, deployed to Google Cloud Run).
+- **Chat worker** (`apps/chat-worker`): Cloudflare Worker with Durable Objects for real-time chat presence. Entry: `apps/chat-worker/src/index.ts`.
+
+## Data Flow Patterns
+
+### React Query (TanStack Query v5) — primary server-state tool
+
+All server state flows through React Query. Default cache time is 5 minutes. Query keys follow a hierarchical convention so that mutations can invalidate just the affected sub-tree:
+
+```ts
+["tablos"] // list
+["tablos", tabloId] // single
+["tablo-files", tabloId] // related collection
+```
+
+Hooks live in:
+- `apps/main/src/hooks/` — feature hooks (`tablos.ts`, `events.ts`, `tasks.ts`, `availabilities.ts`, `stripe.ts`, `notes.ts`, ...).
+- `packages/shared/src/hooks/` — cross-app hooks (`auth.ts`, `book.ts`, `public.ts`).
+
+### Zustand — global client state
+
+Used sparingly, primarily for the current user. The user is fetched via React Query then mirrored into a Zustand store so any component can read it synchronously:
+- `useUser()` — throws if no session (use inside protected routes).
+- `useMaybeUser()` — returns null if unauthenticated (use in route guards / public-aware components).
+
+Provider: `apps/main/src/providers/UserStoreProvider.tsx` (mirrored in `apps/external/src/UserStoreProvider.tsx`).
+
+### Direct Supabase queries vs API calls
+
+Two parallel data-access patterns coexist:
+
+1. **Direct Supabase** (`supabase.from("table").select()...`) — used from frontend hooks when row-level security is sufficient and no server-side logic is required. Client lives in `packages/shared/src/lib/supabase.ts` (re-exported via `apps/main/src/lib/supabase.ts`).
+2. **API calls** (`api.get("/api/v1/...")`) — used when the operation needs the service role key, must run server-side logic (Stripe, file ops, email, multi-tenant integrity), or aggregates data. The HTTP client wrapper lives in `packages/shared/src/lib/api.ts` (re-exported via `apps/main/src/lib/api.ts`) and attaches the Supabase JWT as a Bearer token.
+
+File operations use specialized mutation hooks (e.g. `useUploadTabloFile`, `useDeleteTabloFile`) that invalidate `["tablo-files", tabloId]` automatically.
+
+## Authentication Flow
+
+- **Supabase Auth** issues JWTs on login / passwordless flows.
+- **SessionContext** (`packages/shared/src/contexts/SessionContext.tsx`) subscribes to `supabase.auth.onAuthStateChange()` and exposes the current session.
+- Frontend HTTP client reads the session token and sends `Authorization: Bearer ` on every API call.
+- The API's `supabase` middleware validates the JWT and attaches the resolved user / supabase clients to the Hono context.
+- **Passwordless onboarding** generates temporary accounts flagged with `is_temporary: true`.
+- **Protected routes** check `useMaybeUser()` and redirect to landing when null.
+- **Client portal** has a parallel auth path: magic-link based, signed cookies issued by `apps/api/src/routers/clientAuth.ts`. Configurable TTLs, cookie domain, JWT secret are passed into the router factory.
+
+## API Architecture (Hono)
+
+Entry: `apps/api/src/index.ts`. Flow:
+
+1. `loadSecrets()` pulls secrets (locally from env, in staging/prod from Google Secret Manager) — `apps/api/src/secrets.ts`.
+2. `createConfig(secrets)` produces the typed `AppConfig` — `apps/api/src/config.ts`.
+3. `MiddlewareManager.initialize(config)` constructs the middleware singleton — `apps/api/src/middlewares/middleware.ts`.
+4. The root Hono app applies `logger()` and a CORS middleware that only accepts `*.xtablo.com` and `localhost` origins.
+5. All routes mount under `/api/v1` via `getMainRouter(config)` (`apps/api/src/routers/index.ts`).
+
+### Middleware Manager (singleton pattern)
+
+`MiddlewareManager` builds each piece of middleware once on init and exposes them as instance properties. The main router pulls them via `MiddlewareManager.getInstance()` and chains them in this fixed order:
+
+```
+supabase -> r2 -> transporter -> stripe -> stripeSync
+```
+
+Auxiliary middleware modules:
+- `apps/api/src/middlewares/middleware.ts` — central singleton and supabase / r2 / email / stripe middlewares.
+- `apps/api/src/middlewares/stripeSync.ts` — bidirectional Supabase <-> Stripe sync engine.
+- `apps/api/src/middlewares/transporter.ts` — email transporter.
+
+### Router ordering
+
+Public-first, then authenticated. From `apps/api/src/routers/index.ts`:
+
+1. `/public` — unauthenticated (`public.ts`).
+2. `/tasks` — task router (`tasks.ts`).
+3. `/revenuecat-webhook`, `/stripe-webhook` — webhook receivers.
+4. `/admin` — admin-only routes (`admin.ts`, `adminAuth.ts`, ...).
+5. `/client-auth`, `/client-portal`, `/client-invites` — client portal stack.
+6. `/` — `maybeAuthRouter.ts` (optional auth — must come before authed to allow public booking).
+7. `/` — `authRouter.ts` (requires JWT).
+
+The exported `ApiRoutes` type (`ReturnType`) enables Hono RPC clients to consume the API in a type-safe way.
+
+## Key Abstractions
+
+- **`packages/shared`** is the central runtime sharing point. Re-exports cover contexts (`SessionContext`, `ThemeContext`), cross-app hooks, the API client, the Supabase client, toast helpers, and shared types. Public surface in `packages/shared/src/index.ts`.
+- **`packages/ui`** — Radix + Tailwind component library. Source-only. Components in `packages/ui/src/components/` (`button.tsx`, `dialog.tsx`, `select.tsx`, ...).
+- **`packages/shared-types`** — zero-runtime-dependency TypeScript types. Auto-generated `database.types.ts` + hand-written domain layers (`tablos.types.ts`, `tablo-data.types.ts`, `events.types.ts`, `kanban.types.ts`, `stripe.types.ts`, `admin.types.ts`).
+- **`packages/auth-ui`, `packages/chat-ui`, `packages/tablo-views`** — feature-scoped UI packages, also source-only.
+- **Query keys** — convention enforced by colocation in `apps/main/src/hooks/` and feature naming (`["tablos", id]`, `["tablo-files", id]`, etc.).
+
+## Source-Only Package Pattern
+
+`@xtablo/shared`, `@xtablo/ui`, `@xtablo/shared-types`, `@xtablo/auth-ui`, `@xtablo/chat-ui`, and `@xtablo/tablo-views` export TypeScript directly with no build step. Consumers import source files; Vite handles transpilation and HMR. Benefits: instant updates, no watch processes, simpler dependency graph. Constraint: no circular dependencies between packages, and the API can only depend on `@xtablo/shared-types` (pure types, no React).
+
+## Entry Points
+
+| App / package | Entry file |
+|----------------------|---------------------------------------------------------|
+| `apps/main` | `apps/main/src/main.tsx` -> `App.tsx` |
+| `apps/external` | `apps/external/src/main.tsx` -> `routes.tsx` |
+| `apps/clients` | `apps/clients/src/main.tsx` -> `App.tsx` / `routes.tsx` |
+| `apps/admin` | `apps/admin/src/main.tsx` -> `routes.tsx` |
+| `apps/api` | `apps/api/src/index.ts` -> `routers/index.ts` |
+| `apps/chat-worker` | `apps/chat-worker/src/index.ts` |
+| `@xtablo/shared` | `packages/shared/src/index.ts` |
+| `@xtablo/ui` | `packages/ui/src/components/index.ts` |
+| `@xtablo/shared-types` | `packages/shared-types/src/index.ts` |
+
+## Build & Deployment Notes
+
+- **Turborepo** orchestrates tasks with caching (`turbo.json` at repo root).
+- `apps/main` and other Vite apps deploy to **Cloudflare Workers** via `wrangler.toml` and the bundled `worker/` folder.
+- `apps/api` compiles TypeScript and deploys to **Google Cloud Run**; secrets resolved via Google Secret Manager.
+- `apps/chat-worker` deploys as a **Cloudflare Worker with Durable Objects**.
+- Observability: Datadog RUM on frontends (`apps/main/src/lib/rum.ts`), `dd-trace` on the API (initialized at the top of `apps/api/src/index.ts`).
diff --git a/.planning/codebase/CONCERNS.md b/.planning/codebase/CONCERNS.md
new file mode 100644
index 0000000..6b0b97a
--- /dev/null
+++ b/.planning/codebase/CONCERNS.md
@@ -0,0 +1,126 @@
+# Codebase Concerns Map
+
+> Generated 2026-05-14. Catalogs tech debt, security considerations, fragile zones, and known gotchas across the `xtablo-source` Turborepo. Pointers use repo-relative paths; line numbers are accurate as of this scan.
+
+## TODOs / FIXMEs / HACKs / XXX
+
+Grep of `apps/` + `packages/` (excluding `node_modules`, `dist`) — TODO markers are sparse, suggesting either healthy hygiene or undocumented debt.
+
+- `apps/api/src/index.ts:67` — `// TODO: Add health check endpoint`. No `/healthz` for Cloud Run, which complicates liveness/readiness probes.
+- `apps/api/src/routers/invite.ts:26` — `// TODO: Verify that the owner_id is correct`. Authorization gap on invite creation.
+- `apps/api/src/routers/invite.ts:128` — `// TODO: Verify that the event start and end correspond to a slot`. Booking integrity not enforced server-side.
+- `apps/main/src/components/WebcalModal.tsx:125` — `{/* TODO: Add webcal URL */}` — feature placeholder shipping incomplete.
+- `apps/main/src/lib/rum.ts:25,31` — Datadog RUM session sampling rates are commented `// TODO: Uncomment when we have enough data` — observability runs in a partial state.
+- `apps/api/src/routers/user.ts:54` — `// Deprecated: name field is deprecated, use first_name and last_name instead` — dual fields still flowing through APIs.
+
+No `FIXME`, `HACK`, or `XXX` markers found in `apps/` or `packages/` source — but absence is not assurance; many concerns live under euphemisms (see `@deprecated` below).
+
+`@deprecated` markers:
+- `apps/main/src/hooks/user.ts:16` — `useUser` hook deprecated in favor of `useSession` from `SessionContext`. Likely still has callers.
+
+## Known issues (from docs)
+
+The `docs/` directory contains 35+ retrospective `*_FIX.md`, `*_SETUP.md`, and migration notes. No single `TROUBLESHOOTING.md` exists; institutional knowledge is scattered. Notable items:
+
+- `docs/MIDDLEWARE_INITIALIZATION_FIX.md` — November 2025 incident: routers called `MiddlewareManager.getInstance()` at module-load time before `initialize()` ran. Fixed by passing the manager into router factories, but the singleton's eager-throw API (`getInstance()` throws if not initialized) remains a footgun for any new router that forgets the pattern.
+- `docs/STRIPE_SECURITY_FIX.md` — Migration 37: `public.active_subscriptions` view exposed all users' subscription data without RLS. Replaced with `SECURITY DEFINER` function `get_my_active_subscription()`. Future views need similar audit.
+- `docs/STRIPE_MIGRATION_36.md`, `STRIPE_WITH_SYNC_ENGINE.md`, `STRIPE_FINAL_SETUP.md`, `STRIPE_IMPLEMENTATION_SUMMARY.md`, `STRIPE_CLEANUP_*` — eleven Stripe documents indicate repeated rework on the billing surface.
+- `docs/TEST_FIXES.md`, `docs/TEST_ROUTER_REFACTOR.md`, `docs/API_TESTS.md`, `docs/MIDDLEWARE_TESTS.md` — test setup has needed periodic refactoring; mocks are tightly coupled to the singleton.
+- `SECURITY_NOTICE.md` (repo root) — `.env` files were previously committed to git history. The checklist of credentials to rotate (Supabase service role, Stripe, Stream, Google OAuth, R2) is in the file; verification that rotation occurred is not tracked here.
+
+## Security considerations
+
+### JWT handling
+- Validation occurs in `apps/api/src/helpers/auth.ts` via `validateAuthHeader` + Supabase `auth.getUser(token)`. Wired into `authMiddleware` / `maybeAuthenticatedMiddleware` in `apps/api/src/middlewares/middleware.ts`.
+- `apps/main/src` uses `jwt-decode` (^4.0.0) to inspect tokens client-side — purely decode, no verification (correct), but any code path that *trusts* decoded claims for authorization would be a bug.
+- Client portal sessions use a separate JWT (`CLIENT_AUTH_JWT_SECRET`, `apps/api/src/helpers/clientSessions.ts`) with cookie storage (`CLIENT_AUTH_COOKIE_NAME`). Two independent token systems = two attack surfaces.
+- Admin sessions live in `apps/api/src/helpers/adminTokens.ts` signed by `ADMIN_TOKEN_SIGNING_SECRET` — a third token surface.
+
+### Service role key usage (RLS bypass)
+The Supabase service role key is created exactly once in `apps/api/src/middlewares/middleware.ts:164` and injected into every authenticated request via `supabaseMiddleware`. This means **every API handler operates with full RLS bypass**. Authorization logic therefore must live in handler code; any forgotten ownership check is a data-exposure bug. Notable areas relying on handler-side checks:
+- `apps/api/src/routers/tablo.ts`, `tablo_data.ts`
+- `apps/api/src/routers/admin*.ts` (six admin routers)
+- `apps/api/src/routers/clientPortal.ts`
+- The two `TODO: Verify ... owner_id`/`slot` comments in `invite.ts` are direct evidence of this risk.
+
+### Stripe webhook verification
+- `apps/api/src/routers/stripe.ts:167-191` — verification is delegated to `@supabase/stripe-sync-engine`'s `processWebhook(rawBody, signature)`. The route does retrieve raw body via `c.req.text()` (correct — verification needs unmodified bytes). Note: webhook router is wired pre-auth in `routers/index.ts`, which is required by Stripe — verify any future restructuring preserves this ordering.
+- Webhook secret comes from `STRIPE_WEBHOOK_SECRET` in config; rotation procedure not documented in repo.
+
+### Secrets / env vars
+- Production/staging: loaded from Google Secret Manager via `apps/api/src/secrets.ts` and assembled in `apps/api/src/config.ts`.
+- Dev: `.env` via `dotenv`. `.env*` is now gitignored (see `SECURITY_NOTICE.md`).
+- `apps/api/src/config.ts:32` requires a long list of secrets — `validateEnvVar` throws on missing; good fail-fast, but means a single missing env aborts boot with no partial-feature degradation.
+- Frontend env: `apps/main` reads `VITE_*` env at build time per environment (`build:staging`, `build:prod`). Anything `VITE_*` is bundled into the public JS — only public keys belong here.
+
+## Performance considerations
+
+- **React Query defaults**: `packages/shared/src/lib/api.ts:18` sets a global `staleTime` of 5 minutes. Aggressive caching is appropriate for dashboard data but can hide write-after-read bugs; mutations must explicitly invalidate hierarchical keys (`["tablos", id]`, etc.).
+- **Custom stale times**: `apps/main/src/hooks/stripe.ts:114` (5 min) and `:221` (10 min) — billing data caching for 10 minutes risks displaying a stale subscription state after a webhook arrives. UI should also listen to mutation success or refresh on Stripe-portal-return paths.
+- **Pagination**: ~63 hits for `.select("*")` / `.range(` / `.limit(` across `apps/main/src` + `apps/api/src` (excluding tests). Many list endpoints (`apps/api/src/routers/tablo.ts`, `admin*.ts`) appear to return full tables; no shared cursor/offset helper exists. AG-Grid in main app loads client-side which exacerbates this for orgs with large datasets.
+- **Source-only packages**: `@xtablo/shared`, `@xtablo/ui`, `@xtablo/chat-ui`, `@xtablo/auth-ui`, `@xtablo/tablo-views`, `@xtablo/shared-types` export TS directly. Pros: instant HMR. Cons: every app re-typechecks and re-bundles them; tree-shaking depends on each app's bundler being able to drop unused exports (Vite generally handles this, but barrel files in `packages/shared/src/index.ts`-style modules can defeat it — worth auditing if bundle size matters).
+- **Bundle size**: `apps/main` has `rollup-plugin-visualizer` available for analysis but no tracked size budgets. Heavy deps: `@blocknote/*` (rich text editor), `ag-grid-community` + `ag-grid-react`, `jspdf`, `@datadog/browser-rum*` — all in `dependencies` of `apps/main/package.json`.
+- **Datadog dd-trace** (`apps/api`) is initialized in `apps/api/src/index.ts:13` before everything else; misconfigured tracing has measurable cold-start cost on Cloud Run.
+
+## Fragile areas
+
+### Stripe sync engine (Supabase ↔ Stripe)
+- `apps/api/src/middlewares/stripeSync.ts` instantiates `@supabase/stripe-sync-engine@^0.45.0` with a direct Postgres connection string (`SUPABASE_CONNECTION_STRING`) and base64-encoded CA cert (`SUPABASE_CA_CERT`). Bypasses Supabase API entirely.
+- `revalidateObjectsViaStripeApi: ["subscription", "customer"]` — explicit workaround for stale-data bugs in the sync engine.
+- Schema is `"stripe"` (separate from `public`); migrations must be applied carefully; eleven separate Stripe docs in `docs/` evidence repeated breakage.
+- Sync is *eventually consistent*: UI hooks with 5–10 min `staleTime` (above) can show pre-webhook state.
+
+### Auth flow (passwordless + temporary accounts)
+- Passwordless flow creates accounts flagged `is_temporary: true`. Lifecycle (cleanup of abandoned temp accounts, upgrade path to permanent) is not documented in `docs/`.
+- Three independent token systems (user JWT, client-portal JWT, admin token) live side by side — see Security section. Test reference: `apps/api/src/__tests__/README.md:28` documents a `test_temp@example.com` fixture.
+- `SessionContext` (main app) listens to `supabase.auth.onAuthStateChange()`. Deprecated `useUser` hook at `apps/main/src/hooks/user.ts:16` still exists and likely has stragglers.
+
+### Middleware singleton initialization order
+- `MiddlewareManager` in `apps/api/src/middlewares/middleware.ts` is a singleton with throw-on-misuse semantics. The November 2025 fix (see `docs/MIDDLEWARE_INITIALIZATION_FIX.md`) is a pattern convention, not a structural guarantee. Any new router that calls `MiddlewareManager.getInstance()` at module top-level reintroduces the bug.
+- The `index.ts` router order (`apps/api/src/routers/index.ts`) is load-bearing: public routes → stripe webhook (no auth, raw body) → auth-applied routes. Refactors that reorder these break either auth or signature verification.
+
+## Build / deploy concerns
+
+- **Cloudflare Workers** (`apps/main`, possibly `apps/clients`, `apps/external`, `apps/admin`): `apps/main/wrangler.toml` sets `compatibility_date = "2025-07-09"` and no `compatibility_flags`. Notably **no `nodejs_compat`** — any dep pulling Node built-ins at runtime will fail at the edge. Watch for incidental Node imports in shared packages.
+- **Type generation**: per `CLAUDE.md`, run `npx supabase gen types typescript > packages/shared-types/src/database.types.ts` after schema changes. There's no CI guard that types are up to date; drift between DB and types is silent.
+- **Cache invalidation gotchas** (from `CLAUDE.md` "Important Notes"): stale builds resolved only by `pnpm clean && rm -rf node_modules/.cache && pnpm install && pnpm build`. Indicates Turborepo cache occasionally misses dependency changes — possibly because source-only packages don't declare outputs.
+- **API deploy** uses Cloud Run with Cloud Build (`docs/CLOUD_BUILD_*.md`, `docs/DOCKER_*.md`). Multi-stage pnpm Docker build is documented and has needed multiple optimization passes (`DOCKER_BUILD_PERFORMANCE.md`, `DOCKER_PNPM_OPTIMIZATION.md`, `DOCKER_FIX_SUMMARY.md`).
+- **API uses `tsc` only** (`apps/api/package.json` `build: tsc`) — no bundling, ships `dist/` + `node_modules`. Large image surface; dependency vulnerabilities are deploy-time concerns.
+- **Pre-commit**: `.pre-commit-config.yaml` exists at root; behavior not audited here.
+
+## Dependencies of concern
+
+Pinned/notable in `apps/main/package.json`:
+- `@typescript/native-preview: 7.0.0-dev.20251010.1` — a *preview/nightly* TypeScript native compiler pinned to a dated dev build. High churn risk; may break unexpectedly.
+- `@types/react: 19.0.10`, `@types/react-dom: 19.0.4` — exact-pinned (no caret). `react: 19.0.0` itself also exact-pinned. Upgrades will require coordinated change.
+- `vitest: ^3.2.4` (in `apps/main`) vs `vitest: ^4.0.8` (in `apps/api`) — different major versions across the monorepo; shared test utilities will be incompatible.
+- `@types/react-router-dom: ^5.3.3` listed alongside `react-router-dom: ^7.9.4` — the v5 type stubs are wrong for v7; either unused or actively misleading.
+- `eslint: ^9.22.0` + `@typescript-eslint/*: ^7.0.2` — typescript-eslint v7 predates flat-config-stable ESLint 9; potential plugin compat issues. Note also Biome is the primary linter (`biome.json`), so ESLint may be vestigial.
+- `jest` + `jest-environment-jsdom` + `@types/jest` in `apps/main` devDeps despite using Vitest — dead dependencies inflating install.
+- `pnpm.overrides`: `form-data: ^4.0.4`, `linkifyjs: ^4.3.2` — root-level overrides usually indicate working around a transitive vulnerability or bug; reason isn't documented in repo.
+
+In `apps/api/package.json`:
+- `stripe: ^20.0.0` — Stripe SDK is currently on v17+ as of cutoff; v20 is a future major. Verify lockfile matches expected runtime version.
+- `ts-node: ^10.9.2` listed in `dependencies` (not devDeps) — likely unused at runtime; should be a devDep.
+- `multer: ^2.0.2` — major version 2.x is recent; ensure middleware patterns aren't using legacy 1.x APIs.
+
+`pnpm-lock.yaml` is ~763 KB indicating substantial dependency graph; `pnpm audit` not run as part of this scan.
+
+## Documentation gaps
+
+- **No `TROUBLESHOOTING.md`** despite 35+ retrospective fix docs — there's no central index.
+- **`docs/`** is fix-log heavy and architecture-light. `STRIPE_ARCHITECTURE.md` and `MIDDLEWARE_TESTS.md` exist but most flows lack an "as-built" diagram.
+- **Three token systems** (user JWT, client session JWT, admin token) — no unified auth doc; you must read `apps/api/src/helpers/{auth,clientSessions,adminTokens}.ts` separately.
+- **Temporary accounts** lifecycle (creation, retention, cleanup, upgrade to permanent) — undocumented.
+- **Six admin routers** (`adminActions`, `adminAuth`, `adminDatasets`, `adminOverview`, `adminTables`, `admin.ts`) — admin surface is large and the only docs are `ADMIN_APP_ACCESS_SETUP.md` for access setup, not authorization model.
+- **Frontend bundle budgets** — none defined; `rollup-plugin-visualizer` available but not enforced.
+- **Type-generation workflow** — only mentioned in `CLAUDE.md`; no CI check.
+- **`apps/chat-worker`, `apps/clients`, `apps/admin`, `apps/external`** — minimal architectural docs; main app dominates `CLAUDE.md`.
+- **Legacy directories** at repo root (`backend/` Python, `go-backend/` Go, `frontend_v2/`, `xtablo-expo/`) are present but unmentioned in `CLAUDE.md`. Status (active? abandoned?) is unclear — this itself is a debt signal.
+- **`SECURITY_NOTICE.md`** lists credentials to rotate after the `.env`-in-git incident, but completion status of that rotation checklist is not tracked.
+
+## Tracked-but-unaddressed observations
+
+- 43 `console.error`/`console.warn` calls in `apps/api/src/routers/` — direct logging instead of going through `pino` (which is a devDep but not wired to the routers).
+- 21 explicit `: any` annotations in `apps/api/src` (excluding tests). Two visible examples: `apps/api/src/routers/clientInvites.ts:192` (`(candidate: any) =>`) and `apps/api/src/helpers/helpers.ts:374` (`(u: any) =>`).
+- Only 2 `@ts-ignore`/`@ts-expect-error` comments across `apps/` + `packages/` — TypeScript discipline appears solid where types exist.
diff --git a/.planning/codebase/CONVENTIONS.md b/.planning/codebase/CONVENTIONS.md
new file mode 100644
index 0000000..7c06e8c
--- /dev/null
+++ b/.planning/codebase/CONVENTIONS.md
@@ -0,0 +1,161 @@
+# Code Conventions
+
+**Last updated:** 2026-05-14
+**Scope:** Turborepo monorepo at `/Users/arthur.belleville/Documents/perso/projects/xtablo-source`
+**Apps:** `apps/main`, `apps/external`, `apps/api`, `apps/admin`, `apps/chat-worker`, `apps/clients`
+**Packages:** `packages/shared`, `packages/ui`, `packages/shared-types`, `packages/auth-ui`, `packages/chat-ui`, `packages/tablo-views`
+
+## Linter & Formatter — Biome
+
+The repo uses a single root [Biome](https://biomejs.dev) config: `biome.json` (Biome 2.2.5, pinned in root `package.json` devDependencies).
+
+Formatting rules (from `biome.json`):
+- `indentStyle: "space"`, `indentWidth: 2`
+- `lineEnding: "lf"`, `lineWidth: 100`
+- JS: `quoteStyle: "double"`, `jsxQuoteStyle: "double"`, `semicolons: "always"`, `trailingCommas: "es5"`, `arrowParentheses: "always"`, `bracketSpacing: true`, `bracketSameLine: false`
+- JSON formatter enabled with same indent settings; parser allows comments but not trailing commas
+
+Key lint rules turned on (Biome `recommended: false` — rules are explicit):
+- `suspicious.noExplicitAny: "error"` (overridden to `off` for some files via overrides; warn in `xtablo-expo`)
+- `correctness.noUnusedVariables`, `noUnusedImports`, `noUndeclaredVariables` — all `error`
+- `style.useConst`, `useTemplate`, `noNamespace`, `noCommonJs` — all `error`
+- `complexity.noBannedTypes`, `suspicious.noDebugger`, `suspicious.noEmptyBlockStatements` — all `error`
+
+Per-package scripts wrap Biome:
+- `pnpm lint` → `turbo lint` → each package runs `biome check .`
+- `pnpm lint:fix` → `biome check --write .`
+- `pnpm format` → `biome format --write .`
+
+## TypeScript Conventions
+
+Every package/app ships its own `tsconfig.json`; there is no root TS config.
+
+Strictness — every config sets `strict: true`. Additional flags consistent across configs:
+- `noUnusedLocals: true`, `noUnusedParameters: true`, `noFallthroughCasesInSwitch: true`
+- `noUncheckedIndexedAccess: true` for `packages/shared-types`, `packages/shared`
+- `isolatedModules: true`, `moduleDetection: "force"`, `skipLibCheck: true`
+- API uses `verbatimModuleSyntax: true` (`apps/api/tsconfig.json`) — type-only imports must be explicit
+
+Module systems:
+- Frontend apps use `module: ESNext` + `moduleResolution: "bundler"` (e.g. `apps/main/tsconfig.app.json`)
+- API uses `module: NodeNext` (TypeScript compiled output via `tsc`)
+- All packages declare `"type": "module"` in package.json
+
+Path aliases (defined per app, resolved by Vite via `vite-tsconfig-paths`):
+- `apps/main`: `@ui/*` → `./src/*`, `@xtablo/auth-ui` → `../../packages/auth-ui/src`, `@xtablo/ui/*` → `../../packages/ui/src/*` (see `apps/main/tsconfig.app.json`)
+- `apps/main/tsconfig.json`: also `@external/*` → `src/external/*`
+
+Package import discipline (per `CLAUDE.md`):
+- No circular dependencies between packages
+- `apps/api` may only import from `@xtablo/shared-types`
+- Frontend apps may import from all shared packages
+- `@xtablo/shared` and `@xtablo/ui` are **source-only** — TypeScript is consumed directly via `vite-tsconfig-paths`; no build step
+
+Types-first workflow:
+- Database types are auto-generated into `packages/shared-types/src/database.types.ts` via `npx supabase gen types typescript`
+- Domain types in `@xtablo/shared-types` are derived from `database.types.ts` (nulls removed, refined shapes)
+- API response types live in the same package so frontends and the API agree
+
+## React Component Conventions
+
+- Functional components only (no class components observed)
+- TSX files use named exports for components, e.g. `export function CustomModal(...)` (see `apps/main/src/components/CustomModal.tsx`)
+- Co-located unit tests: `Foo.tsx` + `Foo.test.tsx`
+- Naming suffix conventions (per `CLAUDE.md`):
+ - Dialogs / modals → `*Modal.tsx` (e.g. `CustomModal.tsx`)
+ - Page sections → `*Section.tsx` (e.g. `TabloEventsSection.tsx`)
+ - Card surfaces → `*Card.tsx` (e.g. `EventTypeCard.tsx`, `AvailabilityCard.tsx`, `EventTypeCard.test.tsx`)
+- Shared primitives (Radix + Tailwind) live in `packages/ui/src`
+- Cross-app business hooks/contexts live in `packages/shared/src`
+
+## Hook Patterns
+
+React Query (TanStack Query v5) is the primary server-state tool. Standard return shapes:
+
+```ts
+// Queries
+const { data, isLoading, error } = useMyQuery();
+
+// Mutations
+const { mutate, isPending } = useMyMutation();
+```
+
+- Default cache time: 5 minutes (configured in `packages/shared`'s QueryClient)
+- Mutations invalidate targeted keys explicitly rather than blowing away the whole cache
+
+Zustand handles global client state (notably the authenticated user store):
+- `useUser()` — throws if no session (use inside protected routes)
+- `useMaybeUser()` — returns null if unauthenticated (use in route guards / public surfaces)
+
+## Query Key Conventions
+
+Hierarchical keys, with the resource name first and identifiers cascading deeper:
+
+```ts
+["tablos"] // list
+["tablos", tabloId] // single
+["tablo-files", tabloId] // related collection
+```
+
+Invalidations should match the deepest key that needs to be refreshed.
+
+## Error Handling
+
+- User-facing: `toast.add({ ... })` (toast system in `packages/shared` / `packages/ui`) — messages should be friendly and actionable
+- Technical: `console.error` for developer-only context (stack traces, raw API errors)
+- API errors are caught at the React Query hook layer and surfaced via `error` from `useQuery`/`useMutation`; UI components branch on `isError`
+- Server side (`apps/api`): Hono routers return `c.json({ error: ... }, statusCode)`; middleware handles auth failures with 401s (see `docs/MIDDLEWARE_TESTS.md`)
+
+## Loading States — three levels
+
+1. **Route level** — `ProtectedRoute` shows a full-page spinner while the session resolves
+2. **Feature level** — React Query `isLoading` / `isPending` drives section-level skeletons or spinners
+3. **Action level** — Buttons set `disabled` during the related mutation's `isPending`
+
+Empty / error states are explicit branches (no silent fallbacks).
+
+## Import / Export Patterns
+
+- ESM throughout (`"type": "module"` in every package.json; Biome enforces `noCommonJs`)
+- Source-only packages (`@xtablo/shared`, `@xtablo/ui`) export from `src/index.ts` and are consumed via TS path aliases — no `dist/` involved
+- Compiled packages (`@xtablo/shared-types` and `apps/api`) emit `dist/` via `tsc`; `shared-types` includes `declaration: true` and `declarationMap: true`
+- Type-only imports preferred where supported (`verbatimModuleSyntax` is on for the API)
+- Biome's `noUnusedImports` rule will flag dead imports at lint time
+
+## File Organization
+
+```
+apps//src/
+ components/ # UI components, *.tsx + *.test.tsx
+ contexts/ # React contexts (e.g. UpgradeBlockContext.tsx)
+ providers/ # Store / provider wrappers (e.g. UserStoreProvider.tsx)
+ hooks/ # App-specific hooks
+ pages/ or routes/ # Route entry points
+ lib/ # Utilities, route table, api client setup
+ utils/testHelpers # Render-with-providers wrappers for tests
+packages//src/
+ index.ts # Single barrel export
+ ... # Domain folders mirror app layout
+```
+
+API layout (`apps/api/src`):
+- `routers/` — Hono routers grouped by concern (`public.ts`, `authRouter.ts`, `tablo.ts`, `stripe.ts`, etc.)
+- `middlewares/` — Auth, Supabase, R2, Stream, email injection
+- `helpers/` — Pure logic (testable in isolation, e.g. `helpers/orgIcons.ts` + `orgIcons.test.ts`)
+- `__tests__/` — Test-only fixtures, setup, globalSetup, route + middleware suites
+
+## Type Safety
+
+- `noExplicitAny` is on as `error` in the root config (relaxed to `warn` in `xtablo-expo` and `off` for select API/legacy file overrides)
+- Prefer derived types from `@xtablo/shared-types` over inline shape literals
+- `Database` table types are generated; domain types should narrow them rather than redeclare from scratch
+- `noUncheckedIndexedAccess: true` in shared packages — index access returns `T | undefined`; handle the undefined branch explicitly
+- API enforces `verbatimModuleSyntax`, so `import type { ... }` is required for type-only imports — relevant for compiled API code
+
+## Reference files
+
+- `biome.json` — single source for lint + format
+- `apps/main/tsconfig.app.json` — canonical frontend TS config
+- `apps/api/tsconfig.json` — canonical backend TS config
+- `packages/shared-types/tsconfig.json` — type-only package config
+- `CLAUDE.md` — high-level conventions (this doc expands it with verified specifics)
diff --git a/.planning/codebase/INTEGRATIONS.md b/.planning/codebase/INTEGRATIONS.md
new file mode 100644
index 0000000..211e4a6
--- /dev/null
+++ b/.planning/codebase/INTEGRATIONS.md
@@ -0,0 +1,140 @@
+# INTEGRATIONS
+
+External systems wired into the xtablo monorepo. Last updated 2026-05-14.
+
+## Database — Supabase Postgres
+
+- **Hosted Postgres** managed by Supabase. The frontend talks to it via the JS SDK; the API talks to it via the service-role key and (for `stripe-sync-engine`) a direct `postgres://` connection string.
+- **Service-role client** is constructed per-request inside the API in `apps/api/src/middlewares/middleware.ts` (`supabaseMiddleware`):
+ ```ts
+ createClient(config.SUPABASE_URL, config.SUPABASE_SERVICE_ROLE_KEY)
+ ```
+ Mounted globally for every route in `apps/api/src/routers/index.ts`.
+- **Browser client** factory at `packages/shared/src/lib/supabase.ts` re-exports `createClient`; instances are produced inside `@xtablo/shared` consumers and used directly (`supabase.from("table")...`).
+- **RLS** — bypassed by the API using the service-role key when needed (see `CLAUDE.md` notes); browser clients use the anon key plus the user's JWT and rely on RLS policies.
+- **Direct Postgres** — `SUPABASE_CONNECTION_STRING` (with base64-encoded `SUPABASE_CA_CERT`) is used by the Stripe sync engine `poolConfig` in `apps/api/src/middlewares/stripeSync.ts`.
+- **Schema, migrations, snippets, tests** — `supabase/config.toml`, `supabase/migrations/`, `supabase/snippets/`, `supabase/tests/`.
+- **Type codegen** — regenerate `packages/shared-types/src/database.types.ts` with:
+ ```bash
+ npx supabase gen types typescript > packages/shared-types/src/database.types.ts
+ ```
+ These types are then refined into domain/API types inside `@xtablo/shared-types` (`tablos.types.ts`, `tablo-data.types.ts`, `events.types.ts`, `stripe.types.ts`, `kanban.types.ts`).
+
+## Authentication
+
+### Supabase Auth (primary users)
+- JWT-based, with sessions managed by `SessionContext` (`packages/shared/src/contexts/SessionContext.tsx`) listening to `supabase.auth.onAuthStateChange()`.
+- API extracts and validates the bearer token via `authenticateFromHeader` in `apps/api/src/helpers/auth.ts`, wired in `authMiddleware` (`apps/api/src/middlewares/middleware.ts`).
+- `maybeAuthenticatedMiddleware` (same file) sets `c.var.user = null` when the header is missing — used by booking and other "maybe authed" endpoints in `apps/api/src/routers/maybeAuthRouter.ts`.
+- Passwordless temporary accounts (`is_temporary: true`) created via the public booking flow.
+- Front-end auth UI lives in `@xtablo/auth-ui`.
+
+### Admin auth (internal)
+- Custom JWTs signed with `ADMIN_TOKEN_SIGNING_SECRET` (Google Secret Manager).
+- Helpers in `apps/api/src/helpers/adminTokens.ts`, verified by `adminAuthMiddleware` (Bearer scheme) in `apps/api/src/middlewares/middleware.ts`.
+- Routes: `apps/api/src/routers/admin.ts`, `adminActions.ts`, `adminAuth.ts`, `adminDatasets.ts`, `adminOverview.ts`, `adminTables.ts`.
+
+### Client portal auth (read-only client users)
+- Magic-link → cookie-based session JWT, stored in cookie `xtablo_client_session` on domain `clients.xtablo.com`.
+- Issuance: `apps/api/src/helpers/clientMagicLinks.ts`, `apps/api/src/helpers/clientSessions.ts`.
+- Middlewares `clientAuthMiddleware` / `maybeClientAuthMiddleware` (`apps/api/src/middlewares/middleware.ts`) verify cookies using `CLIENT_AUTH_JWT_SECRET`.
+- Routers: `apps/api/src/routers/clientAuth.ts`, `clientInvites.ts`, `clientPortal.ts`.
+
+### Task auth (cron / job runners)
+- HTTP Basic with `TASKS_SECRET`, enforced by `basicAuthMiddleware` for `apps/api/src/routers/tasks.ts`.
+
+### Chat worker auth
+- Independent JWT verification using `jose` against `JWT_SECRET` Wrangler secret. WebSocket connections receive the token via `?token=` query string; REST via Authorization header (`apps/chat-worker/src/index.ts`, `apps/chat-worker/src/lib/auth.ts`).
+
+## Payments — Stripe
+
+- **Server SDK** — `stripe ^20.0.0` instantiated per-request inside `stripeMiddleware` with `apiVersion: "2025-11-17.clover"` (`apps/api/src/middlewares/middleware.ts`).
+- **Browser SDK** — `@stripe/stripe-js` in `apps/main`.
+- **Webhook router** — `apps/api/src/routers/stripe.ts`, mounted at `/api/v1/stripe-webhook` in `apps/api/src/routers/index.ts`. Plan tiers (`solo`, `team`, `founder`) keyed off `STRIPE_*_PRICE_ID` env vars.
+- **Sync engine** — `@supabase/stripe-sync-engine` writes Stripe objects directly into the `stripe` schema of Supabase via a direct Postgres pool (`apps/api/src/middlewares/stripeSync.ts`). Refetches `subscription` and `customer` objects to avoid stale state.
+- **Secrets** — `stripe-secret-key`, `stripe-webhook-secret` (prod) and `…-staging` variants pulled from Google Secret Manager (`apps/api/src/secrets.ts`).
+- **Billing helpers** — `apps/api/src/helpers/billing.ts`. Active-plan enforcement via `activePlanAccessMiddleware`; only the org's billing owner can manage billing.
+- **Docs** — `docs/STRIPE_ARCHITECTURE.md`, `docs/STRIPE_README.md`, `docs/STRIPE_WITH_SYNC_ENGINE.md`, `docs/STRIPE_INTEGRATION_COMPLETE.md` plus several setup/testing guides.
+
+## Mobile Payments — RevenueCat
+
+- Webhook router `apps/api/src/routers/revenuecat.ts`, mounted at `/api/v1/revenuecat-webhook`.
+- Maps Apple in-app product IDs to plans via `apps/api/src/helpers/appleBilling.ts`.
+- Env: `REVENUECAT_WEBHOOK_AUTH_HEADER`, `REVENUECAT_SOLO_PRODUCT_ID`, `REVENUECAT_ANNUAL_PRODUCT_ID`.
+
+## Storage — Cloudflare R2 (S3 SDK)
+
+- `@aws-sdk/client-s3` pointed at `https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com` with `region: "auto"`.
+- Instantiated per-request in `r2Middleware` (`apps/api/src/middlewares/middleware.ts`) and exposed as `c.get("s3_client")`.
+- Credentials: `R2_ACCESS_KEY_ID` / `R2_SECRET_ACCESS_KEY` from Secret Manager.
+- Upload pipelines use `multer` + `sharp` for image processing (declared in `apps/api/package.json`).
+
+## Email — Gmail OAuth2 (Nodemailer)
+
+- SMTP through `smtp.gmail.com:465` using Google OAuth2 tokens.
+- Built in `apps/api/src/middlewares/transporter.ts` via `nodemailer.createTransport` with `EMAIL_USER`, `EMAIL_CLIENT_ID`, `EMAIL_CLIENT_SECRET`, `EMAIL_REFRESH_TOKEN`.
+- Exposed as `c.get("transporter")` everywhere `transporterMiddleware` is applied (mounted globally in `apps/api/src/routers/index.ts`).
+
+## Chat — Custom Cloudflare Durable Objects
+
+The CLAUDE.md mentions Stream Chat historically, but the current implementation is a **custom WebSocket chat** running on Cloudflare Workers + Durable Objects (no `stream-chat` dependency found anywhere in the repo).
+
+- Worker entry: `apps/chat-worker/src/index.ts` (Hono).
+- Durable Object class: `apps/chat-worker/src/durable-objects/ChatRoom.ts`. Declared in `apps/chat-worker/wrangler.toml`:
+ ```toml
+ [durable_objects]
+ bindings = [{ name = "CHAT_ROOM", class_name = "ChatRoom" }]
+ [[migrations]]
+ tag = "v1"
+ new_sqlite_classes = ["ChatRoom"]
+ ```
+- Membership enforced by querying Supabase via PostgREST (`apps/chat-worker/src/lib/supabase.ts`).
+- UI consumed via `@xtablo/chat-ui` (used by `apps/main`, `apps/clients`, `@xtablo/tablo-views`).
+- Custom domain: `chat.xtablo.com`.
+
+## Observability
+
+### Frontend — Datadog RUM
+- `apps/main/src/lib/rum.ts` initialises `@datadog/browser-rum` with `applicationId: "8e268e1a-1be0-44c6-b12a-978530d497c7"`, `service: "xtablo-ui"`, `sessionSampleRate: 100`, `sessionReplaySampleRate: 80`, `defaultPrivacyLevel: "mask-user-input"`, and the React plugin with router instrumentation.
+- `apps/clients/src/lib/rum.ts` does the same for the clients app.
+- User is set in `apps/main/src/providers/UserStoreProvider.tsx` (`datadogRum.setUser` on login, `clearUser` on logout).
+- Manual view names via `apps/main/src/hooks/useDatadogRumViewName.tsx`.
+
+### Backend — dd-trace
+- `tracer.init({ logInjection: true })` is the very first call in `apps/api/src/index.ts`.
+- Datadog CI tooling (`@datadog/datadog-ci`, `@datadog/datadog-ci-plugin-cloud-run`) is wired for Cloud Run instrumentation uploads.
+- Static analysis is configured at the repo root in `static-analysis.datadog.yml`.
+
+## Secrets Management — Google Secret Manager
+
+- `apps/api/src/secrets.ts` fetches every sensitive value from `projects/xtablo/secrets/{name}/versions/latest` using `@google-cloud/secret-manager`.
+- Secrets loaded: `supabase-service-role-key`, `supabase-connection-string`, `supabase-ca-cert`, `admin-token-signing-secret`, `client-auth-jwt-secret`, `email-client-secret`, `email-refresh-token`, `r2-access-key-id`, `r2-secret-access-key`, `stripe-secret-key`, `stripe-webhook-secret`, plus their `…-staging` variants.
+- Setup guide: `docs/GOOGLE_SECRET_MANAGER_SETUP.md`.
+
+## Deployment Platforms
+
+### Cloudflare Workers (web apps + chat)
+- All Vite-built frontends are deployed to Workers via Wrangler (see `apps/*/wrangler.toml`).
+- `apps/main/wrangler.toml` binds `app.xtablo.com` (prod) and `app-staging.xtablo.com` (staging), uses `not_found_handling = "single-page-application"`.
+- `apps/external/wrangler.toml`, `apps/admin/wrangler.toml`, `apps/clients/wrangler.toml` each have their own custom domain.
+- `apps/chat-worker/wrangler.toml` declares the `ChatRoom` Durable Object SQLite class and binds `chat.xtablo.com`.
+- Worker scripts include observability blocks (`[observability] enabled = true`).
+
+### Google Cloud Run (API)
+- Containerised by `apps/api/Dockerfile` (multi-stage Node 20 Alpine, pnpm-driven).
+- CI/CD via Google Cloud Build using `apps/api/cloudbuild.yaml`.
+- Runtime configuration uses Cloud Run env vars + Secret Manager (no env files inside the image).
+- Deployment guides: `docs/CLOUD_BUILD_SETUP.md`, `docs/CLOUD_BUILD_ENV_CONFIG.md`, `docs/DOCKER_BUILD.md`, `docs/DOCKER_PNPM_OPTIMIZATION.md`, `docs/DOCKER_BUILD_PERFORMANCE.md`.
+
+### Go backend (separate)
+- `go-backend/` deploys via its own `compose.yaml` / `justfile` and is independent of the TypeScript build pipeline (uses `chi`, `templ`, `sqlc`, `pgx`).
+
+### Infra utilities
+- `infra/docker-compose.yaml`, `infra/docker-compose.traefik.yaml`, `infra/Dockerfile` — generic infra/traefik setup. `infra/app/main.py` is a small Python helper.
+
+## Other Integrations / Tooling
+
+- **Google APIs** (`googleapis ^161.0.0`) — currently used to mint OAuth2 access tokens for the Gmail SMTP transport (`apps/api/src/middlewares/transporter.ts`).
+- **PWA** — `vite-plugin-pwa` in `apps/main` generates the service worker; `workbox-window` registers it.
+- **Sourcemaps to Datadog** — `@datadog/datadog-ci` is available at the workspace root for `datadog-ci sourcemaps upload`.
+- **Chromatic** — `chromatic ^11.5.0` listed in `apps/main` dev deps for visual regression (no CI workflow inspected here).
diff --git a/.planning/codebase/STACK.md b/.planning/codebase/STACK.md
new file mode 100644
index 0000000..61b523d
--- /dev/null
+++ b/.planning/codebase/STACK.md
@@ -0,0 +1,142 @@
+# STACK
+
+Technology stack reference for the xtablo monorepo. Last updated 2026-05-14.
+
+## Languages & Runtimes
+
+- **TypeScript** — pinned at `^5.7.0` across most workspaces (api uses `^5.8.3`). Root devDependency in `package.json`.
+- **Node.js** — `>=20.0.0` enforced via the `engines` field in the root `package.json`. The API Dockerfile builds on `node:20-alpine` (`apps/api/Dockerfile`).
+- **Package manager** — pnpm `10.19.0` (declared as `packageManager` in the root `package.json`). Cloudflare Workers builds and the API container use `corepack` to install pnpm.
+- **Go** — a parallel `go-backend/` service exists alongside the TS monorepo (`go-backend/go.mod`, `go 1.26.0`) using `chi`, `templ`, `pgx/v5`, and `sqlc`. It is included in the pnpm workspace but is otherwise its own toolchain.
+- **Python** — a small infra utility at `infra/app/main.py` with `infra/requirements.txt` (not part of the runtime stack of the apps).
+
+## Frameworks
+
+### Frontend (React)
+- **React 19.0.0** + **React DOM 19.0.0** — used by `apps/main`, `apps/external`, `apps/admin`, `apps/clients`, and all React packages.
+- **React Router** — `react-router-dom ^7.9.4`.
+- **Vite 6.2** — bundler/dev server for every web app (`apps/main/vite.config.ts`, `apps/external/vite.config.ts`, `apps/admin/vite.config.ts`, `apps/clients/vite.config.ts`).
+- **TailwindCSS 4.1** — utility CSS via `@tailwindcss/vite`, with `tw-animate-css` and `tailwind-merge`.
+- **Radix UI** + **React Aria** — primitives composed in `@xtablo/ui` (see `packages/ui/package.json`).
+- **BlockNote** — rich-text editor (`@blocknote/core`, `@blocknote/mantine`, `@blocknote/react`) used in `apps/main`.
+- **AG Grid Community** — data grid in `apps/main` (`ag-grid-community`, `ag-grid-react`).
+- **React Hook Form** + **Zod** — forms and validation, wired through `@hookform/resolvers`.
+- **i18next** + `react-i18next` — translations (`apps/main/src/i18n.ts`, plus `external`, `clients`, `tablo-views`).
+- **react-day-picker** — calendar UI.
+- **PWA** — `vite-plugin-pwa` + `workbox-window` in `apps/main`.
+
+### Backend (Hono)
+- **Hono ^4.7.7** — HTTP framework for both `apps/api` and `apps/chat-worker`.
+- **@hono/node-server** — Node adapter that drives `apps/api` (`apps/api/src/index.ts`).
+- **hono-sessions** — session helpers in the API.
+- **Cloudflare Workers + Durable Objects** — `apps/chat-worker` uses Hono with a `ChatRoom` SQLite-backed DO class (`apps/chat-worker/wrangler.toml`, `apps/chat-worker/src/durable-objects/ChatRoom.ts`).
+
+## Core Dependencies
+
+### Server State / Data Fetching
+- `@tanstack/react-query ^5.69.0` — primary server-state cache. Hierarchical query keys; 5-minute default cache. Used in `apps/main`, `apps/clients`, `apps/admin`, `apps/external`, and `@xtablo/tablo-views`.
+- `axios ^1.12.2` — HTTP client wrapper at `packages/shared/src/lib/api.ts`.
+
+### Client State
+- `zustand ^5.0.5` — global stores (notably user). Lives in `@xtablo/shared` and is consumed via `useUser` / `useMaybeUser`.
+
+### Auth & JWT
+- `@supabase/supabase-js ^2.49.x` — front-end and API client.
+- `jwt-decode ^4.0.0` — decode access tokens on the client.
+- `jose ^6.0.0` — JWT verification in the chat worker (`apps/chat-worker/src/lib/auth.ts`).
+
+### UI Primitives
+- Radix: `react-avatar`, `react-checkbox`, `react-collapsible`, `react-dialog`, `react-dropdown-menu`, `react-label`, `react-popover`, `react-select`, `react-separator`, `react-slider`, `react-slot`, `react-switch`, `react-tabs`, `react-tooltip`, `react-radio-group` (`packages/ui/package.json`).
+- `react-aria` / `react-aria-components ^1.7.0`, `@react-stately/*`, `@react-aria/*`.
+- `lucide-react ^0.460.0` — iconography.
+- `class-variance-authority`, `clsx`, `tailwind-merge` — class composition.
+- `sonner ^2.0.7` — toast notifications (re-exported via `packages/shared/src/lib/toast.ts`).
+
+### Dates, IDs, Utilities
+- `date-fns ^4.1.0`, `luxon ^3.7.2` (API only), `@internationalized/date`.
+- `uuid ^11.1.0`, `pluralize ^8.0.0`, `ts-pattern ^5.6.2`.
+- `jspdf ^3.0.3` — PDF export (main + shared).
+
+### Payments / Billing
+- `stripe ^20.0.0` — server SDK (`apps/api`).
+- `@stripe/stripe-js ^8.2.0` — browser SDK (`apps/main`).
+- `@supabase/stripe-sync-engine ^0.45.0` — Stripe ↔ Supabase sync (`apps/api/src/middlewares/stripeSync.ts`).
+
+### Storage / Email
+- `@aws-sdk/client-s3 ^3.850.0` — used against Cloudflare R2 in `apps/api/src/middlewares/middleware.ts` (`r2Middleware`).
+- `multer ^2.0.2`, `sharp ^0.34.5` — file upload handling and image processing.
+- `nodemailer ^7.0.4` + `googleapis ^161.0.0` — Gmail OAuth2 SMTP (`apps/api/src/middlewares/transporter.ts`).
+
+### Observability
+- `@datadog/browser-rum ^6.13.0` + `@datadog/browser-rum-react ^6.13.0` — initialised in `apps/main/src/lib/rum.ts` and `apps/clients/src/lib/rum.ts`.
+- `dd-trace ^5.74.0` — APM tracer started at the top of `apps/api/src/index.ts`.
+- `@datadog/datadog-ci`, `@datadog/datadog-ci-base`, `@datadog/datadog-ci-plugin-cloud-run` — CI source-map upload and Cloud Run integration.
+- `static-analysis.datadog.yml` — repo-level Datadog static analysis config.
+
+## Build / Dev Tooling
+
+- **Turborepo `^2.5.8`** — pipeline orchestration (`turbo.json`). Tasks: `build`, `dev`, `deploy(:staging|:prod)`, `build:staging`, `build:prod`, `lint`, `lint:fix`, `typecheck`, `test`, `test:watch`, `format`, `clean`. Caches `dist/**` and `tsconfig.tsbuildinfo`.
+- **Biome `2.2.5`** — formatter + linter, config at `biome.json` with explicit per-package `files.includes`.
+- **Vite `^6.2.2`** with `@vitejs/plugin-react ^4.3.4`, `vite-tsconfig-paths`, `@tailwindcss/vite`, `@cloudflare/vite-plugin`, `rollup-plugin-visualizer`, `vite-plugin-pwa`.
+- **Vitest** — `^3.2.4` in frontend apps, `^4.0.8` in `apps/api`. Browser env via `happy-dom` (main, admin) or `jsdom` (clients).
+- **Testing Library** — `@testing-library/react`, `@testing-library/jest-dom`, `@testing-library/user-event`.
+- **tsc** — every package has its own `tsconfig.json` and runs `tsc -b` or `tsc --noEmit` for typecheck.
+- **Wrangler `^4.24.3`** — Cloudflare Workers CLI used by `main`, `external`, `admin`, `clients`, `chat-worker`.
+- **tsx `^4.7.1`** — dev runner for the API (`pnpm dev` invokes `tsx watch src/index.ts`).
+
+## Configuration
+
+### Environment loading
+- API: `dotenv.config({ path: `.env.${NODE_ENV}` })` in `apps/api/src/config.ts`. `createConfig(secrets)` synthesizes a typed `AppConfig` from env + Google Secret Manager values.
+- Frontend: Vite `import.meta.env.*`; modes `dev`, `staging`, `production` selected via `vite build --mode`.
+
+### Secret loading
+- `apps/api/src/secrets.ts` pulls all sensitive values from `projects/xtablo/secrets/*/versions/latest` using `@google-cloud/secret-manager`.
+- Test mode bypasses Secret Manager and uses raw env vars.
+
+### Build targets per app
+| App | Bundler | Output | Deploy target |
+| --- | --- | --- | --- |
+| `apps/main` | Vite + `@cloudflare/vite-plugin` | `dist/` + worker | Cloudflare Workers (`apps/main/wrangler.toml`, routes `app.xtablo.com`, `app-staging.xtablo.com`) |
+| `apps/external` | Vite + Cloudflare plugin | `dist/` | Cloudflare Workers (`apps/external/wrangler.toml`) |
+| `apps/admin` | Vite + Cloudflare plugin | `dist/` | Cloudflare Workers (`apps/admin/wrangler.toml`) |
+| `apps/clients` | Vite + Cloudflare plugin | `dist/` | Cloudflare Workers (`apps/clients/wrangler.toml`) |
+| `apps/chat-worker` | Wrangler-native | Worker bundle | Cloudflare Workers + Durable Objects, `chat.xtablo.com` |
+| `apps/api` | `tsc` → `dist/` | Node 20 container | Google Cloud Run (`apps/api/Dockerfile`, `apps/api/cloudbuild.yaml`) |
+| `go-backend` | `go build` | Binary | Separate (see `go-backend/justfile`) |
+
+### Static configs
+- `biome.json` — single source of truth for formatting/linting scope.
+- `turbo.json` — task graph; `globalDependencies` include `**/.env.*local`.
+- `tsconfig.json` per workspace; project references across packages.
+
+## Workspace Structure
+
+`pnpm-workspace.yaml` defines:
+```yaml
+packages:
+ - 'apps/*'
+ - 'go-backend'
+ - 'packages/*'
+```
+
+### Apps (`apps/`)
+- `@xtablo/main` — authenticated dashboard (port 5173).
+- `@xtablo/external` — embeddable public booking widget (port 5174).
+- `@xtablo/clients` — read-only client portal (port 5175, `clients.xtablo.com`).
+- `@xtablo/admin` — internal admin app (port 5176).
+- `@xtablo/api` — Hono REST API (port 8080).
+- `@xtablo/chat-worker` — Cloudflare Worker hosting Durable-Object chat (`chat.xtablo.com`).
+
+### Packages (`packages/`)
+- `@xtablo/shared` — React contexts, hooks, supabase wrapper, axios client, toast helper. **Source-only** (no build step; consumers import TS directly — see `packages/shared/package.json` `"main": "./src/index.ts"`).
+- `@xtablo/ui` — Radix + Tailwind + react-aria component library. **Source-only**.
+- `@xtablo/shared-types` — pure TS types including Supabase-generated `database.types.ts`. **Source-only**, zero runtime deps.
+- `@xtablo/auth-ui` — shared auth screens. **Source-only**.
+- `@xtablo/chat-ui` — chat UI components consumed by main, clients, tablo-views. **Source-only**.
+- `@xtablo/tablo-views` — tablo view components shared between main and clients. **Source-only**.
+
+All shared packages export `./src/*` directly, so Vite HMR / tsc project references pick up changes instantly without a build step.
+
+### pnpm overrides
+Root `package.json` pins `form-data ^4.0.4` and `linkifyjs ^4.3.2`, plus a `packageExtensions` entry adding `zod` as a peer of `@hookform/resolvers`.
diff --git a/.planning/codebase/STRUCTURE.md b/.planning/codebase/STRUCTURE.md
new file mode 100644
index 0000000..938553d
--- /dev/null
+++ b/.planning/codebase/STRUCTURE.md
@@ -0,0 +1,296 @@
+# Structure
+
+_Last updated: 2026-05-14_
+
+Directory layout, conventions, and where to look for things in the `xtablo-source` monorepo.
+
+## Top-Level Layout
+
+```
+xtablo-source/
+├── apps/ # Deployable applications
+│ ├── main/ # Authenticated dashboard SPA (Vite + CF Workers)
+│ ├── external/ # Public booking widget (Vite)
+│ ├── clients/ # Client portal (Vite + CF Workers)
+│ ├── admin/ # Internal admin tools (Vite + CF Workers)
+│ ├── api/ # Hono REST API (Node, GCP Cloud Run)
+│ └── chat-worker/ # Cloudflare Durable Object worker for chat
+├── packages/ # Shared workspace packages (source-only)
+│ ├── shared/ # Contexts, hooks, API client, toast, supabase client
+│ ├── ui/ # Radix + Tailwind component library
+│ ├── shared-types/ # Pure TypeScript types (zero runtime deps)
+│ ├── auth-ui/ # Auth screens (login/register shells)
+│ ├── chat-ui/ # Chat-specific UI components
+│ └── tablo-views/ # Tablo detail / sections components
+├── backend/ # Legacy / auxiliary backend code
+├── go-backend/ # Go services (separate stack)
+├── frontend_v2/ # Frontend v2 prototype
+├── xtablo-expo/ # React Native (Expo) app
+├── supabase/ # Supabase project (migrations, config)
+├── infra/ # Infrastructure-as-code
+├── docs/ # Architecture, deployment, integration docs
+├── prompts/ # Prompt templates
+├── scripts/ # Repo scripts
+├── CLAUDE.md # Claude Code guidance (root)
+├── DEVELOPMENT.md # Dev guide
+├── turbo.json # Turborepo pipeline config
+├── pnpm-workspace.yaml
+└── package.json
+```
+
+## Apps
+
+### `apps/main` (`@xtablo/main`)
+
+Primary dashboard. Internal layout:
+
+```
+apps/main/src/
+├── main.tsx # Vite entry
+├── App.tsx # Root component, providers
+├── i18n.ts # i18next setup
+├── main.css # Tailwind entry
+├── components/ # UI components (Modals, Sections, Cards, ...)
+├── contexts/ # App-local React contexts (UpgradeBlockContext, ...)
+├── hooks/ # Feature hooks (tablos, events, tasks, stripe, ...)
+├── lib/
+│ ├── api.ts # API client wrapper
+│ ├── supabase.ts # Supabase client re-export
+│ ├── routes.tsx # Protected routes
+│ ├── publicRoutes.tsx # Public/auth routes
+│ ├── billing.ts # Stripe-related helpers
+│ ├── env.ts # Env validation
+│ └── rum.ts # Datadog RUM init
+├── pages/ # Route page components
+├── providers/
+│ └── UserStoreProvider.tsx # Zustand user store
+├── locales/ # i18n JSON
+├── utils/
+└── assets/
+```
+
+### `apps/external` (`@xtablo/external`)
+
+Embeddable booking widget. Internal layout:
+
+```
+apps/external/src/
+├── main.tsx
+├── routes.tsx
+├── EmbeddedBookingPage.tsx
+├── FloatingBookingWidget.tsx
+├── CustomModal.tsx
+├── UserStoreProvider.tsx
+├── lib/
+└── locales/
+```
+
+Mode is driven by query string: `?mode=embed&eventTypeId=...` or `?mode=floating`.
+
+### `apps/clients` (`@xtablo/clients`)
+
+Public-facing client portal.
+
+```
+apps/clients/src/
+├── main.tsx
+├── App.tsx
+├── routes.tsx
+├── components/
+├── hooks/
+├── lib/
+├── pages/
+├── locales/
+└── test/
+```
+
+### `apps/admin` (`@xtablo/admin`)
+
+Internal admin app.
+
+```
+apps/admin/src/
+├── main.tsx
+├── App.tsx
+├── routes.tsx
+├── components/
+├── hooks/
+├── lib/
+├── pages/
+└── registry/
+```
+
+### `apps/api` (`@xtablo/api`)
+
+Hono REST API. Internal layout:
+
+```
+apps/api/src/
+├── index.ts # Entry: tracer, secrets, server start
+├── config.ts # createConfig(secrets) -> AppConfig
+├── secrets.ts # loadSecrets() (env or GCP Secret Manager)
+├── client.ts # Supabase admin client factory
+├── middlewares/
+│ ├── middleware.ts # MiddlewareManager singleton + supabase/r2/stripe
+│ ├── stripeSync.ts # Stripe<->Supabase sync engine
+│ └── transporter.ts # Email transport middleware
+├── routers/
+│ ├── index.ts # getMainRouter — composition + ordering
+│ ├── public.ts # Unauthenticated endpoints
+│ ├── authRouter.ts # Requires JWT
+│ ├── maybeAuthRouter.ts # Optional auth (booking-aware)
+│ ├── tablo.ts # Tablo CRUD
+│ ├── tablo_data.ts # Tablo content blocks
+│ ├── tasks.ts # Tasks
+│ ├── notes.ts # Notes
+│ ├── events.ts (in user.ts/tablo_data.ts)
+│ ├── stripe.ts # Stripe webhook + ops
+│ ├── revenuecat.ts # RevenueCat webhook
+│ ├── invite.ts # Tablo invites
+│ ├── user.ts # User profile / settings
+│ ├── admin*.ts # admin.ts, adminActions.ts, adminAuth.ts,
+│ │ # adminDatasets.ts, adminOverview.ts, adminTables.ts
+│ ├── clientAuth.ts # Client portal magic-link auth
+│ ├── clientPortal.ts # Client portal endpoints
+│ └── clientInvites.ts # Public client invites
+├── helpers/
+├── types/ # BaseEnv, app.types.ts
+└── __tests__/
+```
+
+### `apps/chat-worker`
+
+```
+apps/chat-worker/src/
+├── index.ts # Worker entry
+├── durable-objects/ # Durable Object classes
+└── lib/
+```
+
+## Packages
+
+### `packages/shared` (`@xtablo/shared`)
+
+Public surface via `packages/shared/src/index.ts`.
+
+```
+packages/shared/src/
+├── index.ts # Barrel
+├── contexts/
+│ ├── SessionContext.tsx # Supabase session listener
+│ └── ThemeContext.tsx
+├── hooks/
+│ ├── auth.ts # useSignIn / useSignOut / useSession ...
+│ ├── book.ts # Booking hooks
+│ ├── public.ts # Public data hooks
+│ └── useClickOutside.ts
+├── lib/
+│ ├── api.ts # HTTP client w/ Bearer token
+│ ├── supabase.ts # Supabase JS client
+│ ├── toast.ts # toast.add() wrapper (sonner)
+│ └── cn.ts # clsx + tailwind-merge
+├── types/ # Re-exported domain types
+└── utils/
+ └── helpers.ts
+```
+
+### `packages/ui` (`@xtablo/ui`)
+
+Radix UI + Tailwind component library. Source-only.
+
+```
+packages/ui/src/
+├── components/ # button.tsx, dialog.tsx, select.tsx,
+│ # popover.tsx, tabs.tsx, dropdown-menu.tsx, ...
+├── hooks/
+└── styles/
+```
+
+### `packages/shared-types` (`@xtablo/shared-types`)
+
+Zero-dependency TypeScript types — safe to import from API and frontend.
+
+```
+packages/shared-types/src/
+├── index.ts
+├── database.types.ts # Auto-generated by supabase gen types
+├── tablos.types.ts
+├── tablo-data.types.ts
+├── events.types.ts
+├── kanban.types.ts
+├── stripe.types.ts
+├── admin.types.ts
+└── utils.ts
+```
+
+### `packages/auth-ui` (`@xtablo/auth-ui`)
+
+Auth screens shared between apps: `AuthCardShell.tsx`, `AuthEmailPasswordForm.tsx`, `AuthInfoBanner.tsx`.
+
+### `packages/chat-ui` (`@xtablo/chat-ui`)
+
+Chat-specific UI (`components/`, `hooks.ts`, `security.ts`, `types.ts`, `chat-ui.css`).
+
+### `packages/tablo-views` (`@xtablo/tablo-views`)
+
+Tablo detail sections and shell: `TabloDetailsShell.tsx`, `TabloEventsSection.tsx`, `TabloFilesSection.tsx`, `TabloTasksSection.tsx`, `TabloDiscussionSection.tsx`, `TabloHeaderActions.tsx`, `EtapesSection.tsx`, `RoadmapSection.tsx`, plus `single-tablo/`, `components/`, `hooks/`, `styles/`.
+
+## Naming Conventions
+
+- **Modals**: `*Modal.tsx` — e.g. `apps/main/src/components/CreateTabloModal.tsx`, `EventDetailsModal.tsx`.
+- **Sections**: `*Section.tsx` — e.g. `packages/tablo-views/src/TabloFilesSection.tsx`.
+- **Cards**: `*Card.tsx` — e.g. `apps/main/src/components/EventTypeCard.tsx`, `AvailabilityCard.tsx`.
+- **Pages**: live in `apps//src/pages/`, lowercased file names matching the route (`planning.tsx`, `events.tsx`, `login.tsx`).
+- **Tests**: co-located as `*.test.tsx` / `*.test.ts` next to the source (`AvailabilityCard.test.tsx`, `auth.signup.test.ts`).
+- **Hooks**: lowercase domain file in `hooks/` (`tablos.ts`, `events.ts`, `tasks.ts`); each exports multiple named hooks.
+- **UI primitives**: lowercase in `packages/ui/src/components/` (`button.tsx`, `select.tsx`).
+
+## Key File Locations (Cheatsheet)
+
+| Concern | Path |
+|-------------------------------|---------------------------------------------------------|
+| Main app routes (protected) | `apps/main/src/lib/routes.tsx` |
+| Main app public routes | `apps/main/src/lib/publicRoutes.tsx` |
+| External app routes | `apps/external/src/routes.tsx` |
+| Clients app routes | `apps/clients/src/routes.tsx` |
+| Admin app routes | `apps/admin/src/routes.tsx` |
+| Session context | `packages/shared/src/contexts/SessionContext.tsx` |
+| User store (Zustand) | `apps/main/src/providers/UserStoreProvider.tsx` |
+| HTTP API client | `packages/shared/src/lib/api.ts` |
+| Supabase client (browser) | `packages/shared/src/lib/supabase.ts` |
+| API entry | `apps/api/src/index.ts` |
+| API router composition | `apps/api/src/routers/index.ts` |
+| API middleware singleton | `apps/api/src/middlewares/middleware.ts` |
+| API config | `apps/api/src/config.ts` |
+| API secrets loader | `apps/api/src/secrets.ts` |
+| Auto-generated DB types | `packages/shared-types/src/database.types.ts` |
+| Shared barrel export | `packages/shared/src/index.ts` |
+| UI component barrel | `packages/ui/src/components/index.ts` |
+| Turborepo pipeline | `turbo.json` |
+| Workspace definition | `pnpm-workspace.yaml` |
+| Root Claude guidance | `CLAUDE.md` |
+
+## Adding New Features (5-Step Flow)
+
+From `CLAUDE.md`, the canonical workflow for a new feature is:
+
+1. **Define types** in `packages/shared-types/src/` (or run `npx supabase gen types typescript > packages/shared-types/src/database.types.ts` if you changed the DB schema).
+2. **Add API endpoint** in `apps/api/src/routers/` — pick the right router (`public.ts`, `maybeAuthRouter.ts`, `authRouter.ts`, or a domain router like `tasks.ts`). Mount it in `apps/api/src/routers/index.ts` if it's new.
+3. **Create a React Query hook** in either `packages/shared/src/hooks/` (cross-app) or `apps/main/src/hooks/` (app-local). Use the hierarchical query-key convention so mutations can invalidate predictably.
+4. **Build the UI** using primitives from `@xtablo/ui` and patterns from `@xtablo/tablo-views` / `@xtablo/chat-ui` where applicable. Follow the `*Modal.tsx` / `*Section.tsx` / `*Card.tsx` naming convention and co-locate a `*.test.tsx`.
+5. **Wire the route** in `apps/main/src/lib/routes.tsx` (or the equivalent in the relevant app) so the new page is reachable.
+
+## Testing Locations
+
+- **API tests**: `apps/api/src/__tests__/` (Vitest, mock Supabase).
+- **Frontend tests**: co-located `*.test.tsx` next to source, run via `pnpm test` per app (`apps/main/src/setupTests.ts` configures happy-dom + Testing Library).
+- **Docs on testing**: `docs/API_TESTS.md`, `docs/MIDDLEWARE_TESTS.md`.
+
+## Documentation Index (in `docs/`)
+
+- `DEVELOPMENT.md` — broad dev guide.
+- `API_*.md` — API testing and integration.
+- `STRIPE_*.md` — Stripe integration deep-dives.
+- `AUTH_*.md` — Authentication patterns.
+- `DOCKER_*.md` — Docker build optimization.
+- `CLOUD_BUILD_*.md` — GCP Cloud Build setup.
diff --git a/.planning/codebase/TESTING.md b/.planning/codebase/TESTING.md
new file mode 100644
index 0000000..22a6602
--- /dev/null
+++ b/.planning/codebase/TESTING.md
@@ -0,0 +1,141 @@
+# Testing
+
+**Last updated:** 2026-05-14
+**Scope:** Turborepo monorepo at `/Users/arthur.belleville/Documents/perso/projects/xtablo-source`
+
+## Frameworks
+
+- **Vitest** is the only JS/TS test runner across every app and package (no Jest)
+- **React Testing Library** (`@testing-library/react`) for component tests; `@testing-library/jest-dom` matchers loaded via setup files
+- **happy-dom / jsdom** for DOM emulation in frontend tests (`apps/main/vite.config.ts` configures `environment: "jsdom"`)
+- **Vitest with mocked Supabase** for `apps/api` — node environment, real Hono router exercised, Supabase client stubbed (`apps/api/src/__tests__/setup.ts` + `globalSetup.ts`)
+- **pgTAP** SQL tests under `supabase/tests/database/*.test.sql` for schema/RLS/trigger correctness (run via the Supabase CLI, separate from Vitest)
+
+## Test Commands
+
+From the repo root (all dispatched by Turborepo via `turbo.json`):
+
+```bash
+pnpm test # Run every package's `test` task
+pnpm test:watch # Watch mode across the workspace
+pnpm test:api # Run only @xtablo/api (turbo --filter)
+cd apps/main && pnpm test # Run a single package directly
+cd apps/api && pnpm test:watch
+```
+
+Per-package script conventions (see e.g. `apps/api/package.json`):
+- `"test": "NODE_ENV=test vitest run"`
+- `"test:watch": "NODE_ENV=test vitest"`
+- Frontend apps similarly use `vitest run` / `vitest` (no `NODE_ENV=test` prefix required)
+
+## Test File Location
+
+Tests are **co-located** with the source they exercise:
+
+- React components: `Foo.tsx` + `Foo.test.tsx` in the same folder
+ - e.g. `apps/main/src/components/CustomModal.test.tsx`, `apps/main/src/components/NavigationBar.test.tsx`
+- Hooks / contexts: `useFoo.ts` + `useFoo.test.ts(x)`
+ - e.g. `apps/main/src/contexts/UpgradeBlockContext.test.tsx`
+- API: tests under `apps/api/src/__tests__//.test.ts` (grouped by router/concern), plus co-located helper tests like `apps/api/src/helpers/orgIcons.test.ts`
+- Expo app sometimes uses `__tests__/` folders: `xtablo-expo/components/__tests__/BillingPaywall.test.tsx`
+
+## Vitest Configs
+
+Two distinct flavors live in the repo:
+
+1. **Frontend (Vite + Vitest)** — config embedded inside `vite.config.ts`. Example, `apps/main/vite.config.ts`:
+ ```ts
+ test: {
+ globals: true,
+ environment: "jsdom",
+ setupFiles: "./src/setupTests.ts",
+ }
+ ```
+ The Cloudflare plugin is skipped when `process.env.VITEST === "true"`. `VITE_SUPABASE_URL` / `VITE_SUPABASE_ANON_KEY` are stubbed via `define` when running under Vitest.
+
+ Other apps with the same pattern:
+ - `apps/admin/vite.config.ts` + `apps/admin/src/setupTests.ts`
+ - `apps/external/vite.config.ts` + `apps/external/src/setupTests.ts`
+ - `apps/clients/vite.config.ts` + `apps/clients/src/setupTests.ts`
+
+2. **API (standalone Vitest)** — dedicated `apps/api/vitest.config.ts`:
+ ```ts
+ test: {
+ globals: true,
+ environment: "node",
+ setupFiles: ["./src/__tests__/setup.ts"],
+ globalSetup: ["./src/__tests__/globalSetup.ts"],
+ testTimeout: 30000,
+ hookTimeout: 60000,
+ include: ["src/__tests__/**/*.test.ts", "src/**/*.test.ts"],
+ pool: "forks",
+ fileParallelism: false,
+ }
+ ```
+ `fileParallelism: false` is deliberate — tests share initialized middleware state and can't safely run concurrently across files.
+
+## Setup Files
+
+- `apps/main/src/setupTests.ts` — imports `@testing-library/jest-dom`, registers an `afterEach(cleanup)`, mocks `ResizeObserver`, `Element.prototype.scrollIntoView`, `Element.prototype.scrollTo`, and `window.matchMedia`. Also imports `./i18n.test` to bootstrap i18next for tests.
+- `apps/admin/src/setupTests.ts`, `apps/external/src/setupTests.ts`, `apps/clients/src/setupTests.ts` — analogous per-app setup
+- `apps/api/src/__tests__/setup.ts` — minimal per-test-file setup (DB init handled by `globalSetup.ts`)
+- `apps/api/src/__tests__/globalSetup.ts` — boots the test middleware manager via `createConfig()` reading `.env.test`
+
+## Mocking Patterns
+
+- **Frontend component tests** import the component and render it with a `renderWithProviders` helper (see `apps/main/src/utils/testHelpers.tsx` and `apps/clients/src/test/testHelpers.test.tsx`) that wires QueryClient, router, i18n, and Zustand stores.
+- **React Query mocking**: integration tests mock the hook return values directly with `vi.mock(...)` and inject `{ data, isLoading, error }` shapes.
+- **Supabase client**: API tests stub `supabase.from(...)` chains via `vi.fn()` — fixtures live in `apps/api/src/__tests__/fixtures/`.
+- **Auth**: API middleware tests bypass real Supabase auth by overriding the Bearer-token validator and asserting on 401 paths (see `docs/MIDDLEWARE_TESTS.md`).
+- **Toast / window APIs**: stubbed globally in `setupTests.ts` so individual tests don't have to.
+- **i18n**: each frontend test setup imports `./i18n.test` so `useTranslation()` works without network.
+
+## API Test Patterns
+
+Documented in `docs/API_TESTS.md` and `docs/MIDDLEWARE_TESTS.md`. Key conventions:
+
+- Tests live under `apps/api/src/__tests__//.test.ts` (e.g. `notes/notes.test.ts`, `tasks/tasks.test.ts`)
+- Each router has at minimum a smoke test that verifies the endpoint is reachable and returns the right shape
+- Middleware is tested independently in `apps/api/src/__tests__/middlewares/middlewares.test.ts` — auth header validation, Supabase client injection, R2/Stream/email middleware behavior
+- Test mode is detected via `NODE_ENV=test` in `createConfig()` so secrets load from `.env.test` instead of Google Secret Manager — no GCP credentials required to run the suite
+- Fixtures (sample DB rows, sample Stripe payloads) live in `apps/api/src/__tests__/fixtures/`
+- Helpers (token mocking, app builders) live in `apps/api/src/__tests__/helpers/`
+- The `__tests__/README.md` documents the suite layout in-tree
+
+Snapshot from `docs/MIDDLEWARE_TESTS.md`: 116 passing tests across the API suite at last documented run (2025-11-10), with explicit coverage for the Supabase, Auth, Stream, R2, and email middlewares.
+
+## Coverage
+
+- **No coverage threshold is enforced** in CI today — `vitest.config.ts` files do not declare `coverage` blocks and `package.json` has no `test:coverage` script at the root.
+- Coverage can still be produced ad-hoc with `vitest run --coverage` in any individual package, but it's not part of the normal workflow.
+- The repo relies on (a) Biome lint errors blocking CI, (b) `pnpm typecheck` (`tsc -b`) blocking CI, and (c) `pnpm test` blocking CI — coverage % is currently advisory only.
+
+## Existing Test Inventory
+
+Total Vitest/RTL test files (`*.test.ts(x)`, excluding `node_modules`, `dist`): **147**
+
+Breakdown by area (approximate):
+- `apps/main/src/**/*.test.tsx` — ~64 (components, contexts, providers, hooks)
+- `apps/api/src/**/*.test.ts` — ~31 (routers, middlewares, helpers)
+- `apps/admin/src/**/*.test.tsx` — pages, components, lib (e.g. `AnalyticsStudioPage.test.tsx`, `PrivilegedGate.test.tsx`)
+- `apps/clients/src/**/*.test.ts(x)` — env + Vite config sanity checks plus page tests
+- `apps/external/src/viteConfig.test.ts`, `apps/main/src/viteConfig.test.ts` — Vite build config smoke tests
+- `xtablo-expo/**/*.test.ts(x)` — React Native (Expo) tests (e.g. `auth.test.ts`, `BillingPaywall.test.tsx`)
+- `supabase/tests/database/*.test.sql` — 13 pgTAP files covering schema, RLS, triggers, indexes, Stripe + Apple billing functions
+
+Representative examples worth reading first:
+- `apps/main/src/components/CustomModal.test.tsx` — minimal RTL test
+- `apps/main/src/components/NavigationBar.test.tsx` — uses `renderWithProviders`
+- `apps/main/src/providers/UserStoreProvider.test.tsx` — Zustand + React Query store test
+- `apps/main/src/contexts/UpgradeBlockContext.test.tsx` — context provider test
+- `apps/api/src/helpers/orgIcons.test.ts` — pure-helper unit test
+- `apps/api/src/__tests__/middlewares/middlewares.test.ts` — middleware integration suite
+- `supabase/tests/database/02_rls_policies_core.test.sql` — pgTAP RLS coverage
+
+## Reference Documents
+
+- `docs/API_TESTS.md` — API router test catalog, env setup, known limitations
+- `docs/MIDDLEWARE_TESTS.md` — middleware-by-middleware coverage notes
+- `docs/ENV_TEST_SETUP.md` — `.env.test` structure
+- `docs/TEST_FIXES.md`, `docs/TEST_ROUTER_REFACTOR.md` — historical notes on test infrastructure changes
+- `docs/TESTING_WITH_FAKE_ACCOUNTS.md` — temporary-account flow for manual + integration testing
diff --git a/.planning/config.json b/.planning/config.json
new file mode 100644
index 0000000..c91200b
--- /dev/null
+++ b/.planning/config.json
@@ -0,0 +1,43 @@
+{
+ "model_profile": "quality",
+ "commit_docs": true,
+ "parallelization": false,
+ "search_gitignored": false,
+ "brave_search": false,
+ "firecrawl": false,
+ "exa_search": false,
+ "git": {
+ "branching_strategy": "none",
+ "phase_branch_template": "gsd/phase-{phase}-{slug}",
+ "milestone_branch_template": "gsd/{milestone}-{slug}",
+ "quick_branch_template": null
+ },
+ "workflow": {
+ "research": true,
+ "plan_check": true,
+ "verifier": true,
+ "nyquist_validation": true,
+ "auto_advance": true,
+ "node_repair": true,
+ "node_repair_budget": 2,
+ "ui_phase": true,
+ "ui_safety_gate": true,
+ "text_mode": false,
+ "research_before_questions": false,
+ "discuss_mode": "discuss",
+ "skip_discuss": false,
+ "code_review": true,
+ "code_review_depth": "standard",
+ "_auto_chain_active": true
+ },
+ "hooks": {
+ "context_warnings": true
+ },
+ "project_code": null,
+ "phase_naming": "sequential",
+ "agent_skills": {},
+ "features": {},
+ "resolve_model_ids": "omit",
+ "mode": "yolo",
+ "granularity": "standard"
+}
diff --git a/.planning/milestones/v2.0-MILESTONE-AUDIT.md b/.planning/milestones/v2.0-MILESTONE-AUDIT.md
new file mode 100644
index 0000000..783ebf4
--- /dev/null
+++ b/.planning/milestones/v2.0-MILESTONE-AUDIT.md
@@ -0,0 +1,131 @@
+---
+milestone: v2.0
+milestone_name: Collaboration, planning, and social sign-in
+audited: 2026-05-16T08:40:45Z
+status: gaps_found
+scores:
+ requirements: 9/27 satisfied
+ phases: 2/5 phase-verifications passed
+ integration: 5/5 phase interfaces checked
+ flows: 5/5 core flows checked
+gaps:
+ requirements:
+ - id: "AUTH-08..13"
+ status: "partial"
+ phase: "8"
+ claimed_by_plans: ["08-01-SUMMARY.md", "08-02-SUMMARY.md", "08-03-SUMMARY.md", "08-04-SUMMARY.md", "08-05-SUMMARY.md"]
+ completed_by_plans: ["08-05-SUMMARY.md"]
+ verification_status: "missing"
+ evidence: "Phase 8 has 08-VALIDATION.md marked verified, but no 08-VERIFICATION.md for milestone audit aggregation."
+ - id: "ETAPE-01..06"
+ status: "partial"
+ phase: "9"
+ claimed_by_plans: ["09-01-SUMMARY.md", "09-02-SUMMARY.md", "09-03-SUMMARY.md", "09-04-SUMMARY.md"]
+ completed_by_plans: ["09-04-SUMMARY.md"]
+ verification_status: "missing"
+ evidence: "Phase 9 has completed summaries and tests, but no 09-VERIFICATION.md; REQUIREMENTS.md traceability still says Pending."
+ - id: "CHAT-01..06"
+ status: "partial"
+ phase: "12"
+ claimed_by_plans: ["12-01-SUMMARY.md", "12-02-SUMMARY.md", "12-03-SUMMARY.md"]
+ completed_by_plans: ["12-01-SUMMARY.md", "12-02-SUMMARY.md", "12-03-SUMMARY.md"]
+ verification_status: "missing"
+ evidence: "Phase 12 has complete summaries and 12-VALIDATION.md, but no 12-VERIFICATION.md for milestone audit aggregation."
+ integration: []
+ flows: []
+tech_debt:
+ - phase: "09-etapes"
+ items:
+ - "09-VALIDATION.md remains draft with pending rows and wave_0_complete=false despite Phase 9 completion."
+ - "REQUIREMENTS.md traceability row for ETAPE-01..06 remains Pending."
+ - phase: "10-events"
+ items:
+ - "10-VALIDATION.md remains draft with pending rows and wave_0_complete=false even though 10-VERIFICATION.md passed."
+ - phase: "11-individual-planning"
+ items:
+ - "11-VALIDATION.md remains draft with pending rows even though 11-VERIFICATION.md passed."
+ - "REQUIREMENTS.md traceability row for PLAN-01..04 remains Pending."
+ - phase: "12-native-tablo-chat"
+ items:
+ - "Create 12-VERIFICATION.md to mirror the completed 12-VALIDATION.md and browser UAT evidence."
+nyquist:
+ compliant_phases: ["08-social-sign-in", "12-native-tablo-chat"]
+ partial_phases: ["09-etapes", "10-events", "11-individual-planning"]
+ missing_phases: []
+ overall: partial
+---
+
+# Milestone v2.0 Audit
+
+## Result
+
+**GAPS FOUND**
+
+The v2.0 implementation appears broadly complete from summaries, tests, and available verification evidence, but the milestone cannot be archived under the strict audit workflow yet because required phase-level verification artifacts are missing or stale.
+
+## Milestone Scope
+
+| Phase | Name | Plans | Disk Status | Verification |
+|-------|------|-------|-------------|--------------|
+| 8 | Social Sign-in | 5/5 | complete | MISSING `08-VERIFICATION.md` |
+| 9 | Etapes | 4/4 | complete | MISSING `09-VERIFICATION.md` |
+| 10 | Events | 4/4 | complete | PASS |
+| 11 | Individual Planning | 2/2 | complete | PASS |
+| 12 | Native Tablo Chat | 3/3 | complete | MISSING `12-VERIFICATION.md` |
+
+## Requirements Coverage
+
+| Requirement Set | Phase | REQUIREMENTS.md | SUMMARY Frontmatter | VERIFICATION.md | Final Status |
+|-----------------|-------|-----------------|---------------------|-----------------|--------------|
+| AUTH-08..13 | 8 | checked | listed | missing | PARTIAL |
+| ETAPE-01..06 | 9 | pending | listed in `09-04-SUMMARY.md` | missing | PARTIAL |
+| EVENT-01..05 | 10 | checked | listed | PASS in `10-VERIFICATION.md` | SATISFIED |
+| PLAN-01..04 | 11 | traceability pending, checkboxes checked | listed | PASS in `11-VERIFICATION.md` | SATISFIED; traceability should be updated |
+| CHAT-01..06 | 12 | checked | listed | missing | PARTIAL |
+
+### Unsatisfied / Partial Requirements
+
+- **AUTH-08..13** (Phase 8)
+ - Reason: all requirements are checked and validation is green, but no `08-VERIFICATION.md` exists. Milestone audit requires phase verification aggregation.
+- **ETAPE-01..06** (Phase 9)
+ - Reason: implementation summaries claim completion, but no `09-VERIFICATION.md` exists and REQUIREMENTS.md traceability still says Pending.
+- **CHAT-01..06** (Phase 12)
+ - Reason: Phase 12 has complete summaries, Nyquist validation, and UAT evidence, but no `12-VERIFICATION.md` exists.
+
+## Cross-Phase Integration
+
+Inline integration check found no concrete broken wiring in the v2.0 runtime surface:
+
+| Flow | Phases | Status | Evidence |
+|------|--------|--------|----------|
+| Social sign-in issues local sessions | 8 -> auth/router | OK | `handlers_social_test.go`, `handlers_auth_test.go`, `08-VALIDATION.md` |
+| Etape organization preserves task board behavior | 9 -> tasks/tablos | OK but verification artifact missing | `handlers_etapes_test.go`, `handlers_tasks_test.go`, `09-04-SUMMARY.md` |
+| Events feed individual planning | 10 -> 11 | OK | `10-VERIFICATION.md`, `11-VERIFICATION.md`, `TestListUserEventsRangeReturnsOnlyOwnedTablos`, `TestPlanningListsOwnedEventsChronologically` |
+| Planning links return to source tablo Events tab | 10 -> 11 | OK | `11-VERIFICATION.md`, `PlanningEventURL` evidence |
+| Native discussion stays owner-only and local-infrastructure-only | 12 -> tablos/router/static | OK but verification artifact missing | `12-VALIDATION.md`, discussion tests, browser UAT summary |
+
+## Broken Flows
+
+No broken cross-phase flow was identified from available tests and artifacts.
+
+## Nyquist Coverage
+
+| Phase | VALIDATION.md | Compliant | Action |
+|-------|---------------|-----------|--------|
+| 8 | exists | true | none |
+| 9 | exists | partial | `$gsd-validate-phase 9` |
+| 10 | exists | partial | `$gsd-validate-phase 10` |
+| 11 | exists | partial | `$gsd-validate-phase 11` |
+| 12 | exists | true | none |
+
+## Required Closure Before Archive
+
+1. Create or regenerate `08-VERIFICATION.md`, `09-VERIFICATION.md`, and `12-VERIFICATION.md`.
+2. Update `REQUIREMENTS.md` traceability rows for `ETAPE-01..06` and `PLAN-01..04` if the verification artifacts confirm completion.
+3. Run `$gsd-validate-phase 9`, `$gsd-validate-phase 10`, and `$gsd-validate-phase 11` to bring stale `VALIDATION.md` files from pending/draft to audited status, or explicitly accept them as tech debt at milestone close.
+
+## Audit Notes
+
+- Agents are not installed in this workspace, so the `gsd-integration-checker` step was performed inline.
+- No code changes were made by this audit.
+- The audit status is `gaps_found` because missing `VERIFICATION.md` files are blockers under the workflow.
diff --git a/.planning/milestones/v2.0-REQUIREMENTS.md b/.planning/milestones/v2.0-REQUIREMENTS.md
new file mode 100644
index 0000000..0a7e12c
--- /dev/null
+++ b/.planning/milestones/v2.0-REQUIREMENTS.md
@@ -0,0 +1,101 @@
+# Requirements Archive: Xtablo v2.0 Collaboration, Planning, and Social Sign-in
+
+**Archived:** 2026-05-16
+**Source commit:** `58a9ce0`
+**Outcome:** Shipped with acknowledged audit debt
+
+## Core Value
+
+A user can sign in and run the Tablos workflow - organize work, attach files, discuss, and plan scheduled events - without a JS framework or managed chat provider.
+
+## Completed Requirements
+
+### Authentication
+
+- [x] **AUTH-08**: User can start a Google sign-in flow from the login/signup page
+- [x] **AUTH-09**: Google callback validates state, exchanges the authorization code, verifies the ID token, and creates or links a local Xtablo user
+- [x] **AUTH-10**: Apple sign-in is disabled and hidden from the login/signup page
+- [x] **AUTH-11**: Direct Apple sign-in routes are unavailable while Apple sign-in is disabled
+- [x] **AUTH-12**: Social sign-in issues the same server-managed Xtablo session cookie used by email/password login
+- [x] **AUTH-13**: Existing email/password login, signup, logout, CSRF, and rate limiting continue to work after social sign-in is added
+
+### Chat
+
+- [x] **CHAT-01**: Each tablo has a discussion view where authenticated authorized users can see persisted message history
+- [x] **CHAT-02**: User can post a text message to a tablo discussion with validation and CSRF protection
+- [x] **CHAT-03**: Messages are stored in Postgres with tablo, author, body, created timestamp, and deletion/edit metadata
+- [x] **CHAT-04**: Open tablo discussion views receive new messages in real time without a manual refresh
+- [x] **CHAT-05**: Real-time delivery uses Xtablo-owned infrastructure only; no managed chat or realtime provider is introduced
+- [x] **CHAT-06**: Message rendering escapes user content and enforces a server-side maximum body length
+
+### Etapes
+
+- [x] **ETAPE-01**: User can create an etape inside a tablo with a title and optional description
+- [x] **ETAPE-02**: User can edit, delete, and reorder etapes inside a tablo
+- [x] **ETAPE-03**: User can assign a task to zero or one etape
+- [x] **ETAPE-04**: Deleting an etape unassigns its tasks by default rather than deleting the tasks
+- [x] **ETAPE-05**: The task UI can show or filter tasks by etape while preserving existing kanban status and ordering behavior
+- [x] **ETAPE-06**: The data model prevents nested etapes; an etape cannot have a parent etape
+
+### Events
+
+- [x] **EVENT-01**: User can create a scheduled event attached to a tablo with title, start time, optional end time, optional description, and optional location
+- [x] **EVENT-02**: User can edit and delete tablo events
+- [x] **EVENT-03**: Tablo detail page includes an events view listing that tablo's scheduled events
+- [x] **EVENT-04**: Event validation requires an end time to be empty or after the start time
+- [x] **EVENT-05**: Event authorization follows tablo access rules so users cannot read or mutate events for inaccessible tablos
+
+### Planning
+
+- [x] **PLAN-01**: Each authenticated user can open an individual planning page
+- [x] **PLAN-02**: Planning page lists the user's scheduled events across tablos in chronological order
+- [x] **PLAN-03**: Planning page links each event back to its tablo context
+- [x] **PLAN-04**: Planning page supports a functional empty state and date navigation/filtering suitable for the first working version
+
+## Traceability
+
+| Requirement | Phase | Final Status | Notes |
+|-------------|-------|--------------|-------|
+| AUTH-08..13 | Phase 8 | Complete with audit artifact debt | Implementation and validation passed; strict milestone audit wanted `08-VERIFICATION.md`. |
+| ETAPE-01..06 | Phase 9 | Complete with archive correction | Source requirements were stale; `09-VALIDATION.md` and UAT evidence confirm completion. |
+| EVENT-01..05 | Phase 10 | Complete | `10-VERIFICATION.md` and `10-VALIDATION.md` passed. |
+| PLAN-01..04 | Phase 11 | Complete with validation draft debt | `11-VERIFICATION.md` passed; `11-VALIDATION.md` remained draft. |
+| CHAT-01..06 | Phase 12 | Complete with audit artifact debt | Implementation, validation, and UAT passed; strict milestone audit wanted `12-VERIFICATION.md`. |
+
+## Future Requirements
+
+### Chat
+
+- **CHAT-FUT-01**: Typing indicators
+- **CHAT-FUT-02**: Read receipts
+- **CHAT-FUT-03**: Message reactions
+- **CHAT-FUT-04**: Threads or replies
+- **CHAT-FUT-05**: File previews in chat messages
+
+### Planning
+
+- **PLAN-FUT-01**: Recurring events
+- **PLAN-FUT-02**: Reminder notifications
+- **PLAN-FUT-03**: ICS import/export
+- **PLAN-FUT-04**: External calendar sync
+
+### Etapes
+
+- **ETAPE-FUT-01**: Etape progress rollups
+- **ETAPE-FUT-02**: Etape templates
+- **ETAPE-FUT-03**: Etape dependencies
+
+## Out of Scope
+
+| Feature | Reason |
+|---------|--------|
+| Managed chat/realtime providers | User explicitly does not want third-party chat. |
+| Managed auth platforms | Google is an identity provider only; Xtablo owns users and sessions. Apple sign-in is disabled for now. |
+| WebSocket-first chat protocol | SSE receive + HTMX POST send shipped for v2. |
+| Nested etapes or arbitrary task hierarchy | User requested one parent per task and no parent-of-parent. |
+| Notes / rich documents | Not part of the requested v2 feature set. |
+| Billing / Stripe | Still deferred until product loop is validated. |
+| Public booking widget | Separate product surface, not part of this milestone. |
+| Client portal | Separate product surface, not part of this milestone. |
+| Admin tooling | Separate product surface, not part of this milestone. |
+| Mobile / Expo app | Web rewrite remains the current product surface. |
diff --git a/.planning/milestones/v2.0-ROADMAP.md b/.planning/milestones/v2.0-ROADMAP.md
new file mode 100644
index 0000000..f34fb16
--- /dev/null
+++ b/.planning/milestones/v2.0-ROADMAP.md
@@ -0,0 +1,129 @@
+# Milestone v2.0: Collaboration, Planning, and Social Sign-in
+
+**Status:** SHIPPED 2026-05-16
+**Phases:** 8-12
+**Total Plans:** 18
+**Source commit:** `58a9ce0`
+
+## Overview
+
+v2.0 added the collaboration and scheduling layer around tablos while keeping the Go + HTMX architecture: Google sign-in, one-level etapes for task organization, tablo-owned events, a personal planning agenda, and native persisted realtime discussions.
+
+The milestone was closed after acknowledging 7 legacy/open artifact items from earlier phase UAT and verification records. Runtime v2.0 flows were kept as shipped; the acknowledged items are tracked in `STATE.md` and the archived audit.
+
+## Phases
+
+### Phase 8: Social Sign-in
+
+**Goal:** Users can sign in with Google while Xtablo keeps owning user accounts and sessions; Apple sign-in is disabled and hidden for now.
+**Plans:** 5
+**Requirements:** AUTH-08, AUTH-09, AUTH-10, AUTH-11, AUTH-12, AUTH-13
+
+Plans:
+
+- [x] 08-01: Social identity schema foundation
+- [x] 08-02: Google OAuth/OIDC sign-in flow
+- [x] 08-03: Apple sign-in implementation, later disabled by UAT scope change
+- [x] 08-04: Social sign-in controls and missing-config states
+- [x] 08-05: Account provider visibility and final docs/regression coverage
+
+Outcome:
+
+- Google social sign-in starts from auth pages, validates callback state, links by provider subject or verified email, and issues existing Xtablo sessions.
+- Apple sign-in was intentionally disabled after UAT; routes are not mounted and controls are hidden.
+- Provider configuration is documented without committing secrets.
+
+### Phase 9: Etapes
+
+**Goal:** A user can organize tasks under one-level etapes inside a tablo while preserving existing task status and ordering behavior.
+**Plans:** 4
+**Requirements:** ETAPE-01, ETAPE-02, ETAPE-03, ETAPE-04, ETAPE-05, ETAPE-06
+
+Plans:
+
+- [x] 09-01: Etape create/filter/assigned-task slice
+- [x] 09-02: Etape edit, delete-unassign, and reorder management
+- [x] 09-03: Task assignment and filter-aware task loading
+- [x] 09-04: Reorder preservation, count refresh fixes, and UAT closure
+
+Outcome:
+
+- Etapes are tablo-owned one-level wrappers; tasks can belong to zero or one etape.
+- Deleting an etape unassigns its tasks instead of deleting them.
+- Task assignment, filtering, and reorder behavior remain compatible with the existing kanban model.
+
+### Phase 10: Events
+
+**Goal:** A user can schedule events that belong to tablos, with the same authorization expectations as tasks and files.
+**Plans:** 4
+**Requirements:** EVENT-01, EVENT-02, EVENT-03, EVENT-04, EVENT-05
+
+Plans:
+
+- [x] 10-01: Event creation and month calendar listing
+- [x] 10-02: Inline edit, update validation, and hard delete
+- [x] 10-03: Month navigation, day prefill, overflow display, and planning query
+- [x] 10-04: Final regression coverage, full backend verification, and browser UAT
+
+Outcome:
+
+- Tablo events support title, date, start time, optional end time, location, and description.
+- Invalid end times are rejected by handler validation and database constraint.
+- Event access follows tablo ownership; inaccessible events cannot be read or mutated.
+
+### Phase 11: Individual Planning
+
+**Goal:** Each authenticated user can open a personal planning view that aggregates their scheduled events across tablos.
+**Plans:** 2
+**Requirements:** PLAN-01, PLAN-02, PLAN-03, PLAN-04
+
+Plans:
+
+- [x] 11-01: Protected planning agenda with owned cross-tablo event aggregation
+- [x] 11-02: Full verification and UAT-approved 14-day planning agenda
+
+Outcome:
+
+- `/planning` is authenticated and lists the user's owned events chronologically.
+- Event rows link back to their source tablo event context.
+- Empty state, invalid-date fallback, and 14-day navigation were verified.
+
+### Phase 12: Native Tablo Chat
+
+**Goal:** Each tablo has a native persisted discussion stream with real-time updates and no managed chat/realtime provider.
+**Plans:** 3
+**Requirements:** CHAT-01, CHAT-02, CHAT-03, CHAT-04, CHAT-05, CHAT-06
+
+Plans:
+
+- [x] 12-01: Discussion schema, history, posting, validation, and templates
+- [x] 12-02: Persistent unread state and dashboard/discussion read paths
+- [x] 12-03: Native realtime discussion delivery with Go-owned SSE receive and HTMX POST send
+
+Outcome:
+
+- Discussion history and sends are persisted in Postgres with ownership, CSRF, escaping, and body length validation.
+- Realtime receive uses local Go SSE infrastructure; no managed chat or realtime provider was added.
+- Duplicate sender rows and composer reset behavior were fixed during UAT.
+
+## Milestone Summary
+
+**Key accomplishments:**
+
+- Added Google social sign-in while preserving Xtablo-owned sessions and disabling Apple sign-in for the shipped scope.
+- Added one-level etapes for organizing tasks without changing the kanban status model.
+- Added tablo-owned events and a personal planning agenda that aggregates owned events across tablos.
+- Added native persisted realtime discussions using Postgres and Go-owned SSE.
+- Kept the milestone inside the low-dependency Go + HTMX architecture.
+
+**Known deferred items at close:** 7 acknowledged open artifact items. See `STATE.md` Deferred Items and `v2.0-MILESTONE-AUDIT.md`.
+
+**Technical debt carried forward:**
+
+- Phase 8, 9, and 12 lacked strict `*-VERIFICATION.md` aggregation artifacts in the archived milestone audit.
+- Phase 11 validation remained draft even though `11-VERIFICATION.md` passed.
+- Earlier v1 UAT/verification records still contain acknowledged human-needed or partial statuses.
+
+---
+
+For current project status, see `.planning/ROADMAP.md`.
diff --git a/.planning/milestones/v3.0-REQUIREMENTS.md b/.planning/milestones/v3.0-REQUIREMENTS.md
new file mode 100644
index 0000000..8deac6f
--- /dev/null
+++ b/.planning/milestones/v3.0-REQUIREMENTS.md
@@ -0,0 +1,110 @@
+# Requirements Archive: v3.0 Design System & Visual Polish
+
+**Archived:** 2026-05-17
+**Status:** SHIPPED
+
+For current requirements, see `.planning/REQUIREMENTS.md`.
+
+---
+
+# Requirements: Xtablo v3.0 Design System & Visual Polish
+
+**Defined:** 2026-05-16
+**Core Value:** A user can sign in and run the Tablos workflow — organize work, attach files, discuss, and plan scheduled events — without a JS framework or managed chat provider.
+
+## v3.0 Requirements
+
+Requirements for milestone v3.0. Each maps to exactly one roadmap phase.
+
+### Design System
+
+- [x] **DS-01**: CSS design tokens (colors, spacing, typography, shadows, gradients) are defined in `backend/internal/web/ui/base.css` matching the go-backend token vocabulary
+- [x] **DS-02**: Button component with primary, secondary, ghost, and danger variants
+- [x] **DS-03**: Input, Textarea, and Select form field components replace inline raw HTML in all templates
+- [x] **DS-04**: Card component is used across tablo list, detail, and content views
+- [x] **DS-05**: Badge component supports semantic tones (primary, warning, success, danger)
+- [x] **DS-06**: Modal component is available for create/edit dialogs
+- [x] **DS-07**: Empty-state component is available for zero-data views
+- [x] **DS-08**: Table component is available for list views (files, events)
+- [x] **DS-09**: Icon-button component replaces inline icon-button patterns across templates
+
+### Auth Views
+
+- [x] **AUTH-UI-01**: Login page matches the JS app's auth-card layout (gradient background, centered card, brand logo, status banner)
+- [x] **AUTH-UI-02**: Signup page matches the same visual treatment as login
+- [x] **AUTH-UI-03**: Google sign-in button uses the Material Design button style
+
+### Dashboard & Tablos
+
+- [ ] **DASH-01**: Sidebar uses the go-backend sidebar design (brand section, nav items with icons, tablo list, user/account footer)
+- [ ] **DASH-02**: Tablo list uses project-card layout with color accents, creation date, and action controls
+- [ ] **DASH-03**: Dashboard empty state uses the empty-state component
+
+### Tablo Detail
+
+- [x] **DETAIL-01**: Tablo detail header area matches the project-card-top design
+- [x] **DETAIL-02**: Task kanban board uses the tasks-section design (section header, task rows, add control)
+- [x] **DETAIL-03**: Etapes section is visually consistent with the task section
+- [x] **DETAIL-04**: Files section uses the table component
+
+### Chat & Planning
+
+- [x] **CHAT-UI-01**: Discussion view uses consistent card/surface design with message bubbles distinguishing own vs. others
+- [x] **PLAN-UI-01**: Planning page uses the overview-section layout for event aggregation
+
+## Future Requirements
+
+### Responsive / Mobile
+
+- **RESP-01**: Sidebar collapses on small screens (hamburger or bottom-nav pattern)
+- **RESP-02**: Kanban board scrolls horizontally on mobile
+- **RESP-03**: Auth pages adapt gracefully to narrow viewports
+
+### Dark Mode
+
+- **DARK-01**: All CSS tokens have dark-mode counterparts via `prefers-color-scheme` or a class toggle
+
+## Out of Scope
+
+| Feature | Reason |
+|---------|--------|
+| New pages or features | v3.0 is purely visual — no new routes or data models |
+| Billing / Stripe | Deferred from v1.0, still out of scope |
+| Client portal / Admin | Deferred from v1.0, still out of scope |
+| Mobile app | Out of scope for this rewrite milestone |
+| Dark mode | Future requirement — tokens will be structured to enable it, not implement it |
+
+## Traceability
+
+| Requirement | Phase | Status |
+|-------------|-------|--------|
+| DS-01 | Phase 13 | Complete |
+| DS-02 | Phase 13 | Complete |
+| DS-03 | Phase 13 | Complete |
+| DS-04 | Phase 13 | Complete |
+| DS-05 | Phase 13 | Complete |
+| DS-06 | Phase 13 | Complete |
+| DS-07 | Phase 13 | Complete |
+| DS-08 | Phase 13 | Complete |
+| DS-09 | Phase 13 | Complete |
+| AUTH-UI-01 | Phase 14 | Complete |
+| AUTH-UI-02 | Phase 14 | Complete |
+| AUTH-UI-03 | Phase 14 | Complete |
+| DASH-01 | Phase 15 | Pending |
+| DASH-02 | Phase 15 | Pending |
+| DASH-03 | Phase 15 | Pending |
+| DETAIL-01 | Phase 16 | Complete |
+| DETAIL-02 | Phase 16 | Complete |
+| DETAIL-03 | Phase 16 | Complete |
+| DETAIL-04 | Phase 16 | Complete |
+| CHAT-UI-01 | Phase 17 | Complete |
+| PLAN-UI-01 | Phase 17 | Complete |
+
+**Coverage:**
+- v3.0 requirements: 21 total
+- Mapped to phases: 21
+- Unmapped: 0 ✓
+
+---
+*Requirements defined: 2026-05-16*
+*Last updated: 2026-05-16 after initial definition*
diff --git a/.planning/milestones/v3.0-ROADMAP.md b/.planning/milestones/v3.0-ROADMAP.md
new file mode 100644
index 0000000..6e4be10
--- /dev/null
+++ b/.planning/milestones/v3.0-ROADMAP.md
@@ -0,0 +1,169 @@
+# Roadmap: Xtablo v3.0 Design System & Visual Polish
+
+**Created:** 2026-05-16
+**Project mode:** Brownfield milestone on existing Go+HTMX backend
+**Milestone:** v3.0 — Design System & Visual Polish
+
+5 phases, sequential. Phase numbering continues from v2.0, so v3.0 starts at Phase 13.
+
+---
+
+## Previous Milestones
+
+- [x] **v2.0 Collaboration, Planning, and Social Sign-in** — Phases 8-12, shipped 2026-05-16. Archive: [`milestones/v2.0-ROADMAP.md`](milestones/v2.0-ROADMAP.md)
+
+---
+
+## Phase Summary
+
+| # | Phase | Goal | Requirements |
+|---|-------|------|--------------|
+| 13 | 5/5 | Complete | 2026-05-16 |
+| 14 | 1/2 | In Progress| |
+| 15 | 3/3 | Complete | 2026-05-16 |
+| 16 | 4/4 | Complete | 2026-05-16 |
+| 17 | 2/2 | Complete | 2026-05-17 |
+
+---
+
+## Phase Details
+
+### Phase 13: Design System Foundation
+**Goal:** Replace the minimal `backend/internal/web/ui` with a full design system ported from `go-backend/internal/web/ui` — tokens, components, and Tailwind integration — so every subsequent phase has a stable component library to consume.
+**Mode:** mvp
+**Status:** Pending
+**Requirements:** DS-01, DS-02, DS-03, DS-04, DS-05, DS-06, DS-07, DS-08, DS-09
+**Plans:** 5/5 plans complete
+**Success Criteria:**
+1. `backend/internal/web/ui/base.css` defines all CSS custom properties (color, spacing, typography, shadows, gradients) matching the go-backend token vocabulary
+2. All component types are implemented as templ components: button, input, textarea, select, card, badge, modal, empty-state, table, icon-button
+3. `backend/tailwind.input.css` imports all component CSS files and the app shell CSS
+4. A component catalog page (`/ui-catalog`, dev-only) renders all components for visual verification
+5. All existing templates compile and unit tests pass with no regressions
+
+Plans:
+**Wave 1**
+- [x] 13-01-PLAN.md — Token vocabulary + enum/helper foundation (base.css, auth.css extraction, variants.go, helpers.go)
+
+**Wave 2** *(blocked on Wave 1 completion)*
+- [x] 13-02-PLAN.md — Migrate existing components to go-backend API (button multi-class, badge pill, card typed API, template hardcodes)
+
+**Wave 3** *(blocked on Wave 2 completion)*
+- [x] 13-03-PLAN.md — Port form-input components: input, textarea, select, form-field (CSS + templ + tests)
+
+**Wave 4** *(blocked on Wave 3 completion)*
+- [x] 13-04-PLAN.md — Port surface components: modal, empty-state, table, icon-button, space + tailwind.input.css manifest
+
+**Wave 5** *(blocked on Wave 4 completion)*
+- [x] 13-05-PLAN.md — Catalog route (build-tag gated) + visual sign-off checkpoint
+
+**User-in-loop:** Review the catalog page before proceeding to per-view application phases. Confirm token choices (brand color, radius, shadow levels) match what you want the product to look like.
+
+### Phase 14: Auth Pages
+**Goal:** Restyle login and signup pages to match the JS app's auth-card layout using the design system from Phase 13.
+**Mode:** mvp
+**Status:** Pending
+**Requirements:** AUTH-UI-01, AUTH-UI-02, AUTH-UI-03
+**Plans:** 1/2 plans executed
+**Success Criteria:**
+1. Login page has gradient background with animated background layer, centered auth card with brand logo, and status banner using design tokens
+2. Signup page matches the same visual treatment as login
+3. Google sign-in button uses the Material Design button style from the go-backend design
+4. All existing auth handler tests pass unchanged
+5. Browser walkthrough of login and signup matches the go-backend app.css auth-card design
+
+Plans:
+**Wave 1**
+- [x] 14-01-PLAN.md — Auth foundation: logo assets + auth.css replacement + auth_components.templ + auth_layout.templ
+
+**Wave 2** *(blocked on Wave 1 completion)*
+- [ ] 14-02-PLAN.md — Page migration: update auth_login.templ and auth_signup.templ to use AuthLayout + FormField inputs + nav links + browser verify checkpoint
+
+**User-in-loop:** Browser walkthrough checkpoint in Plan 02 — approve visual result before considering the phase complete.
+
+### Phase 15: Dashboard & Tablos
+**Goal:** Restyle the layout shell (sidebar + main) and tablo list/dashboard to match the JS app's sidebar + project-card layout.
+**Mode:** mvp
+**Status:** Pending
+**Requirements:** DASH-01, DASH-02, DASH-03
+**Plans:** 3/3 plans complete
+**Success Criteria:**
+1. Sidebar has brand section, nav items with icons, tablo list section, and user/account footer using sidebar-nav-shell classes
+2. Tablo list uses project-card layout with color avatars, creation date, and action controls
+3. Dashboard empty state uses the empty-state component
+4. All existing tablo CRUD handler tests pass unchanged
+5. Browser walkthrough of tablos list matches the go-backend project-card / sidebar design
+
+Plans:
+**Wave 0**
+- [x] 15-01-PLAN.md — Wave 0 test stubs: TestTablosDashboard_Sidebar, TestTablosDashboard_ProjectCards, TestTablosDashboard_EmptyState (RED baseline for DASH-01/02/03)
+
+**Wave 1** *(blocked on Wave 0 completion)*
+- [x] 15-02-PLAN.md — CSS foundation + AppLayout: app.css ported from go-backend, tailwind.input.css updated, app_layout.templ + app_layout_helpers.go created
+
+**Wave 2** *(blocked on Wave 1 completion)*
+- [x] 15-03-PLAN.md — Dashboard wiring: tablos.templ restyled (project-card grid, ui.EmptyState), handlers updated, planning + account_providers switched to AppLayout, browser verify checkpoint
+
+**User-in-loop:** Approve sidebar shape (nav items, tablo list section) and tablo card layout before implementation.
+
+### Phase 16: Tablo Detail
+**Goal:** Restyle the tablo detail view — header, task kanban, etapes, and files — using design system components.
+**Mode:** mvp
+**Status:** Pending
+**Requirements:** DETAIL-01, DETAIL-02, DETAIL-03, DETAIL-04
+**Plans:** 4/4 plans complete
+**Success Criteria:**
+1. Tablo detail header uses project-card-top layout with title, avatar, and action controls
+2. Task kanban uses tasks-section design: section header with add button, task rows with checkbox and meta
+3. Etapes section uses the same card/section visual pattern as tasks
+4. Files section uses the table component with consistent row actions
+5. All existing task, etape, and file handler tests pass unchanged
+
+Plans:
+**Wave 1**
+- [x] 16-01-PLAN.md — CSS foundation + icons: append CSS Sections 19–25 to app.css; add download + chat icon cases to UIIcon switch
+
+**Wave 2** *(blocked on Wave 1 completion)*
+- [x] 16-02-PLAN.md — Header + tab nav + overview tab: restyle TabloDetailPage header (project-card-top), metadata row, tab nav (design token classes), move desc to overview tab, remove EtapeStrip call from TasksTabFragment
+
+**Wave 3** *(blocked on Wave 2 completion)*
+- [x] 16-03-PLAN.md — Kanban + etape grouping: groupTasksByEtape helper, restyled KanbanBoard/Column/TaskCard, EtapeStrip OOB removal, handlers_tasks.go call sites updated
+
+**Wave 4** *(blocked on Wave 3 completion)*
+- [x] 16-04-PLAN.md — Files section: @ui.Table, @ui.EmptyState, FileListRow as tr, FileDeleteConfirmFragment as tr, browser verify checkpoint
+
+**User-in-loop:** Browser verify checkpoint in Plan 04 — approve visual result before considering the phase complete.
+
+### Phase 17: Chat & Planning
+**Goal:** Restyle the discussion view and planning page using design system components to make them visually consistent with the rest of the app.
+**Mode:** mvp
+**Status:** Pending
+**Requirements:** CHAT-UI-01, PLAN-UI-01
+**Plans:** 2/2 plans complete
+**Success Criteria:**
+1. Discussion view uses card/surface design; own messages vs. others are visually differentiated
+2. Planning page uses overview-section layout with chronological event list
+3. All existing chat and planning handler tests pass unchanged
+4. Browser walkthrough confirms both views look consistent with the Phase 15–16 restyled surfaces
+
+Plans:
+**Wave 1** *(both plans parallel — no shared files)*
+- [x] 17-01-PLAN.md — Discussion view: CSS message-bubble classes + DiscussionTabData view model + ChatMainContent() component + handler wiring + browser verify
+- [x] 17-02-PLAN.md — Planning view: h1 selector fix + PlanningTabData view model + PlanningShowDaySeparator + PlanningMainContent() component + handler wiring + browser verify
+
+**User-in-loop:** Browser verify checkpoints in both plans — approve visual result before considering the phase complete.
+
+---
+
+## Coverage
+
+**21 requirements mapped across 5 phases**
+
+| Phase | Requirements | Count |
+|-------|-------------|-------|
+| 13 | DS-01, DS-02, DS-03, DS-04, DS-05, DS-06, DS-07, DS-08, DS-09 | 9 |
+| 14 | AUTH-UI-01, AUTH-UI-02, AUTH-UI-03 | 3 |
+| 15 | DASH-01, DASH-02, DASH-03 | 3 |
+| 16 | DETAIL-01, DETAIL-02, DETAIL-03, DETAIL-04 | 4 |
+| 17 | CHAT-UI-01, PLAN-UI-01 | 2 |
+| **Total** | | **21 / 21 ✓** |
diff --git a/.planning/milestones/v3.0-phases/13-design-system-foundation/13-01-PLAN.md b/.planning/milestones/v3.0-phases/13-design-system-foundation/13-01-PLAN.md
new file mode 100644
index 0000000..f40b8d8
--- /dev/null
+++ b/.planning/milestones/v3.0-phases/13-design-system-foundation/13-01-PLAN.md
@@ -0,0 +1,258 @@
+---
+phase: 13-design-system-foundation
+plan: 01
+type: execute
+wave: 1
+depends_on: []
+files_modified:
+ - backend/internal/web/ui/base.css
+ - backend/internal/web/ui/auth.css
+ - backend/internal/web/ui/variants.go
+ - backend/internal/web/ui/helpers.go
+ - backend/tailwind.input.css
+autonomous: true
+requirements:
+ - DS-01
+
+must_haves:
+ truths:
+ - "base.css contains the full 223-line CSS custom property vocabulary from go-backend"
+ - "auth-provider button styles are preserved in auth.css (not lost when button.css is replaced)"
+ - "variants.go declares ButtonVariantGhost, BadgeVariantPrimary, IconButtonVariant, IconButtonTone, and SpacingStep enums"
+ - "helpers.go declares buttonType, inputType, inputID, and textareaRows helper functions"
+ - "tailwind.input.css imports auth.css so the login page retains provider button styling"
+ - "go test ./internal/web/ui/... passes (no regressions from enum additions)"
+ artifacts:
+ - path: "backend/internal/web/ui/base.css"
+ provides: "Full CSS custom property token vocabulary"
+ contains: "--color-brand-primary"
+ - path: "backend/internal/web/ui/auth.css"
+ provides: "Auth provider button CSS extracted from button.css"
+ contains: ".auth-provider-button"
+ - path: "backend/internal/web/ui/variants.go"
+ provides: "All variant enums including Ghost, Primary, IconButton, SpacingStep"
+ contains: "ButtonVariantGhost"
+ - path: "backend/internal/web/ui/helpers.go"
+ provides: "Helper functions for templ components"
+ contains: "buttonType"
+ key_links:
+ - from: "backend/tailwind.input.css"
+ to: "backend/internal/web/ui/auth.css"
+ via: "@import"
+ pattern: "auth\\.css"
+ - from: "backend/internal/web/ui/variants.go"
+ to: "ButtonVariantGhost"
+ via: "const block"
+ pattern: "ButtonVariantGhost"
+---
+
+## Phase Goal
+
+**As a** developer, **I want to** have a complete CSS token vocabulary and Go variant enums in the backend, **so that** every subsequent plan can port component CSS and templ files against a stable foundation without losing any existing styling.
+
+
+Replace the 28-line backend/base.css stub with the full 223-line token vocabulary from go-backend,
+extract auth-provider CSS from button.css into auth.css before button.css is replaced,
+and extend variants.go + helpers.go with all new enums and helper functions.
+
+Purpose: This is the mandatory prerequisite wave. Without the full token vocabulary, component CSS
+ported in Plans 02–04 cannot reference var(--...) tokens. Without auth.css extraction, replacing
+button.css in Plan 02 silently destroys the login page's provider button styling.
+
+Output:
+- backend/internal/web/ui/base.css — 223-line token vocabulary (verbatim port, per D-T01/T02/T03)
+- backend/internal/web/ui/auth.css — auth-provider selectors extracted from current button.css
+- backend/internal/web/ui/variants.go — extended with Ghost/Primary variants + new enums
+- backend/internal/web/ui/helpers.go — extended with buttonType/inputType/inputID/textareaRows
+- backend/tailwind.input.css — auth.css import added (button.css replacement in Plan 02)
+
+
+
+@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.claude/get-shit-done/workflows/execute-plan.md
+@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/ROADMAP.md
+@.planning/REQUIREMENTS.md
+@.planning/phases/13-design-system-foundation/13-CONTEXT.md
+@.planning/phases/13-design-system-foundation/13-RESEARCH.md
+@.planning/phases/13-design-system-foundation/13-PATTERNS.md
+
+
+
+
+
+ Task 1: Extract auth.css and replace base.css
+
+ backend/internal/web/ui/auth.css,
+ backend/internal/web/ui/base.css,
+ backend/tailwind.input.css
+
+
+ - backend/internal/web/ui/button.css (lines 121–180 contain the auth-provider CSS to extract verbatim)
+ - backend/internal/web/ui/base.css (current 28-line stub to understand what is being replaced)
+ - go-backend/internal/web/ui/base.css (the 223-line source to port verbatim)
+ - backend/tailwind.input.css (current 4-import manifest to update)
+
+
+ Step 1 — Create backend/internal/web/ui/auth.css: copy lines 121–180 from the current
+ backend/internal/web/ui/button.css verbatim. The first line must be a comment:
+ "/* auth.css — sign-in provider controls, extracted from button.css in Phase 13 */".
+ The file must contain the selectors: .auth-provider-stack, .auth-provider-button,
+ .auth-provider-button:hover, .auth-provider-button:focus-visible,
+ .auth-provider-button-disabled, .auth-provider-separator, .auth-provider-separator span,
+ .auth-provider-separator em.
+
+ Step 2 — Replace backend/internal/web/ui/base.css entirely with the contents of
+ go-backend/internal/web/ui/base.css (verbatim — per D-T01, D-T02, D-T03).
+ Do NOT merge with the existing 28-line stub. The target file is 223 lines.
+
+ Step 3 — Update backend/tailwind.input.css: add the line
+ "@import "./internal/web/ui/auth.css";" immediately after the base.css import line.
+ Do not yet add imports for the component CSS files being created in Plans 02–04 — those
+ are added in Plan 04.
+
+
+ grep -c 'color-brand-primary' backend/internal/web/ui/base.css
+ Expected output: at least 3 (the token appears in multiple rules).
+ Also: grep -c 'auth-provider-button' backend/internal/web/ui/auth.css — must return at least 3.
+ Also: grep 'auth\.css' backend/tailwind.input.css — must match.
+
+
+ - backend/internal/web/ui/base.css is exactly 223 lines (or close — the exact line count from go-backend)
+ - backend/internal/web/ui/base.css contains "--color-brand-primary: #804eec"
+ - backend/internal/web/ui/base.css contains "--color-text-primary"
+ - backend/internal/web/ui/base.css contains "--shadow-surface-md"
+ - backend/internal/web/ui/base.css does NOT contain "box-sizing: border-box" at the top (the stub had that; go-backend version puts it inside :root or omits it)
+ - backend/internal/web/ui/auth.css exists and contains ".auth-provider-button {"
+ - backend/internal/web/ui/auth.css contains ".auth-provider-separator {"
+ - backend/tailwind.input.css contains "@import "./internal/web/ui/auth.css";"
+ - The auth.css import appears after the base.css import in tailwind.input.css
+
+ base.css replaced with 223-line token vocabulary; auth.css created with extracted provider styles; tailwind.input.css updated with auth.css import
+
+
+
+ Task 2: Extend variants.go with new enums and helpers.go with new helper functions
+
+ backend/internal/web/ui/variants.go,
+ backend/internal/web/ui/helpers.go,
+ backend/internal/web/ui/ui_test.go
+
+
+ - backend/internal/web/ui/variants.go (current file — full read to understand existing enum patterns)
+ - backend/internal/web/ui/helpers.go (current file — full read to understand existing helper pattern)
+ - backend/internal/web/ui/ui_test.go (current test file — understand existing test structure)
+ - go-backend/internal/web/ui/variants.go (source for new enums and class functions)
+ - go-backend/internal/web/ui/helpers.go (source for new helper functions)
+ - 13-PATTERNS.md (Pattern for variants.go and helpers.go changes)
+ - 13-RESEARCH.md (Code Examples section for exact function signatures)
+
+
+ - Test: NormalizedButtonVariant(ButtonVariantGhost) returns ButtonVariantGhost (not ButtonVariantDefault)
+ - Test: ButtonClass(ButtonVariantGhost, ButtonToneSolid, SizeMD) contains "ui-button-ghost"
+ - Test: NormalizedBadgeVariant(BadgeVariantPrimary) returns BadgeVariantPrimary
+ - Test: BadgeClass(BadgeVariantPrimary) == "ui-badge ui-badge-primary"
+ - Test: IconButtonClass(IconButtonVariantNeutral, IconButtonToneGhost) contains "borderless-icon-button"
+ - Test: IconButtonClass(IconButtonVariantNeutral, IconButtonToneSolid) contains "ui-icon-button-solid"
+ - Test: SpaceXClass(SpacingStepMD) == "ui-space-x ui-space-x-md"
+ - Test: SpaceYClass(SpacingStepLG) == "ui-space-y ui-space-y-lg"
+
+
+ Step 1 — Write the failing tests in ui_test.go first (RED). Add test functions for:
+ TestButtonVariantGhost_Normalizer, TestButtonClass_GhostVariant,
+ TestBadgeVariantPrimary_Normalizer, TestBadgeClass_PrimaryVariant,
+ TestIconButtonClass_GhostNeutral, TestIconButtonClass_SolidNeutral,
+ TestSpaceXClass_MD, TestSpaceYClass_LG.
+ Run go test ./internal/web/ui/... and confirm failures (RED).
+
+ Step 2 — Update variants.go (GREEN):
+ a. Add "ButtonVariantGhost ButtonVariant = "ghost"" to the ButtonVariant const block.
+ b. Add "ButtonVariantGhost" case to NormalizedButtonVariant switch: "case ButtonVariantGhost: return variant".
+ c. Add "BadgeVariantPrimary BadgeVariant = "primary"" to the BadgeVariant const block.
+ d. Add "BadgeVariantPrimary" case to NormalizedBadgeVariant switch.
+ e. Add IconButtonVariant type with constants: IconButtonVariantNeutral="neutral",
+ IconButtonVariantWarning="warning", IconButtonVariantSuccess="success",
+ IconButtonVariantDanger="danger".
+ f. Add IconButtonTone type with constants: IconButtonToneSolid="solid", IconButtonToneGhost="ghost".
+ g. Add SpacingStep type with constants: SpacingStepXS="xs", SpacingStepSM="sm",
+ SpacingStepMD="md", SpacingStepLG="lg", SpacingStepXL="xl".
+ h. Add NormalizedIconButtonVariant function (defaults to IconButtonVariantNeutral).
+ i. Add NormalizedIconButtonTone function (defaults to IconButtonToneSolid).
+ j. Add NormalizedSpacingStep function (defaults to SpacingStepMD).
+ k. Add IconButtonClass(variant IconButtonVariant, tone IconButtonTone) string function.
+ Ghost tone path: returns "borderless-icon-button ui-icon-button-ghost ui-icon-button-" + normalizedVariant.
+ Solid tone path: returns "ui-icon-button ui-icon-button-solid ui-icon-button-" + normalizedVariant.
+ l. Add SpaceXClass(step SpacingStep) string — returns "ui-space-x ui-space-x-" + normalizedStep.
+ m. Add SpaceYClass(step SpacingStep) string — returns "ui-space-y ui-space-y-" + normalizedStep.
+
+ Step 3 — Update helpers.go (GREEN):
+ Add four new unexported helper functions (following the existing mergeAttrs pattern):
+ a. buttonType(value string) string — returns "button" if value is empty, otherwise value.
+ b. inputType(value string) string — returns "text" if value is empty, otherwise value.
+ c. inputID(id string, name string) string — returns id if non-empty, otherwise name.
+ d. textareaRows(rows int) string — returns strconv.Itoa(rows) if rows > 0, else "4".
+ Add import "strconv" to helpers.go.
+
+ Do NOT change ButtonClass() output yet — that is Plan 02's work.
+ Run go test ./internal/web/ui/... and confirm all tests pass (GREEN).
+
+
+ cd backend && go test ./internal/web/ui/... -run "TestButtonVariantGhost|TestBadgeVariantPrimary|TestIconButtonClass|TestSpaceXClass|TestSpaceYClass" -v
+
+
+ - All 8 new test functions pass
+ - go test ./internal/web/ui/... (full suite) is green — no existing tests broken
+ - variants.go contains "ButtonVariantGhost ButtonVariant = "ghost""
+ - variants.go contains "BadgeVariantPrimary BadgeVariant = "primary""
+ - variants.go contains type IconButtonVariant string
+ - variants.go contains type SpacingStep string
+ - variants.go contains func IconButtonClass
+ - variants.go contains func SpaceXClass
+ - helpers.go imports "strconv"
+ - helpers.go contains func buttonType
+ - helpers.go contains func textareaRows
+ - ButtonClass() still returns the OLD compound pattern (ui-button-solid-default-md) — this is intentional; Plan 02 migrates it
+
+ variants.go extended with all new enums and class functions; helpers.go extended with component helper functions; all tests green
+
+
+
+
+
+## Trust Boundaries
+
+| Boundary | Description |
+|----------|-------------|
+| Static build → browser | CSS compiled by Tailwind CLI; no runtime input |
+
+## STRIDE Threat Register
+
+| Threat ID | Category | Component | Disposition | Mitigation Plan |
+|-----------|----------|-----------|-------------|-----------------|
+| T-13-01-01 | Information Disclosure | base.css token values | accept | Token values are design constants (brand colors, spacing). No secrets. Acceptable public visibility. |
+| T-13-01-02 | Tampering | variants.go normalizer | accept | Normalizers return safe defaults for unknown inputs; no user-controlled data flows through variant enums at runtime. |
+
+
+
+After this plan completes:
+- cd backend && go test ./internal/web/ui/... — must be green
+- grep -c 'color-brand-primary' backend/internal/web/ui/base.css — must return >= 3
+- grep 'ButtonVariantGhost' backend/internal/web/ui/variants.go — must match
+- grep 'auth-provider-button' backend/internal/web/ui/auth.css — must match
+- grep 'auth\.css' backend/tailwind.input.css — must match
+
+
+
+1. base.css contains the full go-backend token vocabulary (223 lines, --color-brand-primary: #804eec present)
+2. auth.css exists with .auth-provider-button and is imported in tailwind.input.css
+3. variants.go has ButtonVariantGhost, BadgeVariantPrimary, IconButtonVariant, IconButtonTone, SpacingStep
+4. variants.go has IconButtonClass(), SpaceXClass(), SpaceYClass() functions
+5. helpers.go has buttonType(), inputType(), inputID(), textareaRows()
+6. go test ./internal/web/ui/... is green
+
+
+
diff --git a/.planning/milestones/v3.0-phases/13-design-system-foundation/13-01-SUMMARY.md b/.planning/milestones/v3.0-phases/13-design-system-foundation/13-01-SUMMARY.md
new file mode 100644
index 0000000..24b01d2
--- /dev/null
+++ b/.planning/milestones/v3.0-phases/13-design-system-foundation/13-01-SUMMARY.md
@@ -0,0 +1,92 @@
+---
+phase: 13-design-system-foundation
+plan: "01"
+subsystem: backend/internal/web/ui
+tags: [css-tokens, go-enums, design-system, foundation]
+dependency_graph:
+ requires: []
+ provides: [css-token-vocabulary, variant-enums, helper-functions, auth-css]
+ affects: [backend/tailwind.input.css, backend/internal/web/ui/variants.go, backend/internal/web/ui/helpers.go]
+tech_stack:
+ added: []
+ patterns: [verbatim-port, tdd-red-green, enum-extension]
+key_files:
+ created:
+ - backend/internal/web/ui/auth.css
+ modified:
+ - backend/internal/web/ui/base.css
+ - backend/internal/web/ui/variants.go
+ - backend/internal/web/ui/helpers.go
+ - backend/internal/web/ui/ui_test.go
+ - backend/tailwind.input.css
+decisions:
+ - "ButtonClass() retains compound format (ui-button-solid-ghost-md) until Plan 02 migrates it to multi-class"
+ - "TestButtonClass_GhostVariant asserts 'ghost' substring rather than 'ui-button-ghost' standalone class, matching the preserved compound format"
+ - "auth-provider CSS extracted verbatim from button.css Phase 8 block (lines 121-180) into standalone auth.css"
+metrics:
+ duration: "~4 minutes"
+ completed_date: "2026-05-16"
+ tasks: 2
+ files: 5
+---
+
+# Phase 13 Plan 01: Design System Foundation — Token Vocabulary and Enum Extension Summary
+
+Full CSS custom property vocabulary (223-line go-backend port) plus auth-provider CSS extraction, Ghost/Primary variant enums, IconButton/SpacingStep enum types, and component helper functions added to backend.
+
+## Tasks Completed
+
+| Task | Name | Commit | Key Files |
+|------|------|--------|-----------|
+| 1 | Extract auth.css and replace base.css | 59e39fe | auth.css (new), base.css (replaced), tailwind.input.css (updated) |
+| 2 | Extend variants.go and helpers.go (TDD) | 8602eb1 (RED), d149965 (GREEN) | variants.go, helpers.go, ui_test.go |
+
+## What Was Built
+
+**Task 1 — CSS foundation:**
+- Replaced the 28-line `backend/internal/web/ui/base.css` stub with the full 223-line `:root` CSS custom property vocabulary verbatim-ported from `go-backend/internal/web/ui/base.css`. Contains `--color-brand-primary: #804eec`, `--color-text-primary`, `--shadow-surface-md`, and all token categories (text, surfaces, borders, brand, status, effects, gradients, legacy aliases).
+- Created `backend/internal/web/ui/auth.css` with the 8 auth-provider selectors (`.auth-provider-stack`, `.auth-provider-button`, `.auth-provider-button:hover`, `.auth-provider-button:focus-visible`, `.auth-provider-button-disabled`, `.auth-provider-separator`, `.auth-provider-separator span`, `.auth-provider-separator em`) extracted verbatim from `button.css` lines 121-180.
+- Added `@import "./internal/web/ui/auth.css";` to `backend/tailwind.input.css` after the base.css import line.
+
+**Task 2 — Go enum and helper extension (TDD):**
+- Extended `variants.go` with `ButtonVariantGhost`, `BadgeVariantPrimary`, `IconButtonVariant` type (Neutral/Warning/Success/Danger), `IconButtonTone` type (Solid/Ghost), `SpacingStep` type (XS/SM/MD/LG/XL).
+- Added exported normalizer functions: `NormalizedIconButtonVariant`, `NormalizedIconButtonTone`, `NormalizedSpacingStep`.
+- Added exported class functions: `IconButtonClass`, `SpaceXClass`, `SpaceYClass`.
+- Updated `NormalizedButtonVariant` to pass through `ButtonVariantGhost`; `NormalizedBadgeVariant` to pass through `BadgeVariantPrimary`.
+- Extended `helpers.go` with `buttonType`, `inputType`, `inputID`, `textareaRows` (with `strconv` import).
+- All 18 tests in `ui_test.go` pass (10 existing + 8 new).
+
+## Verification Results
+
+- `go test ./internal/web/ui/... PASS` (18/18 tests)
+- `grep -c 'color-brand-primary' backend/internal/web/ui/base.css` returns 4
+- `grep 'ButtonVariantGhost' backend/internal/web/ui/variants.go` matches
+- `grep 'auth-provider-button' backend/internal/web/ui/auth.css` returns 5 matches
+- `grep 'auth.css' backend/tailwind.input.css` matches
+
+## Deviations from Plan
+
+### Auto-fixed Issues
+
+**1. [Rule 1 - Bug] Fixed TestButtonClass_GhostVariant test assertion**
+- **Found during:** Task 2 GREEN phase
+- **Issue:** The plan's behavior spec stated `ButtonClass(ButtonVariantGhost, ButtonToneSolid, SizeMD) contains "ui-button-ghost"`, but the plan also explicitly requires `ButtonClass()` to retain the old compound format (`ui-button-solid-ghost-md`) until Plan 02 migrates it. The compound format produces `"ui-button-solid-ghost-md"` which does NOT contain the substring `"ui-button-ghost"` — only `"ghost"`.
+- **Fix:** Changed the test assertion from `strings.Contains(got, "ui-button-ghost")` to `strings.Contains(got, "ghost")` with a comment explaining the compound format is intentionally preserved for Plan 02.
+- **Files modified:** `backend/internal/web/ui/ui_test.go`
+- **Commit:** d149965
+
+## Known Stubs
+
+None — this plan creates foundational infrastructure only (CSS tokens, Go enums, helpers). No UI rendering or data wiring involved.
+
+## Threat Flags
+
+No new network endpoints, auth paths, file access patterns, or schema changes introduced.
+
+## TDD Gate Compliance
+
+- RED gate: commit 8602eb1 (`test(13-01): add failing tests for new variant enums and class functions (RED)`)
+- GREEN gate: commit d149965 (`feat(13-01): extend variants.go with new enums and helpers.go with helper functions (GREEN)`)
+- REFACTOR gate: Not needed — implementation was clean on first pass.
+
+## Self-Check: PASSED
diff --git a/.planning/milestones/v3.0-phases/13-design-system-foundation/13-02-PLAN.md b/.planning/milestones/v3.0-phases/13-design-system-foundation/13-02-PLAN.md
new file mode 100644
index 0000000..8de45b9
--- /dev/null
+++ b/.planning/milestones/v3.0-phases/13-design-system-foundation/13-02-PLAN.md
@@ -0,0 +1,314 @@
+---
+phase: 13-design-system-foundation
+plan: 02
+type: execute
+wave: 2
+depends_on:
+ - 13-01
+files_modified:
+ - backend/internal/web/ui/button.css
+ - backend/internal/web/ui/button.templ
+ - backend/internal/web/ui/badge.css
+ - backend/internal/web/ui/badge.templ
+ - backend/internal/web/ui/card.css
+ - backend/internal/web/ui/card.templ
+ - backend/internal/web/ui/variants.go
+ - backend/internal/web/ui/ui_test.go
+ - backend/templates/planning.templ
+ - backend/templates/tasks.templ
+ - backend/templates/events.templ
+ - backend/templates/etapes.templ
+autonomous: true
+requirements:
+ - DS-02
+ - DS-04
+ - DS-05
+
+must_haves:
+ truths:
+ - "ButtonClass() emits four separate classes: ui-button, ui-button-{tone}, ui-button-{variant}, ui-button-{size}"
+ - "All hardcoded compound button class strings in templates are updated to multi-class equivalents"
+ - "ButtonVariantGhost class combo renders with transparent background and brand color text"
+ - "BadgeVariantPrimary renders with brand-purple surface"
+ - "Card uses typed Header/Body/Footer templ.Component fields (not children passthrough)"
+ - "go test ./... passes — no regressions in any package"
+ - "just generate succeeds — all templ files compile"
+ artifacts:
+ - path: "backend/internal/web/ui/button.css"
+ provides: "Multi-class button selectors + ghost variant"
+ contains: ".ui-button-solid.ui-button-default {"
+ - path: "backend/internal/web/ui/card.templ"
+ provides: "Typed Header/Body/Footer Props API"
+ contains: "CardProps"
+ - path: "backend/internal/web/ui/variants.go"
+ provides: "ButtonClass() emitting multi-class output"
+ contains: "ui-button-"
+ key_links:
+ - from: "backend/templates/planning.templ"
+ to: "multi-class button pattern"
+ via: "class attribute strings"
+ pattern: "ui-button ui-button-soft ui-button-neutral"
+ - from: "backend/internal/web/ui/button.templ"
+ to: "ButtonClass()"
+ via: "ButtonClass(props.Variant, props.Tone, props.Size)"
+ pattern: "ButtonClass"
+---
+
+## Phase Goal
+
+**As a** developer, **I want to** migrate existing Button/Badge/Card components to go-backend's API (multi-class button pattern, typed Card fields, new variants), **so that** buttons render correctly from the ported button.css and templates remain functional with no visual regressions.
+
+
+Migrate the three existing components (Button, Badge, Card) to match go-backend's API and CSS.
+The critical task is the button multi-class migration — both the CSS selectors and the class
+generation function must change atomically, and all hardcoded compound class strings in templates
+must be updated in the same wave.
+
+Purpose: This wave closes the gap between the current compound-class pattern and go-backend's
+multi-class pattern. Without this, porting button.css (which uses compound CSS selectors like
+.ui-button-solid.ui-button-default) would silently break all button rendering.
+
+Output:
+- button.css replaced with go-backend's multi-class selector version + new ghost rules
+- ButtonClass() updated to emit "ui-button ui-button-solid ui-button-default ui-button-md"
+- button.templ updated with Icon field and buttonType() helper (from Plan 01 helpers.go)
+- badge.css replaced with go-backend's pill-shape version + new primary variant
+- card.css replaced with go-backend's token-based version
+- card.templ migrated from children to typed Header/Body/Footer fields
+- All compound class strings in planning.templ, tasks.templ, events.templ, etapes.templ updated
+- ui_test.go updated to match new patterns (multi-class assertions, card typed API)
+
+
+
+@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.claude/get-shit-done/workflows/execute-plan.md
+@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/phases/13-design-system-foundation/13-CONTEXT.md
+@.planning/phases/13-design-system-foundation/13-RESEARCH.md
+@.planning/phases/13-design-system-foundation/13-PATTERNS.md
+@.planning/phases/13-design-system-foundation/13-01-SUMMARY.md
+
+
+
+From backend/internal/web/ui/variants.go (after Plan 01):
+ func ButtonClass(variant ButtonVariant, tone ButtonTone, size Size) string
+ // Currently returns: "ui-button ui-button-solid-default-md" — Plan 02 changes this output
+ // Target: "ui-button ui-button-solid ui-button-default ui-button-md"
+
+From backend/internal/web/ui/helpers.go (after Plan 01):
+ func buttonType(value string) string // returns "button" if empty
+ func UIIcon(kind string) templ.Component // does NOT exist yet — created in Plan 04
+
+Compound class → multi-class mapping (all occurrences to replace in templates):
+ "ui-button ui-button-solid-default-md" → "ui-button ui-button-solid ui-button-default ui-button-md"
+ "ui-button ui-button-soft-neutral-md" → "ui-button ui-button-soft ui-button-neutral ui-button-md"
+ "ui-button ui-button-soft-danger-md" → "ui-button ui-button-soft ui-button-danger ui-button-md"
+
+
+
+
+
+ Task 1: Update ButtonClass() multi-class output + button.templ + update ui_test.go button assertions
+
+ backend/internal/web/ui/variants.go,
+ backend/internal/web/ui/button.templ,
+ backend/internal/web/ui/ui_test.go
+
+
+ - backend/internal/web/ui/variants.go (current — confirm ButtonClass() still uses old pattern after Plan 01)
+ - backend/internal/web/ui/button.templ (current — full file)
+ - backend/internal/web/ui/ui_test.go (current — find TestButton_DefaultSolidMD, TestButtonClass_String assertions to update)
+ - go-backend/internal/web/ui/button.templ (source for updated Props struct with Icon field)
+ - 13-PATTERNS.md (button.templ target pattern section)
+ - 13-RESEARCH.md (Multi-Class Button Pattern section, Code Examples section)
+
+
+ - Test: ButtonClass(ButtonVariantDefault, ButtonToneSolid, SizeMD) == "ui-button ui-button-solid ui-button-default ui-button-md"
+ - Test: ButtonClass(ButtonVariantGhost, ButtonToneSolid, SizeMD) contains "ui-button-ghost" and does NOT contain "ui-button-solid"
+ - Test: Button(ButtonProps{Label: "x"}) renders class attribute containing "ui-button ui-button-solid" (not "ui-button-solid-default-md")
+
+
+ Step 1 — Update the test assertions in ui_test.go (they will now FAIL — this is intentional RED state):
+ a. TestButton_DefaultSolidMD: change wantClass from "ui-button ui-button-solid-default-md"
+ to a for-loop checking for each of: "ui-button", "ui-button-solid", "ui-button-default", "ui-button-md"
+ using the multi-assertion slice pattern from 13-PATTERNS.md.
+ b. TestButtonClass_String: change want from "ui-button ui-button-solid-default-md"
+ to "ui-button ui-button-solid ui-button-default ui-button-md".
+ Run go test ./internal/web/ui/... — confirm these two tests FAIL (RED).
+
+ Step 2 — Update ButtonClass() in variants.go (GREEN):
+ Change the return statement from:
+ "ui-button ui-button-" + string(t) + "-" + string(v) + "-" + string(s)
+ to:
+ "ui-button ui-button-" + string(t) + " ui-button-" + string(v) + " ui-button-" + string(s)
+ The ghost variant is a special case: when variant is ButtonVariantGhost, the tone class is
+ omitted. Add a conditional: if v == ButtonVariantGhost, return "ui-button ui-button-ghost ui-button-" + string(s).
+
+ Step 3 — Update button.templ:
+ a. Add "Icon string" field to ButtonProps struct (after the Size field).
+ b. Replace the inline btnType variable logic with: type={ buttonType(props.Type) }.
+ c. Replace the class variable lookup with: class={ ButtonClass(props.Variant, props.Tone, props.Size) }.
+ d. Add icon rendering inside the button: if props.Icon != "" { — leave
+ a placeholder comment "// UIIcon added in Plan 04" because UIIcon does not exist yet.
+ DO NOT reference UIIcon yet — it does not compile until Plan 04. Instead, skip the icon rendering
+ entirely in the templ for now: the Icon field is present in the struct, but icon rendering is
+ wired in Plan 04 when icon_button.templ creates UIIcon.
+
+ Run go test ./internal/web/ui/... — confirm all tests pass (GREEN).
+ Run cd backend && just generate — confirm templ generates without errors.
+
+
+ cd backend && just generate && go test ./internal/web/ui/... -run "TestButton" -v
+
+
+ - ButtonClass(ButtonVariantDefault, ButtonToneSolid, SizeMD) returns "ui-button ui-button-solid ui-button-default ui-button-md" (not the old compound form)
+ - ButtonClass(ButtonVariantGhost, ButtonToneSolid, SizeMD) returns "ui-button ui-button-ghost ui-button-md"
+ - TestButton_DefaultSolidMD passes with multi-class assertions
+ - TestButtonClass_String passes with new expected string
+ - button.templ uses buttonType(props.Type) helper
+ - ButtonProps struct contains "Icon string" field
+ - just generate succeeds (no templ compile errors)
+ - All other existing tests still pass (TestButton_PassesThroughAttrs, TestButton_ExplicitTypeSubmit)
+
+ ButtonClass() emits multi-class output; button.templ updated with Icon field and buttonType helper; all button tests green
+
+
+
+ Task 2: Replace button.css + badge.css + card.css; migrate card.templ; update template hardcodes; extend ui_test.go
+
+ backend/internal/web/ui/button.css,
+ backend/internal/web/ui/badge.css,
+ backend/internal/web/ui/card.css,
+ backend/internal/web/ui/card.templ,
+ backend/internal/web/ui/ui_test.go,
+ backend/templates/planning.templ,
+ backend/templates/tasks.templ,
+ backend/templates/events.templ,
+ backend/templates/etapes.templ
+
+
+ - backend/internal/web/ui/button.css (current — 180 lines; lines 1–120 are button CSS, lines 121–180 are auth-provider CSS that was extracted in Plan 01 but the file still needs full review)
+ - backend/internal/web/ui/badge.css (current — see current selectors)
+ - backend/internal/web/ui/card.css (current — see current selectors)
+ - backend/internal/web/ui/card.templ (current — children-based API to replace)
+ - backend/internal/web/ui/ui_test.go (current — TestCard_RendersChildren to rewrite)
+ - go-backend/internal/web/ui/button.css (source for multi-class selectors)
+ - go-backend/internal/web/ui/badge.css (source for pill shape + token colors)
+ - go-backend/internal/web/ui/card.css (source for header/body/footer pattern)
+ - go-backend/internal/web/ui/card.templ (source for typed Props API)
+ - 13-PATTERNS.md (button.css, badge.css, card.css, card.templ, template hardcode sections)
+ - 13-RESEARCH.md (Pitfall 1 — template compound class strings; Pitfall 3 — Card API break)
+
+
+ - Test (rewritten): Card(CardProps{Body: textComponent("hello")}) renders "ui-card-body" in HTML and "hello" inside it
+ - Test (new): BadgeClass(BadgeVariantPrimary) returns "ui-badge ui-badge-primary"
+ - Test (new): Badge(BadgeProps{Label: "x", Variant: BadgeVariantPrimary}) HTML contains "ui-badge-primary"
+
+
+ Step 1 — Rewrite TestCard_RendersChildren in ui_test.go to TestCard_RendersTypedRegions:
+ Add textComponent helper function (from 13-PATTERNS.md) and add imports "io".
+ New test passes CardProps{Header: textComponent("header"), Body: textComponent("body")}
+ and asserts "ui-card-header", "header", "ui-card-body", "body" are all in the output.
+ Add test for nil footer: CardProps{Body: textComponent("x")} must NOT contain "ui-card-footer".
+ Add TestBadge_PrimaryVariant: Badge(BadgeProps{Label: "x", Variant: BadgeVariantPrimary})
+ must contain "ui-badge-primary".
+ Run go test — these will fail RED (card.templ not yet migrated, PrimaryVariant normalizer not yet applied to BadgeClass).
+
+ Step 2 — Replace button.css with go-backend's multi-class version:
+ Port go-backend/internal/web/ui/button.css verbatim (multi-class selectors like
+ ".ui-button-solid.ui-button-default { background: var(--color-brand-primary); }").
+ Do NOT include the auth-provider selectors (those are in auth.css from Plan 01).
+ After the go-backend content, append the ghost variant rules (per D-CA01):
+ .ui-button-ghost { background: transparent; color: var(--color-brand-primary); }
+ .ui-button-ghost:hover { background: var(--color-surface-brand-soft); }
+ .ui-button-ghost:focus-visible { box-shadow: 0 0 0 3px var(--color-focus-ring); outline: none; }
+
+ Step 3 — Replace badge.css with go-backend's version:
+ Port go-backend/internal/web/ui/badge.css verbatim (pill shape, border-radius: 999px).
+ After the go-backend content, append the primary variant (per D-CA02):
+ .ui-badge-primary { background: var(--color-surface-brand-soft); border-color: rgba(128, 78, 236, 0.3); color: var(--color-text-brand); }
+
+ Step 4 — Replace card.css with go-backend's version:
+ Port go-backend/internal/web/ui/card.css verbatim (ui-card, ui-card-header, ui-card-body, ui-card-footer).
+
+ Step 5 — Migrate card.templ to typed Props API:
+ Replace the current children-based Card(attrs templ.Attributes) with:
+ type CardProps struct { Header templ.Component; Body templ.Component; Footer templ.Component }
+ templ Card(props CardProps) — with nil-guard conditionals for each region (see 13-PATTERNS.md).
+
+ Step 6 — Update compound class strings in templates. For each file, replace all occurrences:
+ planning.templ: "ui-button ui-button-soft-neutral-md" → "ui-button ui-button-soft ui-button-neutral ui-button-md",
+ "ui-button ui-button-solid-default-md" → "ui-button ui-button-solid ui-button-default ui-button-md"
+ tasks.templ: "ui-button ui-button-soft-danger-md flex-shrink-0 text-xs" → "ui-button ui-button-soft ui-button-danger ui-button-md flex-shrink-0 text-xs",
+ "ui-button ui-button-soft-neutral-md w-full text-left text-sm mt-2" → "ui-button ui-button-soft ui-button-neutral ui-button-md w-full text-left text-sm mt-2"
+ events.templ: "ui-button ui-button-soft-neutral-md" → "ui-button ui-button-soft ui-button-neutral ui-button-md"
+ etapes.templ: "ui-button ui-button-soft-neutral-md px-2" → "ui-button ui-button-soft ui-button-neutral ui-button-md px-2",
+ "ui-button ui-button-soft-danger-md px-2" → "ui-button ui-button-soft ui-button-danger ui-button-md px-2",
+ "ui-button ui-button-soft-neutral-md flex-shrink-0" → "ui-button ui-button-soft ui-button-neutral ui-button-md flex-shrink-0"
+ Do NOT strip Tailwind utility classes that follow the button classes — keep them as-is.
+
+ Run just generate && go test ./... — all tests must pass GREEN.
+
+
+ cd backend && just generate && go test ./... -count=1
+
+
+ - go test ./... passes with no failures
+ - just generate produces no errors
+ - button.css contains ".ui-button-solid.ui-button-default {" (multi-class compound selector)
+ - button.css contains ".ui-button-ghost {" (new ghost variant)
+ - button.css does NOT contain ".auth-provider-button" (that is in auth.css)
+ - badge.css contains "border-radius: 999px" (pill shape from go-backend)
+ - badge.css contains ".ui-badge-primary {"
+ - card.css contains ".ui-card-header,"
+ - card.templ contains "type CardProps struct"
+ - card.templ contains "if props.Header != nil"
+ - planning.templ contains "ui-button ui-button-soft ui-button-neutral ui-button-md" and does NOT contain "ui-button-soft-neutral-md"
+ - tasks.templ does NOT contain "ui-button-soft-danger-md"
+ - TestCard_RendersTypedRegions passes (card renders typed regions correctly)
+ - TestBadge_PrimaryVariant passes (ui-badge-primary in output)
+
+ button.css/badge.css/card.css replaced; card.templ migrated to typed API; all template compound class strings updated; full test suite green
+
+
+
+
+
+## Trust Boundaries
+
+| Boundary | Description |
+|----------|-------------|
+| Template HTML → browser | templ auto-escapes string interpolations; CSS class changes are compile-time constants |
+
+## STRIDE Threat Register
+
+| Threat ID | Category | Component | Disposition | Mitigation Plan |
+|-----------|----------|-----------|-------------|-----------------|
+| T-13-02-01 | Tampering | card.templ typed API migration | accept | No user input flows through CardProps fields at runtime; templ auto-escapes all string interpolations |
+| T-13-02-02 | Information Disclosure | button.css ghost variant | accept | CSS is a public static asset; ghost variant is visual styling only, no sensitive data |
+
+
+
+After this plan completes:
+- cd backend && just generate — must succeed (no templ errors)
+- cd backend && go test ./... -count=1 — must be green across all packages
+- grep -r 'ui-button-soft-neutral-md\|ui-button-solid-default-md\|ui-button-soft-danger-md' backend/templates/ — must return nothing
+- grep 'ui-button-solid.ui-button-default' backend/internal/web/ui/button.css — must match
+- grep 'ui-badge-primary' backend/internal/web/ui/badge.css — must match
+- grep 'type CardProps struct' backend/internal/web/ui/card.templ — must match
+
+
+
+1. ButtonClass() emits "ui-button ui-button-solid ui-button-default ui-button-md" (multi-class)
+2. button.css uses multi-class compound selectors, includes ghost variant, excludes auth-provider CSS
+3. badge.css is pill-shape, includes primary variant
+4. card.templ uses typed Header/Body/Footer CardProps (no children passthrough)
+5. All 4 template files have multi-class button strings (no compound "ui-button-solid-default-md")
+6. Full test suite (go test ./...) is green
+
+
+
diff --git a/.planning/milestones/v3.0-phases/13-design-system-foundation/13-02-SUMMARY.md b/.planning/milestones/v3.0-phases/13-design-system-foundation/13-02-SUMMARY.md
new file mode 100644
index 0000000..1b216d6
--- /dev/null
+++ b/.planning/milestones/v3.0-phases/13-design-system-foundation/13-02-SUMMARY.md
@@ -0,0 +1,127 @@
+---
+phase: 13-design-system-foundation
+plan: "02"
+subsystem: backend/internal/web/ui
+tags: [css-migration, multi-class-pattern, component-api, design-system]
+dependency_graph:
+ requires: [13-01]
+ provides: [multi-class-button-css, typed-card-api, pill-badge-css, ghost-button-variant]
+ affects:
+ - backend/internal/web/ui/button.css
+ - backend/internal/web/ui/button.templ
+ - backend/internal/web/ui/badge.css
+ - backend/internal/web/ui/card.css
+ - backend/internal/web/ui/card.templ
+ - backend/internal/web/ui/variants.go
+ - backend/internal/web/ui/ui_test.go
+ - backend/templates/planning.templ
+ - backend/templates/tasks.templ
+ - backend/templates/events.templ
+ - backend/templates/etapes.templ
+ - backend/templates/tablos.templ
+ - backend/templates/auth_login.templ
+ - backend/templates/auth_signup.templ
+tech_stack:
+ added: []
+ patterns: [multi-class-css-compound-selectors, typed-templ-component-props, tdd-red-green]
+key_files:
+ created: []
+ modified:
+ - backend/internal/web/ui/variants.go
+ - backend/internal/web/ui/button.templ
+ - backend/internal/web/ui/button.css
+ - backend/internal/web/ui/badge.css
+ - backend/internal/web/ui/card.css
+ - backend/internal/web/ui/card.templ
+ - backend/internal/web/ui/ui_test.go
+ - backend/templates/planning.templ
+ - backend/templates/tasks.templ
+ - backend/templates/events.templ
+ - backend/templates/etapes.templ
+ - backend/templates/tablos.templ
+ - backend/templates/auth_login.templ
+ - backend/templates/auth_signup.templ
+decisions:
+ - "TabloCard wraps ui.Card in a
instead of passing id via Attrs — typed CardProps has no Attrs field; outer div preserves HTMX id-targeting without changing the delete/confirm zone selectors"
+ - "auth_login.templ and auth_signup.templ extract loginCardBody/signupCardBody as private templ components — cleaner than ComponentFunc inline; matches the typed Props pattern"
+ - "button.css adds .ui-button-soft.ui-button-neutral rule (not in go-backend) — original button.css had soft-neutral-md compound; must exist in new multi-class form to avoid visual regression"
+metrics:
+ duration: "~15 minutes"
+ completed_date: "2026-05-16"
+ tasks: 2
+ files: 14
+---
+
+# Phase 13 Plan 02: Component API Migration — Multi-Class Button, Typed Card, New Variants Summary
+
+Multi-class button CSS and ButtonClass() output migration (compound to multi-class), card.templ rewritten to typed Header/Body/Footer Props API, badge.css and card.css replaced with go-backend token-based versions, all 4 template files updated from compound to multi-class button class strings.
+
+## Tasks Completed
+
+| Task | Name | Commit | Key Files |
+|------|------|--------|-----------|
+| 1 | Update ButtonClass() multi-class + button.templ + ui_test.go button assertions | 66f23bb | variants.go, button.templ, ui_test.go |
+| 2 | Replace CSS files, migrate card.templ, update template hardcodes | a30a6f9 | button.css, badge.css, card.css, card.templ, ui_test.go, 6 template files |
+
+## What Was Built
+
+**Task 1 — ButtonClass() multi-class migration (TDD):**
+- Updated `ButtonClass()` in `variants.go` from compound format (`ui-button ui-button-solid-default-md`) to multi-class format (`ui-button ui-button-solid ui-button-default ui-button-md`).
+- Ghost variant special case: omits tone class, emits `ui-button ui-button-ghost ui-button-md`.
+- Updated `button.templ`: added `Icon string` field to `ButtonProps`, replaced inline `btnType` variable with `buttonType(props.Type)` helper from `helpers.go`.
+- Updated `ui_test.go`: `TestButton_DefaultSolidMD` now uses multi-class slice assertion pattern; `TestButtonClass_String` updated to new expected string; `TestButtonClass_GhostVariant` updated to assert standalone ghost format.
+- TDD RED gate: tests failed before variants.go change (3 failures). GREEN: all 18 tests pass after.
+
+**Task 2 — CSS replacement + card.templ migration + template hardcode updates (TDD):**
+- **button.css**: Replaced 180-line compound-class version with go-backend multi-class selector version (`.ui-button-solid.ui-button-default { ... }`). Added ghost variant rules (`.ui-button-ghost`). Added `.ui-button-soft.ui-button-neutral` (required for existing templates; not in go-backend). Auth-provider CSS correctly excluded (already in `auth.css` from Plan 01).
+- **badge.css**: Replaced with go-backend pill-shape version (`border-radius: 999px`). Added `.ui-badge-primary` variant with brand-purple surface.
+- **card.css**: Replaced with go-backend token-based version with `.ui-card-header`, `.ui-card-body`, `.ui-card-footer` padding rules.
+- **card.templ**: Migrated from `Card(attrs templ.Attributes)` with children to `Card(CardProps)` with typed `Header/Body/Footer templ.Component` fields. Nil-guard conditionals for each region.
+- **ui_test.go**: Removed `TestCard_RendersChildren` (old children API). Added `textComponent()` helper, `TestCard_RendersTypedRegions`, `TestBadge_PrimaryVariant`. Added `"io"` import.
+- **Template hardcodes**: Updated all compound button class strings across 6 template files.
+- **auth_login.templ / auth_signup.templ**: Migrated `@ui.Card(nil) { ... }` to `@ui.Card(ui.CardProps{Body: loginCardBody(...)})` with extracted private templ components.
+- **tablos.templ**: `TabloCard` migrated from `@ui.Card(templ.Attributes{id:...}) { ... }` to `
@ui.Card(ui.CardProps{Body: tabloCardBody(...)})` with extracted `tabloCardBody` component.
+
+## Verification Results
+
+- `just generate`: succeeds (no templ compile errors)
+- `go test ./... -count=1`: all packages pass (auth, db, files, jobs, web, web/ui, templates)
+- `grep ".ui-button-solid.ui-button-default" backend/internal/web/ui/button.css`: matches
+- `grep "ui-badge-primary" backend/internal/web/ui/badge.css`: matches
+- `grep "type CardProps struct" backend/internal/web/ui/card.templ`: matches
+- `grep -r "ui-button-soft-neutral-md|ui-button-solid-default-md|ui-button-soft-danger-md" backend/templates/`: no matches
+
+## Deviations from Plan
+
+### Auto-fixed Issues
+
+**1. [Rule 2 - Missing critical functionality] Added .ui-button-soft.ui-button-neutral to button.css**
+- **Found during:** Task 2, Step 2
+- **Issue:** The original button.css had a `.ui-button-soft-neutral-md` compound rule. After migrating to multi-class, `.ui-button-soft.ui-button-neutral` must exist in button.css for the neutral soft buttons in planning.templ, events.templ, and etapes.templ to render correctly. The go-backend button.css only has `.ui-button-solid.ui-button-neutral`, not `.ui-button-soft.ui-button-neutral`.
+- **Fix:** Added `.ui-button-soft.ui-button-neutral` with hover/active rules to button.css using the same token variable pattern as other soft variants.
+- **Files modified:** `backend/internal/web/ui/button.css`
+- **Commit:** a30a6f9
+
+**2. [Rule 3 - Blocking issue] Migrated auth_login.templ, auth_signup.templ, tablos.templ Card call sites**
+- **Found during:** Task 2, Step 5
+- **Issue:** The plan stated "No production templates call Card with children" but 3 templates used the old `Card(attrs)` API: auth_login.templ, auth_signup.templ, and tablos.templ. These were breaking build failures.
+- **Fix:** Extracted content into private templ components (`loginCardBody`, `signupCardBody`, `tabloCardBody`) and passed them as `CardProps.Body`. For TabloCard, the `id` attribute moved to a wrapping `
` since `CardProps` has no Attrs field. HTMX targeting via `tablo-delete-zone` class is unaffected.
+- **Files modified:** `backend/templates/auth_login.templ`, `backend/templates/auth_signup.templ`, `backend/templates/tablos.templ`
+- **Commit:** a30a6f9
+
+## Known Stubs
+
+None — all CSS changes are complete multi-class implementations. All template call sites fully migrated.
+
+## Threat Flags
+
+No new network endpoints, auth paths, file access patterns, or schema changes introduced.
+
+## TDD Gate Compliance
+
+- RED gate Task 1: 3 test failures (TestButton_DefaultSolidMD, TestButtonClass_String, TestButtonClass_GhostVariant) before variants.go update
+- GREEN gate Task 1: commit 66f23bb — all 18 tests pass
+- RED gate Task 2: compile failure (`undefined: CardProps`) + badge test fail before card.templ migration
+- GREEN gate Task 2: commit a30a6f9 — full test suite (all packages) green
+
+## Self-Check: PASSED
diff --git a/.planning/milestones/v3.0-phases/13-design-system-foundation/13-03-PLAN.md b/.planning/milestones/v3.0-phases/13-design-system-foundation/13-03-PLAN.md
new file mode 100644
index 0000000..54d71f7
--- /dev/null
+++ b/.planning/milestones/v3.0-phases/13-design-system-foundation/13-03-PLAN.md
@@ -0,0 +1,317 @@
+---
+phase: 13-design-system-foundation
+plan: 03
+type: execute
+wave: 3
+depends_on:
+ - 13-01
+ - 13-02
+files_modified:
+ - backend/internal/web/ui/input.css
+ - backend/internal/web/ui/input.templ
+ - backend/internal/web/ui/textarea.css
+ - backend/internal/web/ui/textarea.templ
+ - backend/internal/web/ui/select.css
+ - backend/internal/web/ui/select.templ
+ - backend/internal/web/ui/select_helpers.go
+ - backend/internal/web/ui/form-field.css
+ - backend/internal/web/ui/form_field.templ
+ - backend/internal/web/ui/ui_test.go
+autonomous: true
+requirements:
+ - DS-03
+
+must_haves:
+ truths:
+ - "Input component renders with class ui-input and respects ID/Name/Placeholder/Type props"
+ - "Textarea component renders with class ui-textarea and respects Rows default (4 when 0)"
+ - "Select component renders with class ui-select-control, inline JS for open/close, and HTMX re-init listener"
+ - "FormField wraps any component with label, hint, and error regions"
+ - "All four CSS files exist and contain their primary .ui-* selectors"
+ - "go test ./internal/web/ui/... passes for all new component tests"
+ - "just generate succeeds — all new .templ files compile"
+ artifacts:
+ - path: "backend/internal/web/ui/input.templ"
+ provides: "Input component with InputProps"
+ contains: "type InputProps struct"
+ - path: "backend/internal/web/ui/textarea.templ"
+ provides: "Textarea component with TextareaProps"
+ contains: "type TextareaProps struct"
+ - path: "backend/internal/web/ui/select.templ"
+ provides: "Select component with inline JS"
+ contains: "SelectProps"
+ - path: "backend/internal/web/ui/select_helpers.go"
+ provides: "Select helper functions"
+ contains: "selectPlaceholder"
+ - path: "backend/internal/web/ui/form_field.templ"
+ provides: "FormField wrapper component"
+ contains: "type FormFieldProps struct"
+ key_links:
+ - from: "backend/internal/web/ui/select.templ"
+ to: "select_helpers.go"
+ via: "selectPlaceholder(), selectOptionSelected() calls"
+ pattern: "selectPlaceholder"
+ - from: "backend/internal/web/ui/textarea.templ"
+ to: "helpers.go"
+ via: "textareaRows(), inputID() calls"
+ pattern: "textareaRows"
+---
+
+## Phase Goal
+
+**As a** developer, **I want to** have Input, Textarea, Select, and FormField components available as templ components with matching CSS, **so that** form-heavy views in Phases 14–17 can use these components instead of raw HTML form elements.
+
+
+Port all four form-input component types from go-backend to backend. Each component delivers
+a complete vertical slice: CSS file → templ Props struct → templ rendering → test coverage.
+
+Purpose: These are the form primitives that all subsequent phases need. Auth pages (Phase 14)
+need Input and FormField. Tablo create/edit dialogs (Phase 15-16) need all four.
+
+Output:
+- input.css + input.templ (update existing stub to match go-backend API)
+- textarea.css + textarea.templ (new files — port from go-backend)
+- select.css + select.templ + select_helpers.go (new files — complex, includes inline JS)
+- form-field.css + form_field.templ (new files — wrapper component)
+- ui_test.go extended with tests for all four component types
+
+
+
+@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.claude/get-shit-done/workflows/execute-plan.md
+@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/phases/13-design-system-foundation/13-CONTEXT.md
+@.planning/phases/13-design-system-foundation/13-RESEARCH.md
+@.planning/phases/13-design-system-foundation/13-PATTERNS.md
+@.planning/phases/13-design-system-foundation/13-01-SUMMARY.md
+@.planning/phases/13-design-system-foundation/13-02-SUMMARY.md
+
+
+
+From backend/internal/web/ui/helpers.go (after Plan 01):
+ func inputType(value string) string // returns "text" if empty
+ func inputID(id string, name string) string // returns id if non-empty, else name
+ func textareaRows(rows int) string // returns "4" if rows <= 0
+
+From go-backend/internal/web/ui/select_helpers.go (source to port):
+ func selectPlaceholder(props SelectProps) string
+ func selectNativeID(id, name string) string
+ func selectMenuID(id, name string) string
+ func selectBoolData(b bool) string
+ func selectOptionSelected(option SelectOption, values []string) bool
+ func selectSelectedLabels(props SelectProps) string
+ func selectSelectedLabel(props SelectProps) string
+ func selectMenuOptionClass(option SelectOption, values []string) string
+ func selectIsDisabled(props SelectProps, option SelectOption) bool
+
+
+
+
+
+ Task 1: Port input.templ, input.css, textarea.templ, textarea.css + tests
+
+ backend/internal/web/ui/input.css,
+ backend/internal/web/ui/input.templ,
+ backend/internal/web/ui/textarea.css,
+ backend/internal/web/ui/textarea.templ,
+ backend/internal/web/ui/ui_test.go
+
+
+ - backend/internal/web/ui/input.templ (existing stub — see current InputProps fields to understand what is already there)
+ - go-backend/internal/web/ui/input.templ (source Props struct and rendering pattern)
+ - go-backend/internal/web/ui/input.css (source CSS — port verbatim)
+ - go-backend/internal/web/ui/textarea.templ (source Props struct and rendering pattern)
+ - go-backend/internal/web/ui/textarea.css (source CSS — port verbatim)
+ - 13-PATTERNS.md (input.templ, textarea.templ, input.css, textarea.css sections)
+ - 13-RESEARCH.md (Props Struct Alignment Detail — Input and Textarea sections)
+ - 13-UI-SPEC.md (Input and Textarea component inventory sections)
+
+
+ - Test: Input(InputProps{Name: "email", Type: "email"}) HTML contains 'class="ui-input"' and 'type="email"'
+ - Test: Input(InputProps{Name: "x"}) HTML contains 'type="text"' (default via inputType helper)
+ - Test: Input(InputProps{ID: "my-id", Name: "x"}) HTML contains 'id="my-id"'
+ - Test: Input(InputProps{Name: "x"}) with no ID — HTML contains 'id="x"' (inputID fallback to name)
+ - Test: Textarea(TextareaProps{Name: "body"}) HTML contains 'class="ui-textarea"'
+ - Test: Textarea(TextareaProps{Name: "x", Rows: 0}) HTML contains 'rows="4"' (default via textareaRows)
+ - Test: Textarea(TextareaProps{Name: "x", Rows: 6}) HTML contains 'rows="6"'
+
+
+ Step 1 — Write failing tests in ui_test.go (RED):
+ Add TestInput_DefaultType, TestInput_EmailType, TestInput_IDFallback,
+ TestTextarea_DefaultRows, TestTextarea_ExplicitRows.
+ Run go test ./internal/web/ui/... — confirm failures.
+
+ Step 2 — Create/update input.css: port go-backend/internal/web/ui/input.css verbatim.
+ The file must contain .ui-input { } selector with: appearance: none, background, border,
+ border-radius: 0.75rem, color, font: inherit, line-height: 1.4, min-height: 44px,
+ padding: 0.75rem 0.95rem, width: 100%.
+ Strip any page-level selectors (body, :root).
+
+ Step 3 — Update input.templ to match go-backend's Props API:
+ InputProps must have: Name string, ID string, Type string, Placeholder string, Value string, Attrs templ.Attributes.
+ The templ function uses: inputID(props.ID, props.Name) for id, inputType(props.Type) for type.
+ Per D-CA03 (Claude's discretion on props alignment) and UI-SPEC explicit fields: also add
+ Disabled bool and Required bool with conditional attribute rendering:
+ if props.Disabled { disabled } and if props.Required { required } in the element.
+
+ Step 4 — Create textarea.css: port go-backend/internal/web/ui/textarea.css verbatim.
+ Must contain .ui-textarea { } with: same appearance/border/border-radius/color/font as input,
+ min-height: 7rem, resize: vertical, width: 100%.
+
+ Step 5 — Create textarea.templ: new file in backend/internal/web/ui/textarea.templ.
+ package ui declaration at top. TextareaProps must have: Name string, ID string,
+ Placeholder string, Value string, Rows int, Disabled bool, Required bool, Attrs templ.Attributes.
+ The templ function renders
+
+ cd backend && just generate && go test ./internal/web/ui/... -run "TestInput|TestTextarea" -v
+
+
+ - TestInput_DefaultType: HTML contains 'type="text"'
+ - TestInput_EmailType: HTML contains 'type="email"'
+ - TestInput_IDFallback: HTML contains 'id="email"' when no explicit ID but Name="email"
+ - TestTextarea_DefaultRows: HTML contains 'rows="4"'
+ - TestTextarea_ExplicitRows: HTML contains 'rows="6"'
+ - input.css contains ".ui-input {" and "min-height: 44px"
+ - textarea.css contains ".ui-textarea {" and "min-height: 7rem" and "resize: vertical"
+ - input.templ contains "type InputProps struct" with Name, ID, Type, Placeholder, Value, Disabled, Required, Attrs fields
+ - textarea.templ contains "type TextareaProps struct" with Name, ID, Placeholder, Value, Rows, Disabled, Required, Attrs fields
+ - just generate succeeds
+ - go test ./internal/web/ui/... is fully green
+
+ input.css/input.templ created; textarea.css/textarea.templ created; all input/textarea tests passing
+
+
+
+ Task 2: Port select.templ + select_helpers.go + select.css + form_field.templ + form-field.css + tests
+
+ backend/internal/web/ui/select.css,
+ backend/internal/web/ui/select.templ,
+ backend/internal/web/ui/select_helpers.go,
+ backend/internal/web/ui/form-field.css,
+ backend/internal/web/ui/form_field.templ,
+ backend/internal/web/ui/ui_test.go
+
+
+ - go-backend/internal/web/ui/select.templ (full file — port verbatim including inline script block)
+ - go-backend/internal/web/ui/select_helpers.go (full file — port verbatim)
+ - go-backend/internal/web/ui/select.css (full file — port verbatim)
+ - go-backend/internal/web/ui/form_field.templ (full file — port verbatim)
+ - go-backend/internal/web/ui/form-field.css (full file — port verbatim)
+ - 13-PATTERNS.md (select.templ section, select_helpers.go section, form_field.templ section)
+ - 13-RESEARCH.md (Select Component — Inline JavaScript section, Pitfall 6)
+ - 13-UI-SPEC.md (Select and Form Field component inventory sections)
+
+
+ - Test: Select(SelectProps{Name: "status", Options: []SelectOption{{Value: "a", Label: "Alpha"}}}) HTML contains "ui-select-control"
+ - Test: Select renders a
+