# Phase 16: Tablo Detail - Research

**Researched:** 2026-05-16
**Domain:** Go templ templates, CSS design system, HTMX fragment wiring
**Confidence:** HIGH

## Summary

Phase 16 is a pure visual restyling phase — no new routes, no new data models, no handler
signature changes. The work touches four templ files and one CSS file:
`backend/templates/tablos.templ`, `backend/templates/tasks.templ`,
`backend/templates/etapes.templ`, `backend/templates/files.templ`, and
`backend/internal/web/ui/app.css`.

All design system components (`@ui.Badge`, `@ui.Button`, `@ui.IconButton`, `@ui.Table`,
`@ui.EmptyState`) are already present in `backend/internal/web/ui/`. The CSS classes to be
ported (`.tasks-section`, `.tasks-section-header`, `.task-row`, `.task-check`, `.task-body`,
`.task-meta`, `.tasks-add-button`, `.task-list`, `.project-progress-track`,
`.project-progress-bar`) are verbatim in `go-backend/internal/web/ui/app.css` and need to be
appended to `backend/internal/web/ui/app.css`. New classes needed for this phase
(`.kanban-column`, `.etape-group`, `.etape-group-header`, `.etape-group-color-dot`,
`.etape-group-label`, `.tab-nav`, `.tab-nav-item`, `.tablo-metadata-row`,
`.tablo-metadata-date`, `.task-list-empty`) are fully specified in the UI-SPEC.

The single structural Go-level change is etape grouping inside `KanbanColumn`: a
`groupTasksByEtape` helper (parallel to the existing `groupTasksByStatus`) produces per-etape
task slices for server-side rendering inside each column, eliminating the need for
`EtapeStrip` in the tasks tab.

**Primary recommendation:** Implement in four incremental waves: (1) CSS additions to
`app.css`, (2) header + metadata + tab nav + overview tab in `tablos.templ`, (3) kanban
restyling + etape grouping in `tasks.templ` and `etapes.templ` (EtapeStrip removal), (4)
files section restyling in `files.templ`. Each wave can be verified independently with the
existing test suite.

---

<user_constraints>
## User Constraints (from CONTEXT.md)

### Locked Decisions

**Header (D-H01 through D-H06)**
- Header uses `.project-card-top` layout (already in `app.css`): tablo color circle avatar
  (left) + inline-editable title zone + action controls (right)
- Action controls: Discussion link button, Invite button, Delete icon button — all restyled
  with `ui.Button` / `ui.IconButton`; hardcoded `#804EEC` replaced with design tokens
- Tablo color avatar: `.project-avatar` pattern with inline `background-color` style;
  falls back to `var(--color-project-fallback)` when empty
- Metadata row (Created date, Status pill, Progress bar): kept, restyled with design tokens;
  status pill uses `@ui.Badge(...)`
- Description zone moves from persistent header into the Overview tab content
- Tab nav bar: replace hardcoded color classes with `.tab-nav` / `.tab-nav-item.is-active`
  design token CSS; preserve all 5 existing tabs and their HTMX behavior

**Kanban (D-K01 through D-K04)**
- Horizontal 3-column layout stays; Sortable.js wiring unchanged
- Column headers use `.tasks-section-header` CSS: status label + task count badge + Add task
  button on right
- Task cards use row style matching `.tasks-section` layout: checkbox + title + meta
- `KanbanBoard`, `KanbanColumn`, `TaskCard` restyled in-place; no endpoint/handler changes

**Etapes (D-E01 through D-E05)**
- `EtapeStrip` removed from the tasks tab UI
- Tasks grouped by etape inside each kanban column (server-side grouping in `KanbanColumn`)
- Etape group headings always expanded, no collapse toggle
- Grouping computed server-side; unassigned tasks rendered last under "No etape" label
- `EtapeFilter` type retained for handler compatibility; filter defaults to "all" permanently

**Files (D-F01 through D-F04)**
- Files section header: `.overview-section-heading` with "Files" label + upload trigger right
- File list: `@ui.Table(...)` with columns Filename / Size / Uploaded / Actions
- Each file row: Download `@ui.IconButton` + Delete `@ui.IconButton`; existing
  `FileDeleteConfirmFragment` preserved intact
- Empty state: `@ui.EmptyState(...)` replaces `FileListEmpty()`

### Claude's Discretion
- Exact sub-heading style for etape groups inside kanban columns
- Whether `EtapeFilter` param is fully removed from all handlers or just hidden from UI
  (recommend keeping the type for handler compatibility, defaulting to "show all")
- Exact badge token for the "In progress" status pill (BadgeVariantPrimary or
  BadgeVariantNeutral per design token alignment)
- Tab nav active state CSS — whether to use `.is-active` class or inline conditional classes
  (match Phase 15 sidebar-nav-item pattern)

### Deferred Ideas (OUT OF SCOPE)
- Collapse toggle for etape groups
- Mobile-responsive kanban
- Etape management UI from within the tablo detail
- Task priority/assignee display in task rows
- Files preview pane or thumbnail generation
</user_constraints>

---

<phase_requirements>
## Phase Requirements

| ID | Description | Research Support |
|----|-------------|------------------|
| DETAIL-01 | Tablo detail header area matches the project-card-top design | `.project-card-top` already in `app.css` (Section 15); `project-avatar` and `project-card-title-row` already defined; `@ui.IconButton` and `@ui.Button` ready |
| DETAIL-02 | Task kanban board uses the tasks-section design (section header, task rows, add control) | `.tasks-section*` and `.task-*` CSS in `go-backend/app.css` lines 1253–1348; `KanbanBoard` / `KanbanColumn` / `TaskCard` components identified for in-place restyling |
| DETAIL-03 | Etapes section is visually consistent with the task section | Server-side grouping required; `groupTasksByStatus` pattern (lines 12–18 of `tasks.templ`) is the model for `groupTasksByEtape`; `EtapeStrip` call in `TasksTabFragment` + `TaskCardGone` + `TaskCardOOB` needs removal |
| DETAIL-04 | Files section uses the table component | `@ui.Table` exists in `backend/internal/web/ui/table.templ`; `@ui.EmptyState` exists; `FileListRow` and `FilesTabFragment` are the targets; `FileDeleteConfirmFragment` preserved unchanged |
</phase_requirements>

---

## Architectural Responsibility Map

| Capability | Primary Tier | Secondary Tier | Rationale |
|------------|-------------|----------------|-----------|
| Header layout restyling | Frontend Server (templ) | CSS | Go template renders HTML; CSS controls visual |
| Tab nav active state | Frontend Server (templ) | CSS | Active tab computed server-side (Go `activeTab` param) |
| Etape grouping | Frontend Server (templ/Go) | — | D-E04: grouping is server-side in `KanbanColumn`; pure Go logic |
| CSS class additions | CDN / Static (CSS file) | — | `app.css` is a static asset served by the Go embed |
| Task kanban restyling | Frontend Server (templ) | CSS | Template structure + CSS classes |
| File table restyling | Frontend Server (templ) | CSS | `@ui.Table` component already handles structure |

---

## Standard Stack

### Core (already in `backend/`)

| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| `github.com/a-h/templ` | v0.3.1020 (pinned) | Type-safe Go HTML templates | Project standard; all UI is templ |
| Custom CSS + design tokens | — | Visual styling via `var(--...)` tokens | Established in Phase 13; no Tailwind utility classes in new CSS |
| HTMX | v2 (pinned in justfile) | Server-driven interactivity | Project constraint; no JS framework |
| Sortable.js | v1.15.7 (pinned) | Drag-and-drop kanban | Already wired in `KanbanBoard`; must not be changed |

### Supporting UI Components (all already exist in `backend/internal/web/ui/`)

| Component | Templ function | Phase 13 status |
|-----------|---------------|-----------------|
| Badge | `@ui.Badge(ui.BadgeProps{...})` | Complete |
| Button | `@ui.Button(ui.ButtonProps{...})` | Complete |
| IconButton | `@ui.IconButton(ui.IconButtonProps{...})` | Complete |
| Table | `@ui.Table(ui.TableProps{Head: ..., Body: ...})` | Complete |
| EmptyState | `@ui.EmptyState(ui.EmptyStateProps{...})` | Complete |

**Key gap discovered:** The `UIIcon` switch in `icon_button.templ` supports: `plus`,
`grid3x3`, `list`, `filter`, `search`, `calendar`, `pencil`, `trash`. It does NOT have a
`download` icon or a `chat`/`message` icon. The `IconButton` component's `default` case
renders the icon name as raw text. [VERIFIED: read `icon_button.templ` lines 19–73]

**Action required:** Add `download` and `chat` (or `message-circle`) cases to the `UIIcon`
switch before using `@ui.IconButton` for the Discussion link and file Download buttons.
These are SVG additions only — no logic change.

---

## Architecture Patterns

### System Architecture Diagram

```
Browser GET /tablos/{id}
       |
       v
TabloDetailPage (tablos.templ)
  ├── AppLayout (layout.templ) — sidebar, shell
  ├── Header zone  ──── .project-card-top
  │    ├── .project-card-title-row: .project-avatar + .tablo-title-zone
  │    └── action controls: IconButton(chat) + Button(invite) + IconButton(trash)
  ├── Metadata row ──── .tablo-metadata-row
  │    ├── .tablo-metadata-date + @ui.Badge(status) + progress track/bar
  ├── Tab nav ─────────  .tab-nav
  │    └── .tab-nav-item[.is-active] × 5 tabs
  │         hx-get → /tablos/{id}/{tab} → #tab-content innerHTML
  └── #tab-content
       ├── overview → TabloOverviewTabFragment (desc zone moved here)
       ├── tasks   → TasksTabFragment
       │    └── KanbanBoard → KanbanColumn × 3
       │         └── .tasks-section
       │              ├── .tasks-section-header (h3 + badge + add button)
       │              └── .task-list
       │                   └── .etape-group × N  [server-side grouped]
       │                        ├── .etape-group-header (dot + name)
       │                        └── .task-row × M (checkbox + body + meta)
       ├── files   → FilesTabFragment
       │    ├── .overview-section-heading (h3 + upload button)
       │    ├── @ui.Table (filename / size / date / actions)
       │    └── @ui.EmptyState (when no files)
       ├── discussion → DiscussionTabFragment (unchanged)
       └── events    → EventsTabFragment (unchanged)
```

### Recommended Project Structure

No new directories. All changes are in-place edits to existing files:

```
backend/
├── internal/web/ui/
│   ├── app.css           ← append CSS sections (tasks, etape-group, tab-nav, metadata, progress)
│   └── icon_button.templ ← add "download" and "chat" icon cases to UIIcon switch
└── templates/
    ├── tablos.templ      ← restyle TabloDetailPage header, tab nav, TabloOverviewTabFragment
    ├── tasks.templ       ← restyle KanbanBoard, KanbanColumn, TaskCard; add etape grouping
    ├── etapes.templ      ← remove EtapeStrip (keep EtapeEdit/Create/DeleteConfirmFragment)
    └── files.templ       ← restyle FilesTabFragment with @ui.Table + @ui.EmptyState
```

### Pattern 1: Server-Side Etape Grouping in KanbanColumn

**What:** Mirror `groupTasksByStatus` with a `groupTasksByEtape` helper that returns an
ordered structure for rendering inside a column.

**When to use:** Called from within the `KanbanColumn` templ component via a Go expression.

```go
// Source: [VERIFIED: tasks.templ lines 12-18 — groupTasksByStatus is the model]

// EtapeGroup holds tasks for a single etape label within a kanban column.
type EtapeGroup struct {
    EtapeID    string // "" for unassigned
    EtapeTitle string // "No etape" for unassigned
    EtapeColor string // "" for unassigned
    Tasks      []sqlc.Task
}

// groupTasksByEtape groups a task slice into ordered EtapeGroup slices.
// Preserves etape declaration order; unassigned tasks always appear last.
func groupTasksByEtape(tasks []sqlc.Task, etapes []sqlc.Etape) []EtapeGroup {
    // Build index: etapeID → etape for O(1) lookup
    // Iterate etapes in order to build groups
    // Append unassigned group at end if any tasks have nil EtapeID
}
```

The `KanbanColumn` signature gains an `etapes []sqlc.Etape` parameter. Since
`KanbanColumn` is called from `KanbanBoard`, `KanbanBoard` also receives `etapes` and
passes it through. `KanbanBoard` already receives tasks and filter — adding etapes matches
the existing `TasksTabFragment` signature pattern.

**KanbanBoard current signature:**
```go
// Source: [VERIFIED: tasks.templ line 23]
templ KanbanBoard(tabloID uuid.UUID, csrfToken string, tasks []sqlc.Task, filter EtapeFilter)
```

**Updated signature:**
```go
templ KanbanBoard(tabloID uuid.UUID, csrfToken string, tasks []sqlc.Task, filter EtapeFilter, etapes []sqlc.Etape)
```

All `KanbanBoard` call sites must be updated:
- `tablos.templ` line 413: `@KanbanBoard(tablo.ID, csrfToken, tasks, filter)` [VERIFIED]
- `handlers_tasks.go` lines 589, 639: `templates.KanbanBoard(tablo.ID, csrf.Token(r), tasks, filter)` [VERIFIED]

### Pattern 2: Tab Nav Active State via CSS Class

**What:** Replace the inline conditional class strings (hardcoded `#804EEC` hex) with
`.tab-nav-item` + `.is-active` modifier. Match the Phase 15 `sidebar-nav-item.is-active`
pattern already in `app.css`.

```templ
// Source: [VERIFIED: tablos.templ lines 306-310, 321-325 — current pattern]
// Current (hardcoded hex):
if activeTab == "overview" || activeTab == "" {
    class="flex items-center gap-2 pb-3 px-1 text-sm font-semibold ... text-[#804EEC] border-[#804EEC]"
} else {
    class="flex items-center gap-2 pb-3 px-1 text-sm font-semibold ... text-[#667085] hover:text-gray-900"
}

// New (design token classes):
if activeTab == "overview" || activeTab == "" {
    class="tab-nav-item is-active"
} else {
    class="tab-nav-item"
}
```

### Pattern 3: Project Avatar in Header Context

The `.project-avatar` CSS class with inline `background-color` style is already implemented
in `TabloProjectCard` (Phase 15) and the CSS is in `app.css`. Reuse verbatim for the detail
header.

```templ
// Source: [VERIFIED: tablos.templ lines 104-107 — Phase 15 pattern]
if card.Tablo.Color.Valid && card.Tablo.Color.String != "" {
    <span class="project-avatar" style={ "background-color: " + card.Tablo.Color.String }>
        { string([]rune(card.Tablo.Title)[0:1]) }
    </span>
} else {
    <span class="project-avatar"></span>
}
```

Note: The UI-SPEC requires the first character of the title to appear inside the avatar as
text. The current `TabloProjectCard` implementation shows an empty avatar (no character).
Phase 16 should render the first character per the UI-SPEC.

### Pattern 4: EtapeStrip Removal and OOB Impact

`EtapeStrip` is called from three sites [VERIFIED]:

1. `tablos.templ` `TasksTabFragment` line 412: `@EtapeStrip(tablo.ID, etapes, counts, filter, csrfToken, false)`
2. `tasks.templ` `TaskCardGone` line 386: `@EtapeStrip(tabloID, etapes, counts, filter, csrfToken, true)` (OOB)
3. `tasks.templ` `TaskCardOOB` line 398: `@EtapeStrip(tabloID, etapes, counts, filter, csrfToken, true)` (OOB)

After removal:
- Site 1: Simply delete the `@EtapeStrip(...)` call from `TasksTabFragment`. Also remove
  the `#etape-strip` div from the tasks tab since it no longer exists.
- Sites 2 and 3: Remove the `@EtapeStrip(...)` OOB call. Since EtapeStrip no longer renders
  the `id="etape-strip"` element, the OOB swap target won't exist — removing the call is
  safe and required.
- The `EtapeStrip` templ function itself should be **retained** in `etapes.templ` but not
  called from anywhere. The `etapes []sqlc.Etape`, `counts EtapeTaskCounts`, and
  `filter EtapeFilter` parameters continue flowing through handlers and `TasksTabFragment`
  unchanged (they feed `KanbanBoard` for etape grouping).

### Anti-Patterns to Avoid

- **Changing handler signatures:** `TabloTasksTabHandler`, `TaskCreateHandler`,
  `TaskDeleteHandler`, `TaskReorderHandler`, etc. must not change. All 19 task tests and
  9 file tests verify handler behavior — any signature change breaks them.
- **Removing `EtapeFilter` from handler flow:** The filter is still parsed in
  `handlers_tasks.go` `loadTasksTabData` and passed through templates. Only the UI element
  that generated the filter value (`EtapeStrip`) is removed. The filter param stays "all"
  because nothing sets it anymore.
- **Using Tailwind utility classes in new CSS:** All new CSS in `app.css` must use
  `var(--...)` tokens. Tailwind is compiled separately — adding `tw-` utility classes to
  `.templ` files risks class name collisions (Tailwind uses `@layer` and purging).
- **Modifying `FileDeleteConfirmFragment`:** This fragment's HTMX `hx-target` points to
  `.file-row-zone`. The new `@ui.Table`-based layout must preserve `<tr>` elements with
  `class="file-row-zone"` and `id="file-{id}"` so the existing delete confirm fragment's
  outerHTML swap continues to work.
- **Nesting OOB elements:** `TaskCardOOB` and `TaskCardGone` use `hx-swap-oob` — these
  must remain top-level siblings, never nested inside the primary swap target.

---

## Don't Hand-Roll

| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| Status badge / count badge | Custom `<span>` with inline styles | `@ui.Badge(...)` | Phase 13 established component; CSS already defined |
| Action buttons (Discussion, Invite, Delete) | Raw `<button>` or `<a>` with hardcoded classes | `@ui.IconButton(...)` / `@ui.Button(...)` | Consistent hover/focus states, aria-label enforcement |
| File list | Custom `<ul>` / `<table>` | `@ui.Table(...)` | DETAIL-04 requirement; table component handles `ui-table-shell` CSS |
| Empty state | Custom `<p>` italic placeholder | `@ui.EmptyState(...)` | DS-07 requirement; consistent padding and typography |
| Design tokens | Hardcoded hex values | `var(--color-*)` references | All hex must be replaced — this is the core requirement of the phase |

---

## Runtime State Inventory

This phase is a visual restyling only. No data migrations, no stored keys, no OS-registered
state.

| Category | Items Found | Action Required |
|----------|-------------|------------------|
| Stored data | None — visual CSS/templ changes only | None |
| Live service config | None | None |
| OS-registered state | None | None |
| Secrets/env vars | None | None |
| Build artifacts | `templ generate` must run after `.templ` file edits | Run `just generate` (or `templ generate`) after each templ edit |

---

## Common Pitfalls

### Pitfall 1: `UIIcon` Missing `download` and `chat` Icon Cases

**What goes wrong:** `@ui.IconButton(ui.IconButtonProps{Icon: "download", ...})` renders
the literal string `"download"` as button text instead of an SVG — the `default` case in
`UIIcon` emits `<span>{kind}</span>`.

**Why it happens:** The `UIIcon` switch was built incrementally; only icons needed by prior
phases were added. `download` and a discussion/chat icon were never added.

**How to avoid:** Wave 0 task — add `download` and `chat` (or `message-circle`) SVG cases
to `UIIcon` in `icon_button.templ` before implementing header or file row IconButtons.

**Warning signs:** Rendered button shows "download" or "chat" as text rather than an icon.

### Pitfall 2: `@ui.Table` Requires Wrapping `<tr>` in Body for HTMX File Delete

**What goes wrong:** The existing file delete flow uses `hx-target="closest .file-row-zone"`
with `hx-swap="outerHTML"`. If `FileListRow` is rendered as a `<tr>` inside
`@ui.Table`, the `class="file-row-zone"` and `id="file-{id}"` must be on the `<tr>` element.
If the structure changes (e.g., wrapping in a `<div>`), the outerHTML swap target breaks.

**Why it happens:** `@ui.Table` renders a `<table>` / `<tbody>` — rows must be `<tr>`
elements. The HTMX swap target class must be on the `<tr>` itself.

**How to avoid:** `FileListRow` should render a `<tr class="file-row-zone" id="file-{id}">`.
`FileDeleteConfirmFragment` similarly renders a `<tr class="file-row-zone" id="file-{id}">`.
Both use outerHTML swap — the container element class must match.

**Warning signs:** Delete confirm button causes a full-page reload or HTMX swap error in
browser DevTools.

### Pitfall 3: `KanbanBoard` Call Site Count

**What goes wrong:** `KanbanBoard` is called from three places; missing any one causes a
compile error (templ components are type-checked at generation time) or a runtime panic.

**Why it happens:** `tablos.templ` has one call; `handlers_tasks.go` has two. All three must
receive the new `etapes []sqlc.Etape` parameter when the signature is extended.

**How to avoid:** Run `templ generate && go build ./...` after changing the `KanbanBoard`
signature. Compiler catches all missing parameters immediately.

**Warning signs:** `go build` errors referencing `KanbanBoard` argument count mismatch.

### Pitfall 4: EtapeStrip OOB Removal Leaves Orphaned Swap Target

**What goes wrong:** `TaskCardGone` and `TaskCardOOB` currently emit an OOB swap for
`#etape-strip`. After `EtapeStrip` is removed from the tasks tab, the `#etape-strip` element
no longer exists in the DOM. HTMX silently ignores OOB swaps for missing targets (no error),
but the calls are dead code and should be removed.

**Why it happens:** The OOB pattern was added to keep the etape filter counts fresh after
task operations. With EtapeStrip gone, there is nothing to refresh.

**How to avoid:** Remove the `@EtapeStrip(...)` lines from both `TaskCardGone` and
`TaskCardOOB` as part of the EtapeStrip removal task.

**Warning signs:** Unused `etapes` and `counts` parameters passed to `TaskCardGone` and
`TaskCardOOB` — these can be removed from those component signatures after EtapeStrip removal.

### Pitfall 5: `tasks-section-header h3` Font Size Override

**What goes wrong:** The base `.tasks-section-header h3` rule is 1.6rem (ported from
go-backend's 1.75rem). Inside 18rem-wide kanban columns, 1.6rem headings overflow or look
oversized.

**Why it happens:** The base rule is designed for full-width list views. Kanban columns are
narrow (18rem fixed).

**How to avoid:** Add the scoped override exactly as specified in the UI-SPEC:
```css
.kanban-column .tasks-section-header h3 { font-size: 1rem; }
```
This scoped rule does not change the base rule — future list-view use is unaffected.

### Pitfall 6: `TabloDeleteButtonFragment` Uses `@ui.Button` Not `@ui.IconButton`

**What goes wrong:** The current `TabloDeleteButtonFragment` renders a `@ui.Button` labelled
"Delete". The design decision (D-H02) requires a trash `@ui.IconButton` in the detail header.
However, `TabloDeleteButtonFragment` is also used by `TabloProjectCard` on the dashboard —
changing it breaks the dashboard card.

**Why it happens:** The fragment is the "canonical single source of truth" for the delete
zone (documented in `tablos.templ` comment). The detail page header and the dashboard card
share it.

**How to avoid:** Either (a) parameterize `TabloDeleteButtonFragment` with a variant enum, or
(b) introduce a new `TabloDetailDeleteButton` inline in `TabloDetailPage` that does NOT use
`TabloDeleteButtonFragment`, while leaving the existing fragment for dashboard cards. Option
(b) is simpler and avoids parameter threading.

**Warning signs:** Dashboard card delete button visual changes unexpectedly, or detail page
delete button doesn't match the icon-button style.

---

## Code Examples

### Verified Pattern: `groupTasksByStatus` (model for `groupTasksByEtape`)

```go
// Source: [VERIFIED: backend/templates/tasks.templ lines 12-18]
func groupTasksByStatus(tasks []sqlc.Task) map[sqlc.TaskStatus][]sqlc.Task {
    result := make(map[sqlc.TaskStatus][]sqlc.Task, len(TaskColumns))
    for _, t := range tasks {
        result[t.Status] = append(result[t.Status], t)
    }
    return result
}
```

### Verified Pattern: `@ui.Badge` for task count in column header

```go
// Source: [VERIFIED: backend/templates/tasks.templ lines 111-112]
<span id={ "task-count-badge-" + string(status) }>
    @ui.Badge(ui.BadgeProps{Label: strconv.Itoa(len(tasks)), Variant: ui.BadgeVariantInfo})
</span>
```

### Verified Pattern: `project-avatar` color fallback

```go
// Source: [VERIFIED: backend/templates/tablos.templ lines 104-107]
if card.Tablo.Color.Valid && card.Tablo.Color.String != "" {
    <span class="project-avatar" style={ "background-color: " + card.Tablo.Color.String }></span>
} else {
    <span class="project-avatar"></span>
}
```

### Verified Pattern: `@ui.Table` component signature

```go
// Source: [VERIFIED: backend/internal/web/ui/table.templ]
type TableProps struct {
    Head templ.Component
    Body templ.Component
}
// Usage:
@ui.Table(ui.TableProps{
    Head: fileTableHead(),
    Body: fileTableBody(tabloID, files),
})
```

### Verified Pattern: `@ui.EmptyState` component signature

```go
// Source: [VERIFIED: backend/internal/web/ui/empty_state.templ]
@ui.EmptyState(ui.EmptyStateProps{
    Title:       "No files yet",
    Description: "Upload your first file to get started.",
})
```

### CSS to Port: `.tasks-section` block (from go-backend)

```css
/* Source: [VERIFIED: go-backend/internal/web/ui/app.css lines 1253-1348] */
.tasks-section {
  background: var(--color-surface-default);
  border: 1px solid var(--color-border-subtle);
  border-radius: 1rem;
  overflow: hidden;
}

.tasks-section-header {
  align-items: center;
  border-bottom: 1px solid var(--color-border-muted);
  display: flex;
  justify-content: space-between;
  padding: 1.2rem 1rem;
}

.tasks-add-button {
  align-items: center;
  background: var(--color-surface-default);
  border: 1px solid var(--color-border-muted);
  border-radius: 0.7rem;
  color: var(--color-text-secondary);
  cursor: pointer;
  display: inline-flex;
  font-weight: 500;
  gap: 0.5rem;
  min-height: 2.75rem;
  padding: 0.7rem 1rem;
}

.task-list { display: flex; flex-direction: column; }

.task-row {
  align-items: center;
  border-bottom: 1px solid var(--color-border-muted);
  cursor: pointer;
  display: flex;
  gap: 0.75rem;
  padding: 0.9rem 1rem;
  transition: background-color 0.2s ease;
}

.task-row:hover { background: var(--color-surface-neutral-hover); }

.task-check {
  align-items: center;
  background: var(--color-surface-default);
  border: 2px solid var(--color-border-strong);
  border-radius: 999px;
  color: var(--color-text-inverse);
  cursor: pointer;
  display: inline-flex;
  flex-shrink: 0;
  height: 2rem;
  justify-content: center;
  width: 2rem;
}

.task-check.is-complete {
  background: var(--color-text-brand-strong);
  border-color: var(--color-text-brand-strong);
}

.task-body { flex: 1; min-width: 0; }

.task-body p {
  color: var(--color-surface-muted-inverse);
  font-size: 0.95rem;
  font-weight: 500;
  margin: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.task-row.is-complete .task-body p {
  color: var(--color-text-faint);
  text-decoration: line-through;
}

.task-meta {
  align-items: center;
  color: var(--color-text-muted);
  display: flex;
  flex-wrap: wrap;
  font-size: 0.75rem;
  gap: 0.45rem;
  margin-top: 0.3rem;
}
```

### CSS to Port: Progress bar (from go-backend)

```css
/* Source: [VERIFIED: go-backend/internal/web/ui/app.css lines 1061-1071] */
.project-progress-track {
  background: var(--color-surface-muted);
  border-radius: 999px;
  height: 0.5rem;
  overflow: hidden;
}

.project-progress-bar {
  background: var(--project-color, var(--color-project-fallback));
  border-radius: 999px;
  height: 100%;
}
```

---

## State of the Art

| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| Hardcoded `#804EEC` and `#667085` hex in class attributes | `var(--color-brand-primary)` and `var(--color-text-muted)` design tokens | Phase 13 (tokens), Phase 15 (applied to dashboard) | Phase 16 applies same token discipline to detail page |
| Tailwind utility classes for all styling | CSS classes with design tokens for new UI | Phase 13 | New CSS in `app.css`; existing Tailwind utilities in edit forms are left in place |
| EtapeStrip filter pills | Server-side etape grouping in kanban columns | Phase 16 (this phase) | Simpler mental model; no filter state to manage |
| Flat task list per column | Etape-grouped task list per column | Phase 16 (this phase) | Tasks organized by etape within each status column |

---

## Assumptions Log

| # | Claim | Section | Risk if Wrong |
|---|-------|---------|---------------|
| A1 | The first character of `tablo.Title` is safe to render with `string([]rune(title)[0:1])` — titles are never empty (DB NOT NULL + required form validation) | Code Examples, Pattern 3 | Panic on empty title; add len guard | [ASSUMED] |
| A2 | `ButtonVariantGhost` is NOT used for Invite Member — `@ui.Button` has no Ghost tone. `ButtonTone` only has two values: `ButtonToneSolid` and `ButtonToneSoft`. Use `ButtonToneSoft + ButtonVariantDefault` for the Invite Member button — this renders a soft/outlined button matching the current `border border-[#804EEC]` style. `button.css` confirms `.ui-button-soft.ui-button-default` renders a soft outline. | User Constraints D-H02 | N/A — resolved by code inspection | [RESOLVED] |
| A3 | `FileDeleteConfirmFragment` can be changed to use `<tr>` as its outer element (instead of `<div>`) without breaking its HTMX outerHTML swap behavior | Pitfall 2 | Test breakage in file delete confirm flow | [ASSUMED] |

---

## Open Questions (RESOLVED)

1. **`ButtonVariantGhost` visual treatment** — RESOLVED
   - Resolution: `@ui.Button` does NOT have a Ghost tone. `ButtonTone` only has two values: `ButtonToneSolid` and `ButtonToneSoft`. For the Invite Member button, use `ButtonToneSoft + ButtonVariantDefault`. Inspection of `go-backend/internal/web/ui/button.css` confirms `.ui-button-soft.ui-button-default` renders a soft/outlined button, matching the current `border border-[#804EEC]` visual treatment.
   - Executor action: Use `ui.ButtonProps{Label: "Invite Member", Variant: ui.ButtonVariantDefault, Tone: ui.ButtonToneSoft, Size: ui.SizeMD, Type: "button"}`

2. **`TaskCardGone` and `TaskCardOOB` parameter cleanup after EtapeStrip removal** — RESOLVED
   - Resolution: Remove the `@EtapeStrip(..., true)` OOB calls from both `TaskCardGone` and `TaskCardOOB`. Keep the `etapes []sqlc.Etape` and `counts EtapeTaskCounts` parameters (avoids handler signature changes and test breakage). Add `// TODO: remove etapes and counts params after Phase 16 cleanup` comment. This is safe — HTMX silently ignores OOB swaps for missing targets.

3. **`FileDeleteConfirmFragment` using `<tr>` as outer element** — RESOLVED
   - Resolution: Use `<tr class="file-row-zone" id="file-{id}">` as the outer element in both `FileListRow` and `FileDeleteConfirmFragment`. HTMX outerHTML swap of a `<tr>` within `<tbody>` is valid — browsers accept `<tr>` replacement via innerHTML of `<tbody>`, and HTMX's outerHTML strategy works correctly at this boundary. This is confirmed safe to use.

---

## Environment Availability

Step 2.6: SKIPPED — Phase 16 is purely CSS/templ changes. No external services, runtimes, or CLIs beyond the existing `just generate` (templ) and `go build` that the project already uses.

---

## Validation Architecture

### Test Framework

| Property | Value |
|----------|-------|
| Framework | Go testing + httptest (standard library) |
| Config file | none — `go test ./...` discovers all `_test.go` files |
| Quick run command | `go test ./backend/internal/web/... -run TestTask -count=1` |
| Full suite command | `TEST_DATABASE_URL='postgres://xtablo:xtablo@localhost:5432/xtablo?sslmode=disable' go test ./... -count=1` |

### Phase Requirements → Test Map

| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|--------|----------|-----------|-------------------|-------------|
| DETAIL-01 | Header renders with project-card-top layout | smoke (template render) | `go test ./backend/internal/web/... -run TestTablos -count=1` | ✅ `handlers_tablos_test.go` |
| DETAIL-02 | Task kanban renders with tasks-section CSS classes | smoke (template render) | `go test ./backend/internal/web/... -run TestTasksKanbanRenders -count=1` | ✅ `handlers_tasks_test.go` line 44 |
| DETAIL-03 | Etapes section consistent — no EtapeStrip, grouping in columns | smoke + integration | `TEST_DATABASE_URL=... go test ./backend/internal/web/... -run TestTask -count=1` | ✅ 19 task tests + 8 etape tests |
| DETAIL-04 | Files section uses table component | smoke (template render) | `go test ./backend/internal/web/... -run TestFilesTab -count=1` | ✅ `handlers_files_test.go` line 292 |

### Sampling Rate

- **Per task commit:** `go test ./backend/internal/web/... -count=1` (unit tests, no DB)
- **Per wave merge:** `TEST_DATABASE_URL='postgres://xtablo:xtablo@localhost:5432/xtablo?sslmode=disable' go test ./... -count=1`
- **Phase gate:** Full suite green before `/gsd-verify-work`

### Wave 0 Gaps

- [ ] Add `download` icon SVG case to `UIIcon` switch in `backend/internal/web/ui/icon_button.templ`
- [ ] Add `chat` (or `message-circle`) icon SVG case to `UIIcon` switch
- [ ] Verify `.ui-button-ghost` CSS rule in `app.css` renders a visible border (or determine correct variant for Invite button)

*(Existing test infrastructure covers all phase requirements — no new test files needed. Wave 0 is icon additions + CSS verification only.)*

---

## Security Domain

> This phase is purely visual restyling — no new routes, no authentication changes, no user
> input validation changes, no cryptographic operations. All existing security controls
> (CSRF tokens, ownership checks via `loadOwnedTablo`, auth session middleware) are
> preserved unchanged.

| ASVS Category | Applies | Standard Control |
|---------------|---------|-----------------|
| V2 Authentication | no | unchanged |
| V3 Session Management | no | unchanged |
| V4 Access Control | no | existing `loadOwnedTablo` ownership guard unchanged |
| V5 Input Validation | no | no new form inputs |
| V6 Cryptography | no | no new crypto operations |

No new threat surface introduced.

---

## Sources

### Primary (HIGH confidence)

- `backend/templates/tablos.templ` — verified all current template structures, class names, HTMX wiring
- `backend/templates/tasks.templ` — verified `KanbanBoard`, `KanbanColumn`, `TaskCard`, `EtapeStrip` call sites
- `backend/templates/etapes.templ` — verified `EtapeStrip` structure and all call sites
- `backend/templates/files.templ` — verified `FilesTabFragment`, `FileListRow`, `FileDeleteConfirmFragment` structure
- `backend/internal/web/ui/app.css` — verified which CSS classes are already present vs. need porting
- `backend/internal/web/ui/icon_button.templ` — verified `UIIcon` switch cases; identified missing `download` and `chat` icons
- `backend/internal/web/ui/table.templ` — verified `TableProps` signature
- `backend/internal/web/ui/empty_state.templ` — verified `EmptyStateProps` signature
- `backend/internal/web/ui/variants.go` — verified all variant/tone constant names
- `go-backend/internal/web/ui/app.css` lines 1253–1348 — verified `.tasks-section*` and `.task-*` CSS; lines 1061–1071 verified progress bar CSS
- `backend/internal/web/handlers_tasks.go` — verified `KanbanBoard` call sites and `EtapeFilter` flow
- `backend/internal/web/handlers_files.go` — verified `TabloTasksTabHandler` and `KanbanBoard` call site
- `.planning/phases/16-tablo-detail/16-CONTEXT.md` — all decisions, canonical refs, specifics
- `.planning/phases/16-tablo-detail/16-UI-SPEC.md` — full layout contracts, CSS class specifications, typography, spacing

### Secondary (MEDIUM confidence)

- `backend/internal/web/handlers_tasks_test.go` — 19 test functions identified; all must pass unchanged [VERIFIED: file read + grep]
- `backend/internal/web/handlers_files_test.go` — 9 test functions identified; all must pass unchanged [VERIFIED: grep]

---

## Metadata

**Confidence breakdown:**
- Standard stack: HIGH — all components verified in-tree
- Architecture: HIGH — all call sites verified by code read
- Pitfalls: HIGH — identified from direct code inspection; Pitfall 1 (missing icon) is CRITICAL
- CSS porting: HIGH — source CSS verified in go-backend

**Research date:** 2026-05-16
**Valid until:** 2026-06-16 (stable CSS/templ stack; no fast-moving dependencies)
