diff --git a/.planning/phases/20-tablo-detail-kanban-restyle/20-RESEARCH.md b/.planning/phases/20-tablo-detail-kanban-restyle/20-RESEARCH.md
new file mode 100644
index 0000000..af350fc
--- /dev/null
+++ b/.planning/phases/20-tablo-detail-kanban-restyle/20-RESEARCH.md
@@ -0,0 +1,616 @@
+# Phase 20: Tablo Detail & Kanban Restyle - Research
+
+**Researched:** 2026-05-18
+**Domain:** Go + HTMX + Tailwind + templ — new page creation + CSS restyle
+**Confidence:** HIGH
+
+---
+
+## Summary
+
+Phase 20 has two intertwined goals: (1) create a **tablo detail page** that does not yet exist in the codebase, and (2) deliver the Figma-matching visual restyle for that page's kanban board and task cards. The current Go backend has no `GET /tablos/{tabloID}` route — clicking a tablo in the list has no detail view. This phase must build the page from scratch, then style it to match the UI-SPEC.
+
+The existing kanban implementation lives exclusively inside the global `/tasks` page (`tasks.templ` → `TasksKanbanLayout`). That layout uses a CSS grid (`grid-cols-1 md:grid-cols-2 lg:grid-cols-4`) and the `TaskCard` component. The tablo detail kanban is a separate surface: a flex-row of fixed-width columns (18rem each, `overflow-x: auto`) scoped to one tablo, with Sortable.js drag-and-drop. The Phase 4 code (task creation, reorder handler, Sortable.js init) for the tablo-detail kanban currently exists only in `router_test.go` integration tests referencing `ListTasksByTablo`, implying it was coded in a previous milestone's tablo detail handler that was later replaced by `go-backend/`. All task mutation infrastructure (CreateTask, GetTaskByID, UpdateTask, SoftDeleteTask, ListTasksByTablo) exists in the repository layer and in the InMemory test repository — it just has no route-level handler wiring for the detail page.
+
+The primary recommendation: build `GET /tablos/{tabloID}` as a new handler + templ component, add `GET/POST /tablos/{tabloID}/tasks/...` for task CRU within the detail context, implement the tab-bar, and apply the full CSS restyle from `20-UI-SPEC.md` in `app.css`.
+
+**Primary recommendation:** Create the tablo detail page as a new handler + view layer; reuse existing `ListTasksByTablo` and task mutation repo methods; apply all UI-SPEC CSS changes to `app.css` and the new templ components.
+
+---
+
+## Architectural Responsibility Map
+
+| Capability | Primary Tier | Secondary Tier | Rationale |
+|------------|-------------|----------------|-----------|
+| Tablo detail route (`GET /tablos/{tabloID}`) | Go handler (server) | — | Fetches tablo record + tasks, builds view model, renders templ |
+| Tab bar (Overview/Tasks/Files/Discussion/Events) | templ template | — | Static HTML tabs, HTMX push-url for navigation |
+| Kanban board layout | templ template (`tablos_detail.templ`) | CSS (`app.css`) | Column+card HTML; flex layout and card styles in CSS |
+| Task CRUD within detail | Go handler (server) | — | CreateTask/UpdateTask/DeleteTask/ReorderTask scoped to tabloID |
+| Drag-and-drop reorder | Browser (Sortable.js) | Go handler (reorder endpoint) | JS-side drag; server persists new order via existing handler pattern |
+| Progress bar computation | Go handler (server) | — | doneTasks/totalTasks computed at render time from task list |
+| CSS restyle (cards, columns, header) | CSS (`app.css`) | — | Token-based rules; no new tokens needed |
+| Files tab (restyle only) | templ + CSS | — | Existing `ui.Table`; add border/radius wrapper and header style |
+
+---
+
+
+## Phase Requirements
+
+| ID | Description | Research Support |
+|----|-------------|------------------|
+| DETAIL-01 | Tablo detail page (header, tasks section, etapes section, files table) is restyled to match Figma | New `GET /tablos/{tabloID}` handler + `TabloDetailPage` templ component + CSS in `app.css` matching UI-SPEC tokens |
+| TASK-01 | Kanban board columns, task cards, and drag-and-drop are restyled to match Figma | New tablo-scoped kanban templ + `.task-card` CSS block in `app.css`; Sortable.js init preserved verbatim |
+
+
+---
+
+## Standard Stack
+
+### Core (existing — no new packages)
+
+| Library | Version | Purpose | Why Standard |
+|---------|---------|---------|--------------|
+| `github.com/a-h/templ` | existing | HTML components | Entire view layer uses templ |
+| Sortable.js | CDN (existing) | Drag-and-drop | Already wired in router_test; Phase 4 integration |
+| HTMX | CDN v4.0.0-beta2 (existing) | Partial page swaps | Entire app uses HTMX for navigation |
+| Tailwind CSS | existing build | Utility classes | Used alongside custom CSS classes |
+
+### No new packages needed
+
+The UI-SPEC explicitly confirms: "No external registries. All components are templ + custom CSS." All needed UI primitives (`ui.Button`, `ui.IconButton`, `ui.Badge`, `ui.Table`) exist in `backend/internal/web/ui/`.
+
+**Installation:** none required.
+
+---
+
+## Package Legitimacy Audit
+
+> No new packages to install in this phase. The phase is purely a restyle + new page creation using existing infrastructure.
+
+**Packages removed due to slopcheck:** none
+**Packages flagged as suspicious:** none
+
+---
+
+## Architecture Patterns
+
+### System Architecture Diagram
+
+```
+Browser
+ │ GET /tablos/{tabloID}
+ ▼
+Go handler: GetTabloDetailPage
+ │ repo.ListTablos (find single tablo by ID + owner check)
+ │ repo.ListTasksByTablo (fetch tasks for this tablo)
+ │ compute progress: doneTasks / totalTasks * 100
+ ▼
+TabloDetailPageViewModel {tablo, tasks by status column, progress}
+ │
+ ▼
+templ: TabloDetailPage
+ ├── TabloDetailHeader (name h1, status badge, progress bar, owner row)
+ ├── TabloTabBar (Overview | Tasks | Files | Discussion | Events)
+ └── tab: Tasks → KanbanBoardSection
+ ├── KanbanColumn × 4 (Todo / In Progress / In Review / Done)
+ │ ├── ColumnHeader (title, count badge, "+ Add" link)
+ │ └── TaskCardList (sortable-column)
+ │ └── TaskCard × N (drag-handle, title, delete icon)
+ └── TaskCreateInlineForm (htmx fragment, within column)
+
+Browser
+ Sortable.js init (DOMContentLoaded + htmx:afterSettle)
+ handles: .sortable-column containers
+ POST /tablos/{tabloID}/tasks/reorder (existing reorder pattern)
+```
+
+### Recommended Project Structure
+
+New files to create:
+
+```
+go-backend/internal/web/
+├── views/
+│ ├── tablo_detail.templ # TabloDetailPage + all sub-components
+│ └── tablo_detail_view.go # TabloDetailViewModel + helpers
+├── handlers/
+│ └── tablo_detail.go # GetTabloDetailPage handler
+```
+
+Modified files:
+
+```
+go-backend/
+├── router.go # Add GET /tablos/{tabloID}
+├── internal/web/ui/app.css # All CSS changes from UI-SPEC delta table
+```
+
+### Pattern 1: Tablo Detail Handler (new page + HTMX swap)
+
+```go
+// Source: existing handler pattern from handlers/auth.go + handlers/tablos.go
+func (h *AuthHandler) GetTabloDetailPage() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ user, ok := h.authenticatedUser(r.Context(), r)
+ if !ok {
+ http.Redirect(w, r, "/login", http.StatusSeeOther)
+ return
+ }
+
+ tabloID, err := uuid.Parse(r.PathValue("tabloID"))
+ if err != nil {
+ http.Error(w, "invalid tablo id", http.StatusBadRequest)
+ return
+ }
+
+ // Reuse ListTablos to find the owned tablo (no GetTabloByID sqlc query exists)
+ tablos, err := h.repo.ListTablos(r.Context(), ListTablosInput{OwnerID: user.ID})
+ if err != nil {
+ http.Error(w, "failed to load tablos", http.StatusInternalServerError)
+ return
+ }
+ tablo, ok := findTabloByID(tablos, tabloID)
+ if !ok {
+ http.Error(w, "tablo not found", http.StatusNotFound)
+ return
+ }
+
+ // ListTasksByTablo is defined in the repo and InMemory repo
+ taskRepo := h.repo.(interface {
+ ListTasksByTablo(context.Context, ListTasksByTabloInput) ([]TaskRecord, error)
+ })
+ tasks, err := taskRepo.ListTasksByTablo(r.Context(), ListTasksByTabloInput{
+ OwnerID: user.ID,
+ TabloID: tabloID,
+ })
+ if err != nil {
+ http.Error(w, "failed to load tasks", http.StatusInternalServerError)
+ return
+ }
+
+ vm := views.NewTabloDetailViewModel(tablo, tasks)
+ content := views.TabloDetailPage(vm)
+
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ var renderErr error
+ if isHXRequest(r) {
+ renderErr = views.DashboardContentSwap("/tablos/"+tabloID.String(), tablos, content).Render(r.Context(), w)
+ } else {
+ renderErr = views.DashboardPage("/tablos/"+tabloID.String(), tablos, content).Render(r.Context(), w)
+ }
+ if renderErr != nil {
+ http.Error(w, "failed to render tablo detail", http.StatusInternalServerError)
+ }
+ }
+}
+```
+
+**[ASSUMED]** — GetTabloByID does not exist in sqlc queries; using ListTablos + findTabloByID is the current pattern from GetEditTabloModal. For a large tablo list this is an O(n) scan. At v1 scale this is acceptable.
+
+### Pattern 2: Progress Computation
+
+```go
+// Source: [ASSUMED] — derived from existing tabloStatusPresentation pattern
+func computeTabloProgress(tasks []TaskRecord) int {
+ total := len(tasks)
+ if total == 0 {
+ return 0
+ }
+ var done int
+ for _, t := range tasks {
+ if t.Status == TaskStatusDone {
+ done++
+ }
+ }
+ return (done * 100) / total
+}
+```
+
+### Pattern 3: Kanban Column templ (new, scoped to tablo detail)
+
+```templ
+// Source: UI-SPEC + existing TasksKanbanLayout pattern
+templ TabloDetailKanbanColumn(col TabloDetailColumnView) {
+
+
+
+ for _, task := range col.Tasks {
+ @TabloDetailTaskCard(task)
+ }
+
+
+
+}
+```
+
+### Pattern 4: Drag Handle (opacity-based hover)
+
+```css
+/* Source: UI-SPEC Component Delta section */
+.task-drag-handle {
+ color: var(--color-text-faint);
+ cursor: grab;
+ font-size: 1rem;
+ opacity: 0;
+ transition: opacity 0.12s ease;
+}
+
+.task-card:hover .task-drag-handle {
+ opacity: 1;
+}
+```
+
+### Pattern 5: Sortable.js init (preserved from Phase 4)
+
+```javascript
+// Source: [ASSUMED] — Sortable.js init pattern from STATE.md Phase 4 decisions
+// Must fire on DOMContentLoaded AND htmx:afterSettle
+function initSortable() {
+ document.querySelectorAll('.sortable-column').forEach(function(el) {
+ if (el._sortable) return; // idempotent
+ el._sortable = Sortable.create(el, {
+ group: 'tablo-tasks',
+ animation: 150,
+ handle: '.task-drag-handle',
+ draggable: '.task-card',
+ onEnd: function(evt) {
+ // Update hidden reorder form + submit
+ document.querySelector('#reorder-form').requestSubmit();
+ }
+ });
+ });
+}
+document.addEventListener('DOMContentLoaded', initSortable);
+document.addEventListener('htmx:afterSettle', initSortable);
+```
+
+The UI-SPEC states: "Sortable.js initialization on `DOMContentLoaded` and `htmx:afterSettle` is PRESERVED verbatim."
+
+### Anti-Patterns to Avoid
+
+- **Using `display: none` for drag handle / delete icon:** UI-SPEC requires `opacity: 0 → 1` (not display-toggling) to preserve layout during hover.
+- **Mixing global tasks kanban with tablo-detail kanban:** The `TasksKanbanLayout` in `tasks.templ` is for the global `/tasks` page and must remain unchanged. The tablo detail gets a new separate `TabloDetailKanbanLayout`.
+- **Adding a task-view switcher (Board/List/Gantt):** UI-SPEC explicitly defers the inner task-view switcher to Phase 21. Do not add it.
+- **Using inline Tailwind for the card restyle:** The UI-SPEC defines `.task-card` as a named CSS class in `app.css`. Inline Tailwind classes on these components would conflict with the CSS cascade.
+- **Routing task create/delete to global `/tasks` endpoints:** Task mutations within the detail page must include `tabloID` context so the response re-renders the detail page, not the global tasks page.
+
+---
+
+## Don't Hand-Roll
+
+| Problem | Don't Build | Use Instead | Why |
+|---------|-------------|-------------|-----|
+| Drag-and-drop column reorder | Custom JS drag | Sortable.js (already included) | Phase 4 already wired; re-implementing is regression risk |
+| Status badge pill | Custom badge HTML | `ui.Badge` with `badgeVariantForTone` | Already has all required tone variants in `badge.css` |
+| Modal dialog | Raw HTML `