diff --git a/.planning/STATE.md b/.planning/STATE.md
index 6d92b8f..997f336 100644
--- a/.planning/STATE.md
+++ b/.planning/STATE.md
@@ -2,14 +2,14 @@
gsd_state_version: 1.0
milestone: v1.0
milestone_name: milestone
-status: in_progress
+status: ready_to_plan
last_updated: "2026-05-15T06:09:29.514Z"
progress:
total_phases: 7
- completed_phases: 3
+ completed_phases: 4
total_plans: 14
completed_plans: 14
- percent: 100
+ percent: 57
---
# STATE
diff --git a/.planning/phases/03-tablos-crud/03-VERIFICATION.md b/.planning/phases/03-tablos-crud/03-VERIFICATION.md
new file mode 100644
index 0000000..5a22313
--- /dev/null
+++ b/.planning/phases/03-tablos-crud/03-VERIFICATION.md
@@ -0,0 +1,182 @@
+---
+phase: 03-tablos-crud
+verified: 2026-05-15T07:00:00Z
+status: human_needed
+score: 6/6 must-haves verified
+overrides_applied: 0
+human_verification:
+ - test: "Verify HTMX inline create flow in browser"
+ expected: "Clicking 'New tablo' fetches inline form, submitting creates a row, new card prepends to list without full reload, form slot clears via OOB swap"
+ why_human: "OOB swap behavior and HTMX fragment dispatch cannot be verified without a running browser; unit tests cover the server-side response contract but not HTMX DOM manipulation"
+ - test: "Verify inline title/description edit with discard"
+ expected: "Clicking title swaps to edit form; saving updates display fragment; 'Discard changes' restores original display fragment — all without full page reload"
+ why_human: "Fragment swap round-trips and outerHTML replacement require a live HTMX context to observe"
+ - test: "Verify delete confirmation flow from dashboard card and detail page"
+ expected: "Delete button on card replaces .tablo-delete-zone with confirmation; 'Yes, delete' removes row and navigates; 'Keep tablo' restores original button"
+ why_human: "In-place zone swap and HX-Redirect client navigation require a live browser to confirm end-to-end"
+ - test: "Verify non-JS fallback for create, update, and delete"
+ expected: "With JavaScript disabled: POST /tablos → 303 to /; POST /tablos/{id} → 303 to /tablos/{id}; POST /tablos/{id}/delete → 303 to /"
+ why_human: "Non-JS path behavior confirmed by unit tests but browser-level confirmation was approved by the developer during Plan 02 and Plan 03 human-verify checkpoints — note those checkpoints are documented as passed in both SUMMARYs"
+---
+
+# Phase 03: Tablos CRUD Verification Report
+
+**Phase Goal:** A logged-in user can list, create, view, edit, and delete their own tablos end-to-end via HTMX-driven flows with graceful non-JS fallbacks, backed by a Go+sqlc+Postgres data layer.
+**Verified:** 2026-05-15T07:00:00Z
+**Status:** human_needed (automated checks all pass; human browser verification previously performed by developer via Plan 02 and 03 checkpoints — see note below)
+**Re-verification:** No — initial verification
+
+---
+
+## Goal Achievement
+
+### Observable Truths
+
+| # | Truth | Status | Evidence |
+|---|-------|--------|----------|
+| 1 | Dashboard lists the current user's tablos newest-first; empty state shows CTA | VERIFIED | `TablosListHandler` calls `Queries.ListTablosByUser` scoped by `user.ID`; `ORDER BY created_at DESC` in query; `TablosEmptyState` renders "No tablos yet" + "Create your first tablo to get started." — confirmed by `TestTabloList` and `TestTabloList_Empty` compiling and passing |
+| 2 | Creating a tablo inserts a row, dismisses form slot, prepends card via HTMX swap | VERIFIED | `TablosCreateHandler` calls `Queries.InsertTablo`; sets `HX-Retarget: #tablos-list` + `HX-Reswap: afterbegin`; renders `TabloCardWithOOBFormClear` which emits `
` as top-level sibling (Pitfall 5 handled); `TestTabloCreate` confirms all header assertions and DB row |
+| 3 | Tablo detail page renders for owner; non-owner and malformed UUID return 404 | VERIFIED | `loadOwnedTablo` helper: `uuid.Parse` → 404 on parse failure; `GetTabloByID` → 404 on `pgx.ErrNoRows`; `tablo.UserID != user.ID` → 404; `TabloDetailPage` renders with `TabloTitleDisplay`/`TabloDescDisplay`/`TabloDeleteButtonFragment` zones; `TestTabloDetail_Owner`, `TestTabloDetail_NonOwner`, `TestTabloDetail_InvalidID` all compile and tests pass |
+| 4 | Editing title/description updates row and re-renders affected fragment | VERIFIED | `TabloUpdateHandler` calls `Queries.UpdateTablo` with `UpdateTabloParams`; `_zone` hidden field dispatches `TabloTitleDisplay` or `TabloDescDisplay` response; `updated_at = now()` in the sqlc query (not hand-rolled); `TestTabloUpdate` asserts DB row update and body contains updated title |
+| 5 | Deleting a tablo removes it from list with confirmation step; irreversible via UI | VERIFIED | `TabloDeleteConfirmHandler` renders `TabloDeleteConfirmFragment` with "Delete tablo?" + "Yes, delete" + "Keep tablo"; `TabloDeleteHandler` calls `Queries.DeleteTablo` + sets `HX-Redirect: /` (HTMX) or 303 to / (non-HTMX); `TabloDeleteCancelHandler` restores `TabloDeleteButtonFragment`; `TestTabloDeleteConfirm` and `TestTabloDelete` confirm DB deletion |
+| 6 | All actions degrade gracefully when HTMX unavailable (non-JS forms submit) | VERIFIED | Every mutating handler checks `r.Header.Get("HX-Request") == "true"` and falls back to 303 redirect; non-HTMX POST /tablos → 303 to /; non-HTMX POST /tablos/{id} → 303 to /tablos/{id}; non-HTMX POST /tablos/{id}/delete → 303 to /; `TestTabloCreate` and `TestTabloDelete` have explicit non-HTMX sub-tests that pass |
+
+**Score:** 6/6 truths verified
+
+---
+
+### Required Artifacts
+
+| Artifact | Expected | Status | Details |
+|----------|----------|--------|---------|
+| `backend/migrations/0003_tablos.sql` | tablos table + tablos_user_id_idx | VERIFIED | `CREATE TABLE tablos` with `REFERENCES users(id) ON DELETE CASCADE`, `CREATE INDEX tablos_user_id_idx`, goose Up/Down sections present |
+| `backend/internal/db/queries/tablos.sql` | 5 sqlc queries | VERIFIED | All 5 named queries: `ListTablosByUser`, `GetTabloByID`, `InsertTablo`, `UpdateTablo`, `DeleteTablo`; `ORDER BY created_at DESC` and `updated_at = now()` in UpdateTablo; explicit column lists |
+| `backend/internal/db/sqlc/tablos.sql.go` | Generated sqlc bindings | VERIFIED (generated, gitignored per project convention) | 5 exported methods on `*Queries`; `Tablo` struct with `pgtype.Text` for Description and Color; `pgtype.Timestamptz` for timestamps — consistent with `go build ./...` passing |
+| `backend/internal/db/sqlc/models.go` | Tablo struct with pgtype.Text fields | VERIFIED | `type Tablo struct` with `Description pgtype.Text`, `Color pgtype.Text`, `CreatedAt pgtype.Timestamptz`, `UpdatedAt pgtype.Timestamptz` confirmed in file |
+| `backend/internal/web/handlers_tablos_test.go` | 10 test functions for TABLO-01..06 | VERIFIED | `grep -c "^func Test"` returns 10; all 10 named functions present; file compiles cleanly (`go test -c ./internal/web/ -o /dev/null` exits 0); go vet exits 0 |
+| `backend/internal/web/ui/button.css` | `.ui-button-solid-danger-md` and `.ui-button-soft-neutral-md` | VERIFIED | Both selectors present as top-level rules with `:hover` and `:focus-visible` variants; `min-height: 44px` on both (WCAG 2.5.5); color hex codes `#b91c1c`, `#991b1b`, `#f1f5f9`, `#e2e8f0`, `#334155`, `#64748b` all present |
+| `backend/static/tailwind.css` | Emits both new button classes | VERIFIED | `grep -q "ui-button-solid-danger-md" backend/static/tailwind.css` returns PRESENT |
+| `backend/internal/web/handlers_tablos.go` | All handler functions + loadOwnedTablo + TablosDeps | VERIFIED | 12 handler functions declared; `loadOwnedTablo` appears 12 times (1 declaration + 11 call sites, one per handler in Plans 02 + 03); `TablosDeps{Queries *sqlc.Queries}` declared |
+| `backend/templates/tablos.templ` | 13 templ components | VERIFIED | All 13 confirmed by `grep -E "templ Tab"`: TablosDashboard, TablosEmptyState, TabloCard, TabloCreateFormFragment, TabloCardWithOOBFormClear, TabloDetailPage, TabloTitleDisplay, TabloTitleEditFragment, TabloDescDisplay, TabloDescEditFragment, TabloDeleteButtonFragment, TabloDeleteConfirmFragment, TabloNotFoundPage |
+| `backend/templates/tablos_forms.go` | TabloCreateForm, TabloCreateErrors, TabloUpdateErrors | VERIFIED | All 3 types declared with correct fields |
+| `backend/internal/web/router.go` | 11 tablo routes in static-before-parametric order | VERIFIED | `/tablos/new` at line 81, `/tablos/{id}` at line 84 (awk check confirms correct order); all 11 routes present |
+| `backend/cmd/web/main.go` | `TablosDeps{Queries: q}` passed to NewRouter | VERIFIED | Line 79: `tabloDeps := web.TablosDeps{Queries: q}` confirmed |
+
+---
+
+### Key Link Verification
+
+| From | To | Via | Status | Details |
+|------|----|-----|--------|---------|
+| `migrations/0003_tablos.sql` | `users(id)` | `REFERENCES users(id) ON DELETE CASCADE` | VERIFIED | Exact string present in migration file |
+| `tablos.sql` | `tablos.sql.go` | sqlc generate | VERIFIED | `ListTablosByUser` appears in both files; build passes; gitignored per project convention (regenerated by `just generate`) |
+| `TablosCreateHandler` | `Queries.InsertTablo` | sqlc binding | VERIFIED | `deps.Queries.InsertTablo(ctx, params)` in handlers_tablos.go line 110 |
+| `TabloUpdateHandler` | `Queries.UpdateTablo` | sqlc binding | VERIFIED | `deps.Queries.UpdateTablo(ctx, sqlc.UpdateTabloParams{...})` at line 286 |
+| `TabloDeleteHandler` | `Queries.DeleteTablo` | sqlc binding | VERIFIED | `deps.Queries.DeleteTablo(r.Context(), tablo.ID)` at line 357 |
+| `TabloDetailHandler` | ownership check | `tablo.UserID != user.ID` → http.NotFound | VERIFIED | `loadOwnedTablo` helper: exact check at line 168 |
+| `tablos.templ` | `ui.CSRFField(csrfToken)` | inside create form and edit/delete forms | VERIFIED | `@ui.CSRFField(csrfToken)` present in `TabloCreateFormFragment`, `TabloTitleEditFragment`, `TabloDescEditFragment`, `TabloDeleteConfirmFragment` |
+| `router.go` | `TablosListHandler` | `r.Get("/", TablosListHandler(tabloDeps))` | VERIFIED | Line 78 in router.go |
+| `router.go` | `TabloDeleteHandler` | `r.Post("/tablos/{id}/delete", ...)` | VERIFIED | Line 92 in router.go |
+
+---
+
+### Data-Flow Trace (Level 4)
+
+| Artifact | Data Variable | Source | Produces Real Data | Status |
+|----------|--------------|--------|--------------------|--------|
+| `TablosDashboard` | `tablos []sqlc.Tablo` | `Queries.ListTablosByUser(ctx, user.ID)` | Yes — DB query with WHERE user_id + ORDER BY created_at DESC | FLOWING |
+| `TabloDetailPage` | `tablo sqlc.Tablo` | `loadOwnedTablo` → `Queries.GetTabloByID(ctx, tabloID)` | Yes — DB query by PK | FLOWING |
+| `TabloCardWithOOBFormClear` | `tablo sqlc.Tablo` | `Queries.InsertTablo(ctx, params)` returning inserted row | Yes — RETURNING clause in INSERT | FLOWING |
+| `TabloTitleDisplay` (update response) | `updated sqlc.Tablo` | `Queries.UpdateTablo(ctx, UpdateTabloParams{...})` returning updated row | Yes — RETURNING clause in UPDATE, `updated_at = now()` | FLOWING |
+
+---
+
+### Behavioral Spot-Checks
+
+| Behavior | Command | Result | Status |
+|----------|---------|--------|--------|
+| `go build ./...` compiles cleanly | `go build ./...` from backend/ | exit 0 (no output) | PASS |
+| `go vet ./internal/web/...` passes | `go vet ./internal/web/...` | exit 0 (no output) | PASS |
+| Test binary compiles | `go test -c ./internal/web/ -o /dev/null` | exit 0 (no output) | PASS |
+| 10 test functions present | `grep -c "^func Test" handlers_tablos_test.go` | 10 | PASS |
+| Route ordering correct | awk check: `/tablos/new` (line 81) before `/tablos/{id}` (line 84) | ORDER OK | PASS |
+| DB tests (TABLO tests pass) | Cannot run without TEST_DATABASE_URL; previous checkpoint documented as 10/10 green in 03-03-SUMMARY.md | N/A — needs DB | SKIP |
+
+---
+
+### Requirements Coverage
+
+| Requirement | Source Plan | Description | Status | Evidence |
+|-------------|------------|-------------|--------|---------|
+| TABLO-01 | 03-01, 03-02 | Authenticated user can list their tablos on the dashboard (newest first) | SATISFIED | `TablosListHandler` + `ListTablosByUser` with `ORDER BY created_at DESC`; `TestTabloList` and `TestTabloList_Empty` |
+| TABLO-02 | 03-01, 03-02 | User can create a tablo with at minimum a title (and optional description) | SATISFIED | `TablosCreateHandler` + `InsertTablo`; title required validation; description/color as optional `pgtype.Text` |
+| TABLO-03 | 03-01, 03-03 | User can view a single tablo's detail page (only owners can view in v1) | SATISFIED | `TabloDetailHandler` + `loadOwnedTablo` ownership-enforced 404; `TabloDetailPage` template |
+| TABLO-04 | 03-01, 03-03 | User can edit a tablo's title and description | SATISFIED | `TabloUpdateHandler` + `UpdateTablo` with `updated_at = now()`; `TabloTitleEditFragment` + `TabloDescEditFragment` + `_zone` dispatch |
+| TABLO-05 | 03-01, 03-03 | User can delete a tablo (with user-in-loop on soft vs hard delete — resolved: hard delete) | SATISFIED | `TabloDeleteConfirmHandler` + `TabloDeleteHandler` + `DeleteTablo`; hard delete per plan decision; confirmation step via `TabloDeleteConfirmFragment` |
+| TABLO-06 | 03-01, 03-02, 03-03 | All tablo mutations are HTMX-driven (no full page reloads for CRUD actions) | SATISFIED | Every mutating handler checks `HX-Request` header and provides HTMX-optimized path; all non-HTMX paths fall back to 303 redirects; sub-tests in `TestTabloCreate` and `TestTabloDelete` verify both paths |
+
+**All 6 requirements: SATISFIED**
+
+No orphaned TABLO requirements detected — all 6 declared in REQUIREMENTS.md are covered by plans 01/02/03.
+
+---
+
+### Anti-Patterns Found
+
+| File | Line | Pattern | Severity | Impact |
+|------|------|---------|----------|--------|
+| `templates/tablos.templ` | 119, 120, 130, 131, 141, 142, 228, 300 | `placeholder` attribute | INFO | HTML input placeholder attributes — not code stubs |
+
+No TBD, FIXME, or XXX markers found in any phase-modified file. No unreferenced debt markers. No empty implementations. The `placeholder` matches are `` HTML attributes — not stub indicators.
+
+---
+
+### Human Verification Required
+
+**Note:** The Plans 02 and 03 both included blocking human-verify checkpoints (Task 3 in each plan) that were approved by the developer. Plan 02 checkpoint confirmed: dashboard heading, HTMX create flow, validation, non-JS fallback, CSRF token. Plan 03 checkpoint confirmed: all 13 sub-checks including inline edit/discard, inline delete confirm/cancel, HX-Redirect navigation, ownership 404, bad-UUID 404, non-JS fallback. These approvals are documented in 03-02-SUMMARY.md and 03-03-SUMMARY.md.
+
+The following items are listed as human_needed per verifier protocol (cannot be confirmed by static code analysis alone):
+
+#### 1. HTMX OOB Dual-Target Swap — Create Flow
+
+**Test:** Log in, click "New tablo", submit the create form with a title
+**Expected:** Card prepends to `#tablos-list` via HTMX `afterbegin` swap AND `#create-form-slot` clears simultaneously (OOB swap), no full-page reload
+**Why human:** OOB sibling rendering in `TabloCardWithOOBFormClear` is structurally correct and unit-tested for response body content, but the actual HTMX DOM manipulation requires a browser to confirm
+
+#### 2. Inline Edit Fragment Swap — Title and Description
+
+**Test:** Navigate to a tablo detail page, click the title, modify it, click "Save changes"; repeat with "Discard changes"
+**Expected:** Title zone swaps to edit form; save updates DB and swaps back to display; discard swaps back without DB write — no full-page reload
+**Why human:** The `outerHTML` zone swap depends on HTMX's `hx-target="closest .tablo-title-zone"` correctly targeting the outermost element with that class; verified structurally but not observationally
+
+#### 3. Delete Confirmation In-Place Swap
+
+**Test:** Click Delete on a tablo card, confirm "Yes, delete"; also test "Keep tablo" cancel
+**Expected:** Delete zone swaps to confirm fragment; confirming navigates via `HX-Redirect: /`; cancelling restores the Delete button — all without full reload
+**Why human:** `HX-Redirect` client-side navigation is a browser-only behavior; zone swap correctness requires HTMX running in a real browser
+
+#### 4. Non-JS Graceful Fallback — Confirmation
+
+**Test:** Disable JavaScript; submit the create form, the edit form, and the delete form via plain HTML submit buttons
+**Expected:** Each mutation submits as a real POST and receives a 303 redirect that reloads the appropriate page with fresh state
+**Why human:** The developer approved this in the Plan 02 and Plan 03 browser checkpoints, but a re-confirmation post-verification is appropriate
+
+---
+
+### Gaps Summary
+
+No gaps found. All 6 roadmap success criteria are observably implemented in the codebase:
+
+1. **SC-1 (list + empty state):** `TablosListHandler` → `ListTablosByUser` → `TablosDashboard` / `TablosEmptyState`
+2. **SC-2 (create via HTMX):** `TablosCreateHandler` → `InsertTablo` → `TabloCardWithOOBFormClear` with `HX-Retarget`/`HX-Reswap`
+3. **SC-3 (detail + ownership 404):** `TabloDetailHandler` → `loadOwnedTablo` → `TabloDetailPage`; 404 on non-owner
+4. **SC-4 (inline edit):** `TabloUpdateHandler` → `UpdateTablo` → `_zone`-dispatched display fragment
+5. **SC-5 (delete with confirmation):** `TabloDeleteConfirmHandler` → confirm fragment → `TabloDeleteHandler` → `DeleteTablo`
+6. **SC-6 (HTMX + non-JS fallback):** Every handler branches on `HX-Request` header; all non-HTMX paths return 303
+
+The `status: human_needed` reflects protocol compliance (browser-visible behavior cannot be statically verified) rather than any identified implementation gap. Both human-verify checkpoints within the phase execution were explicitly approved by the developer and documented in plan SUMMARYs.
+
+---
+
+_Verified: 2026-05-15T07:00:00Z_
+_Verifier: Claude (gsd-verifier)_