# Phase 15: Dashboard & Tablos - Research

**Researched:** 2026-05-16
**Domain:** Go templ layout shell + CSS porting + HTMX dashboard
**Confidence:** HIGH

## Summary

Phase 15 replaces the current top-nav `Layout` with a sidebar-based `AppLayout` and reskins the tablo dashboard as a project-card grid. The go-backend (`go-backend/internal/web/views/dashboard_components.templ` + `go-backend/internal/web/ui/app.css`) is the canonical visual reference and source of CSS to port. All architecture decisions are locked in CONTEXT.md — this research confirms their technical feasibility and documents the exact files to touch, Go patterns to follow, and pitfalls to avoid.

The primary work splits into three parallel tracks: (1) new `AppLayout` templ component in `backend/templates/`, (2) CSS extraction from `go-backend/internal/web/ui/app.css` into a new `backend/internal/web/ui/app.css`, and (3) restyled `TablosDashboard` + `TablosEmptyState` + `TabloCard` in `backend/templates/tablos.templ`. Handler changes are minimal — only the signature of `TablosDashboard` call sites changes.

**Primary recommendation:** Port the CSS sections verbatim from go-backend's app.css (lines 455–743 cover all sidebar CSS; lines 894–1046 cover project-card CSS). Then create `AppLayout` following the `AuthLayout` pattern already established in Phase 14.

---

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

### Locked Decisions

#### App Layout Shell
- **D-L01:** Create a new `AppLayout(title string, user *auth.User, csrfToken string, activePath string, tablos []sqlc.Tablo)` templ in `backend/templates/`. All authenticated dashboard pages use `AppLayout`. The old `Layout` templ is preserved but no longer used by dashboard pages (it becomes dead code, can be removed in a future cleanup).
- **D-L02:** `AppLayout` uses the `.dashboard-shell` CSS grid (sidebar + main columns) from `go-backend/internal/web/ui/app.css`. Port `.dashboard-shell`, `.dashboard-sidebar`, `.sidebar-nav-shell`, all `.sidebar-*` CSS, `.sidebar-organization`, and `.dashboard-main` into `backend/internal/web/ui/app.css` (a new CSS file, not auth.css).
- **D-L03:** Active nav item: `AppLayout` receives `activePath string`. A helper function (pure Go, not templ) returns `"is-active"` class when a nav item's href matches `activePath`. Handlers pass the current route path explicitly (e.g., `"/"`, `"/planning"`, `"/tablos"`).
- **D-L04:** Scripts: `AppLayout` loads `htmx.min.js`, `sortable.min.js`, and `discussion-sse.js` — same as the current `Layout`. No change to script loading.

#### Sidebar Nav Items
- **D-N01:** Match go-backend nav item set: Dashboard (`/`), Tablos (`/` or tablo overview — same as Dashboard for now), Tasks, Planning (`/planning`), Chat, Files, Settings. Use go-backend's `dashboard_components.templ` icon SVGs verbatim.
- **D-N02:** Routes that exist in the backend get real `href` values: Dashboard (`/`), Planning (`/planning`). Routes not yet built as standalone pages (Tasks, Files, Settings) render as visual nav items with no `href` attribute (display only, `cursor: default` on the item).
- **D-N03:** Chat is per-tablo. In the sidebar nav, Chat links to `/` (dashboard) as a placeholder, or is rendered as a visual-only item like Tasks — Claude's discretion.
- **D-N04:** Sidebar-projects section: shows the user's tablos as a list with a colored circle icon (derived from tablo color field) + truncated title. Each item links to `/tablos/{id}`. Section label: "Projects" (English, matching backend convention).

#### Tablo Project Cards
- **D-C01:** Tablo dashboard uses the `.project-grid` 3-column CSS grid layout from go-backend. Port `.project-grid`, `.project-card`, `.project-card-top`, and related CSS into the new `app.css`.
- **D-C02:** Each project card shows (matching go-backend exactly): colored circle avatar (top-left) + title + edit icon button (top-right) + delete icon button (top-right) + creation date (bottom row).
- **D-C03:** Color avatar: use `tablo.Color` field when set. When null/empty, show a default neutral/gray circle (no derived color, no initials). The circle renders via a small `<span>` styled with background-color, matching go-backend's `.sidebar-project-icon` pattern.
- **D-C04:** Edit and delete action icons use `@ui.IconButton(...)` from the design system (Phase 13 component). The HTMX attributes for edit (`hx-get=/tablos/{id}/edit`) and delete (`hx-delete=/tablos/{id}`) are preserved from the current `TabloCard` implementation.
- **D-C05:** The "New tablo" button and inline create form slot (`#create-form-slot`) move to a section header above the grid, matching go-backend's `.overview-section-heading` pattern.

#### Tablo Empty State
- **D-E01:** When no tablos exist, use `@ui.EmptyState(ui.EmptyStateProps{...})` — the Phase 13 component. Replace the current raw HTML empty state in `TablosEmptyState()`. Title: "No tablos yet". Description: "Create your first tablo to get started." Action: "New tablo" button.

#### Sidebar Footer (User/Account)
- **D-F01:** Include a sidebar footer matching go-backend's `.sidebar-organization` section. Shows: avatar circle (first letter of email, colored background) + user email + a small account link. Logout is moved from the old top-nav into this footer area.
- **D-F02:** The account/settings page doesn't exist yet, so the footer button/link only shows the email + logout button. No settings navigation for Phase 15. The `.organization-button` wraps a simple display element, not a dropdown.
- **D-F03:** Port `.sidebar-organization`, `.organization-button`, `.organization-avatar`, `.organization-name`, `.organization-meta` CSS from go-backend into `app.css`.

### Claude's Discretion
- Exact set of SVG icons for each nav item (copy from go-backend `dashboard_components.templ` — no new icon design needed)
- Whether the sidebar-projects section is a separate templ component or inline in `AppLayout` (prefer separate `SidebarProjectsSection` for testability)
- Exact color for the neutral avatar fallback (use `--color-surface-muted` or similar design token)
- Whether sidebar-footer avatar background color is derived from email hash or a fixed brand color
- Chat nav item: visual-only placeholder or links to `/` dashboard

### Deferred Ideas (OUT OF SCOPE)
- Sidebar collapse toggle (JS interaction)
- Mobile-responsive sidebar (hamburger menu, slide-in drawer)
- Tablo color picker in the create/edit form
- Tasks, Files, Settings as standalone pages
</user_constraints>

---

<phase_requirements>
## Phase Requirements

| ID | Description | Research Support |
|----|-------------|------------------|
| DASH-01 | Sidebar uses the go-backend sidebar design (brand section, nav items with icons, tablo list, user/account footer) | AppLayout + SidebarProjectsSection templ components; CSS ported from go-backend app.css lines 455–743 |
| DASH-02 | Tablo list uses project-card layout with color accents, creation date, and action controls | TablosDashboard restyled with `.project-grid` + `.project-card`; CSS from app.css lines 894–1046 |
| DASH-03 | Dashboard empty state uses the empty-state component | `ui.EmptyState` already implemented (Phase 13, `backend/internal/web/ui/empty_state.templ`), ready to use |
</phase_requirements>

---

## Architectural Responsibility Map

| Capability | Primary Tier | Secondary Tier | Rationale |
|------------|-------------|----------------|-----------|
| App layout shell (sidebar + main) | Backend (Go templ) | — | Server-rendered HTML; AppLayout is a templ component |
| Sidebar CSS (`.dashboard-shell`, `.sidebar-nav-shell`, etc.) | Static (CSS file) | — | `backend/internal/web/ui/app.css` serves via `/static/tailwind.css` build |
| Active nav item computation | Backend (pure Go helper) | — | `isActivePath(activePath, href)` pure function, no UI logic |
| Tablo project card grid | Backend (Go templ) | — | `TablosDashboard` restyled; CSS ported |
| Sidebar tablo list (SidebarProjectsSection) | Backend (Go templ) | — | Receives `[]sqlc.Tablo` from `AppLayout` |
| Color avatar rendering | Backend (Go templ) | — | Inline `style=` attribute with `tablo.Color` or fallback token |
| Empty state | Backend (Go templ) | — | `@ui.EmptyState(...)` already in Phase 13 |
| HTMX interactions (edit, delete) | Browser (HTMX attributes) | Backend (handler) | Attributes remain unchanged from current `TabloCard` |

---

## Standard Stack

### Core (already in backend — no new installs)

| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| templ | see go.mod | HTML templating for Go | Project standard; all templates use it |
| sqlc | see go.mod | Type-safe SQL | Project standard; `sqlc.Tablo` is the data type |
| chi | see go.mod | HTTP routing | Project standard |
| gorilla/csrf | see go.mod | CSRF token injection | Project standard; `csrf.Token(r)` in every handler |

### Supporting (CSS tokens)

| Asset | Location | Purpose | When to Use |
|-------|----------|---------|-------------|
| `backend/internal/web/ui/base.css` | Existing | Design tokens (`var(--...)`) | All new CSS must use these tokens, not raw colors |
| `go-backend/internal/web/ui/app.css` | Reference | Source CSS to port verbatim | Extract only sidebar + project-card sections |
| `backend/static/logo_dark.png` | Existing | Sidebar brand logo | Already copied in Phase 14 |

**Version verification:** No new packages needed — all required tools are already in the go.mod. [VERIFIED: codebase grep]

---

## Architecture Patterns

### System Architecture Diagram

```
HTTP GET /
    |
    v
TablosListHandler
    |-- Queries.ListTablosByUserWithDiscussionUnread(userID) --> []ListRow
    |-- Queries.ListTablosByUser(userID)                    --> []sqlc.Tablo  (for sidebar)
    v
templates.TablosDashboard(user, csrfToken, activePath="/", tablos, cardViews)
    |
    v
AppLayout(title, user, csrfToken, activePath, tablos)
    |-- DashboardSidebar
    |     |-- brand section (logo_dark.png + "XTablo")
    |     |-- SidebarNavItems (panels/tasks/layers/planning/chat/files)
    |     |-- SidebarProjectsSection(tablos)
    |     |-- SidebarOrganizationFooter(user.Email)
    v
    |-- <main id="app-main-content">
          { children... }
          |-- section-heading (New tablo button + #create-form-slot)
          |-- #tablos-list
                |-- if empty: @ui.EmptyState(...)
                |-- else: for each card: @TabloProjectCard(card, csrfToken)
```

### Recommended Project Structure

```
backend/
├── templates/
│   ├── app_layout.templ         # NEW — AppLayout + SidebarProjectsSection + SidebarNavItem
│   ├── app_layout_helpers.go    # NEW — isActivePath, sidebarNavItemClass, sidebarNavItemID
│   ├── tablos.templ             # MODIFIED — TablosDashboard, TablosEmptyState, TabloCard
│   ├── layout.templ             # UNCHANGED (preserved as dead code)
│   └── ...
├── internal/web/ui/
│   ├── app.css                  # NEW — ported from go-backend app.css (sidebar + project-card sections only)
│   ├── base.css                 # UNCHANGED — design tokens used by app.css
│   └── ...
├── internal/web/
│   ├── handlers_tablos.go       # MODIFIED — TablosDashboard call adds activePath + tablos params
│   ├── handlers_planning.go     # MODIFIED — Planning page switches to AppLayout
│   └── ...
```

### Pattern 1: AppLayout Templ (follow AuthLayout pattern)

The AuthLayout in Phase 14 is the established pattern for a top-level HTML shell. `AppLayout` follows the same convention — package `templates`, explicit param list, `{ children... }` slot.

```go
// Source: backend/templates/auth_layout.templ (Phase 14 pattern)
// backend/templates/app_layout.templ
package templates

import (
    "backend/internal/auth"
    "backend/internal/db/sqlc"
)

templ AppLayout(title string, user *auth.User, csrfToken string, activePath string, tablos []sqlc.Tablo) {
    <!DOCTYPE html>
    <html lang="en">
        <head>
            <meta charset="utf-8"/>
            <meta name="viewport" content="width=device-width, initial-scale=1"/>
            <title>{ title }</title>
            <link rel="stylesheet" href="/static/tailwind.css"/>
        </head>
        <body>
            <div class="dashboard-shell">
                @DashboardSidebar(activePath, tablos, user, csrfToken)
                <main id="app-main-content" class="dashboard-main flex-1 overflow-auto">
                    { children... }
                </main>
            </div>
            <script src="/static/htmx.min.js" defer></script>
            <script src="/static/sortable.min.js" defer></script>
            <script src="/static/discussion-sse.js" defer></script>
        </body>
    </html>
}
```

[VERIFIED: codebase read — `AuthLayout` signature + `Layout` script list]

### Pattern 2: Active Nav Item Helper (pure Go, not templ)

```go
// Source: go-backend/internal/web/views/home.go — sidebarNavItemClass, isActivePath
// backend/templates/app_layout_helpers.go
package templates

import "strings"

func sidebarNavItemClass(active bool) string {
    if active {
        return "sidebar-nav-item is-active"
    }
    return "sidebar-nav-item"
}

func isActivePath(activePath string, href string) bool {
    return strings.TrimSpace(activePath) != "" && activePath == href
}
```

[VERIFIED: go-backend/internal/web/views/home.go lines 17–37]

### Pattern 3: SidebarProjectsSection as separate templ

Per D-N04, sidebar projects render colored circle + truncated title. The go-backend uses `.sidebar-project-icon` as a circle (border-radius: 999px). The backend `sqlc.Tablo` type has `Color pgtype.Text` (nullable). The color span uses inline `style=` only when color is set.

```go
// Source: go-backend/internal/web/views/dashboard_components.templ lines 103–115
// backend/templates/app_layout.templ (separate templ component)
templ SidebarProjectsSection(tablos []sqlc.Tablo) {
    <div id="sidebar-projects-section" class="sidebar-projects">
        <hr role="separator"/>
        <div class="sidebar-section-label">Projects</div>
        <ul class="sidebar-project-list">
            for _, tablo := range tablos {
                <li>
                    <a class="sidebar-project-link" href={ templ.SafeURL("/tablos/" + tablo.ID.String()) }>
                        <span class="sidebar-project-icon"
                            if tablo.Color.Valid && tablo.Color.String != "" {
                                style={ "background-color: " + tablo.Color.String }
                            }>
                        </span>
                        <span class="sidebar-project-label">{ tablo.Title }</span>
                    </a>
                </li>
            }
        </ul>
    </div>
}
```

[VERIFIED: go-backend templ + backend sqlc.Tablo type from handlers_tablos.go + discussion_forms.go]

### Pattern 4: TabloProjectCard restyled

The existing `TabloCard` wraps `@ui.Card(...)`. The new `TabloProjectCard` (or restyled `TabloCard`) uses the `.project-card` CSS directly. Edit/delete use `@ui.IconButton(...)` (already available, Phase 13).

Key difference from go-backend: backend uses `hx-get=/tablos/{id}/edit` (not a full-page swap) and `hx-post=/tablos/{id}/delete` — **preserve existing HTMX attributes exactly**, only restyle the surrounding HTML.

```go
// Source: go-backend/internal/web/views/tablos.templ TabloGridCardWithAttrs
// Adapted for backend's HTMX interaction contract
templ TabloProjectCard(card TabloCardView, csrfToken string) {
    <article id={ "tablo-" + card.Tablo.ID.String() } class="project-card">
        <div class="project-card-top">
            <div class="flex items-center gap-2">
                @ui.IconButton(ui.IconButtonProps{
                    Label: "Edit tablo", Icon: "pencil",
                    Variant: ui.IconButtonVariantNeutral, Tone: ui.IconButtonToneGhost,
                    Type: "button",
                    Attrs: templ.Attributes{
                        "hx-get": "/tablos/" + card.Tablo.ID.String() + "/edit",
                        // ...existing HTMX attrs...
                    },
                })
                // delete icon button with existing HTMX attrs
            </div>
        </div>
        <div class="project-card-title-row">
            <span class="project-avatar"
                if card.Tablo.Color.Valid && card.Tablo.Color.String != "" {
                    style={ "background-color: " + card.Tablo.Color.String }
                }>
            </span>
            <h4>{ card.Tablo.Title }</h4>
        </div>
        <div class="project-date-row">
            // creation date from card.Tablo.CreatedAt
        </div>
    </article>
}
```

[ASSUMED: exact HTMX attribute targets need to be verified against current TabloDeleteButtonFragment and edit handler behavior before writing the final template]

### Pattern 5: Handler Changes — TablosDashboard Call Sites

Currently, `TablosDashboard(user, csrfToken, tabloCards)` is called from `TablosListHandler`. Per D-L01, the new signature is `TablosDashboard(user, csrfToken, activePath, tablos, cardViews)` where `tablos []sqlc.Tablo` feeds the sidebar and `cardViews []TabloCardView` feeds the grid.

The tablos list is **already fetched** by `TablosListHandler` — it calls `ListTablosByUserWithDiscussionUnread` which returns `[]ListRow`. We need a second lightweight query `ListTablosByUser(userID)` → `[]sqlc.Tablo` for the sidebar (or derive from existing rows).

```go
// Source: backend/internal/db/sqlc/tablos.sql.go line 88–100
// ListTablosByUser already exists:
// func (q *Queries) ListTablosByUser(ctx context.Context, userID uuid.UUID) ([]Tablo, error)
```

[VERIFIED: grep in backend/internal/db/sqlc/tablos.sql.go]

**Simpler option:** Derive `[]sqlc.Tablo` from the already-fetched unread rows without a second DB query:

```go
// In TabloCardsFromUnreadRows, each row already contains all Tablo fields.
// We can produce []sqlc.Tablo by extracting the Tablo field from each TabloCardView.
tablos := make([]sqlc.Tablo, 0, len(cardViews))
for _, cv := range cardViews {
    tablos = append(tablos, cv.Tablo)
}
```

This avoids an extra query. [ASSUMED: confirm this approach in planning since it avoids a new DB call but relies on already-fetched data order]

### Pattern 6: Planning and Other Layout Callers

Currently `planning.templ` calls `@Layout(...)`. Per D-L01, it must switch to `@AppLayout(...)`. Planning handler (`PlanningPageHandler`) does not currently fetch tablos. It will need to fetch `[]sqlc.Tablo` for the sidebar. The `PlanningDeps` struct already has `Queries *sqlc.Queries`, so `deps.Queries.ListTablosByUser(ctx, user.ID)` is available.

Pages that call `@Layout(...)` today and need to switch:
- `tablos.templ`: `TablosDashboard`, `TabloDetailPage`, `TabloNotFoundPage`
- `planning.templ`: `PlanningPage`
- `account_providers.templ`: `AccountProvidersPage` (uses `@Layout`)

[VERIFIED: grep of Layout( in backend/templates/*.templ]

### Anti-Patterns to Avoid

- **Re-using `.sidebar-project-icon` as a colored SVG wrapper:** In go-backend it wraps an SVG icon. In this phase, per D-C03, it should wrap a plain colored `<span>` (no SVG) — the color is the only visual indicator.
- **Nesting OOB elements inside AppLayout:** OOB HTMX fragments (`hx-swap-oob`) must be top-level siblings, not nested under AppLayout. This pattern is already established in `TabloCardWithOOBFormClear`.
- **Hard-coded colors in app.css:** All color values must reference `var(--...)` design tokens from `base.css`. The go-backend app.css already does this — port verbatim.
- **Two queries where one suffices:** `ListTablosByUserWithDiscussionUnread` already returns all tablo fields; derive `[]sqlc.Tablo` from it rather than issuing a second query in `TablosListHandler`.

---

## Don't Hand-Roll

| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| Empty state component | Raw HTML `<div>` with inline styles | `@ui.EmptyState(ui.EmptyStateProps{...})` | Phase 13 component; `.ui-empty-state` CSS already loaded |
| Icon buttons (edit/delete on card) | Raw `<button>` with SVG | `@ui.IconButton(ui.IconButtonProps{...})` | Phase 13 component; `.ui-icon-button-*` CSS already loaded |
| CSRF hidden field | `<input type="hidden">` | `@ui.CSRFField(csrfToken)` | Existing helper; gorilla/csrf invariant |
| Design tokens | Inline hex color strings | `var(--color-*)` tokens from base.css | Token system already in place; ensures theme coherence |

---

## Common Pitfalls

### Pitfall 1: `tailwind.css` serves the compiled CSS bundle — app.css must be imported into it

**What goes wrong:** Creating `backend/internal/web/ui/app.css` but forgetting to `@import` it into `backend/internal/web/ui/tailwind.css` (or equivalent entry point). The file exists but its CSS never reaches the browser.

**Why it happens:** The backend serves `/static/tailwind.css` as the CSS entry point. All component CSS files (`auth.css`, `button.css`, etc.) are imported into this bundle. A new `app.css` must be added the same way.

**How to avoid:** Check how `auth.css` is imported (it was added in Phase 14) and add `app.css` the same way.

**Warning signs:** Sidebar renders with no styles in the browser despite the file existing.

[VERIFIED: `backend/internal/web/ui/auth.css` exists; Phase 14 added it — same pattern needed for `app.css`]

### Pitfall 2: `Color pgtype.Text` null-safety in templates

**What goes wrong:** Accessing `tablo.Color.String` directly without checking `tablo.Color.Valid` causes the template to emit an empty or zero-value string when Color is NULL in the database.

**Why it happens:** pgtype.Text is a nullable wrapper; `.Valid` must be checked before using `.String`.

**How to avoid:** Always guard: `if tablo.Color.Valid && tablo.Color.String != ""` before using `tablo.Color.String` in a `style=` attribute.

[VERIFIED: existing `tabloCardBody` in tablos.templ already demonstrates this pattern at lines 85–90]

### Pitfall 3: HTMX target for edit action changed between go-backend and backend

**What goes wrong:** Copying go-backend's `EditTabloButton` HTMX attrs verbatim. Go-backend uses `hx-target="#app-main-content"` (full-page swap). The backend uses in-place zone swaps (`hx-target="closest .tablo-delete-zone"`).

**Why it happens:** The two codebases have different interaction contracts for edit/delete.

**How to avoid:** Per D-C04, preserve the existing HTMX attributes from the current `TabloDeleteButtonFragment` and inline edit flow. Only restyle the surrounding HTML structure — do not change the `hx-*` attributes.

[VERIFIED: current tablos.templ + go-backend tablos.templ comparison]

### Pitfall 4: `AppLayout` children slot — `TablosListHandler` renders `TablosDashboard` which calls `AppLayout`

**What goes wrong:** `TablosListHandler` currently calls `templates.TablosDashboard(user, csrf, cards).Render(...)`. After the change, `TablosDashboard` renders inside `AppLayout` via `{ children... }`. The handler call must pass `activePath` and `tablos` to `TablosDashboard`, which in turn passes them to `AppLayout`.

**How to avoid:** Update the `TablosDashboard` templ signature to accept `activePath string` and `tablos []sqlc.Tablo` and forward them to `AppLayout`. Update the handler call accordingly.

### Pitfall 5: `templ generate` must run after every `.templ` file change

**What goes wrong:** Editing `.templ` files without re-running `templ generate` means the `*_templ.go` generated files are stale. Go compilation fails or uses old logic.

**How to avoid:** The project uses `air` for hot-reload which calls `templ generate` automatically in dev. For Wave 0 / test tasks, run `go generate ./...` or `templ generate` explicitly.

[VERIFIED: `go-backend/justfile` and project convention]

### Pitfall 6: CreatedAt formatting — `pgtype.Timestamptz` not `time.Time`

**What goes wrong:** Calling `.Format(...)` directly on `card.Tablo.CreatedAt` — it is `pgtype.Timestamptz`, not `time.Time`. The Time value is at `.Time` field.

**How to avoid:** Use `card.Tablo.CreatedAt.Time.Format("Jan 2, 2006")` or a helper function (follow go-backend's `dates.FormatFrenchDate` pattern, but using English format per backend convention).

[VERIFIED: sqlc.Tablo type — CreatedAt is pgtype.Timestamptz; existing files use `.Time` accessor pattern]

---

## Code Examples

### CSS sections to port from go-backend app.css

**Sidebar CSS block** (go-backend app.css lines 455–743):
- `.dashboard-shell` — grid layout (line 455)
- `.dashboard-sidebar` — sidebar column (line 463)
- `.sidebar-nav-shell` — sticky nav shell (line 467)
- `.sidebar-brand`, `.sidebar-brand-link`, `.sidebar-brand-logo`, `.sidebar-brand-title` (lines 481–509)
- `.sidebar-collapse-button` (lines 511–527) — include but will be non-functional (no JS in Phase 15)
- `.sidebar-primary`, `.sidebar-list`, `.sidebar-divider` (lines 533–558)
- `.sidebar-nav-item`, `.sidebar-nav-item:hover`, `.sidebar-nav-item.is-active` (lines 560–578)
- `.sidebar-nav-link`, `.sidebar-nav-link-inner`, `.sidebar-nav-icon`, `.sidebar-nav-label` (lines 580–605)
- `.sidebar-projects`, `.sidebar-section-label`, `.sidebar-project-list` (lines 607–628)
- `.sidebar-project-link`, `.sidebar-project-icon`, `.sidebar-project-label` (lines 630–668)
- `.sidebar-footer-links` (lines 670–673)
- `.sidebar-organization` + all `.organization-*` classes (lines 675–732)
- `.dashboard-main` (lines 734–741)

**Project card CSS block** (go-backend app.css lines 893–1046):
- `.overview-section`, `.overview-section-heading` (lines 875–884)
- `.project-grid` (lines 894–899)
- `.project-card`, `.project-card-top` (lines 901–914)
- `.project-card-top .borderless-icon-button` overrides (lines 945–963) — NOTE: backend uses `.ui-icon-button-*` classes (Phase 13), not `.borderless-icon-button`; these overrides may need adaptation
- `.project-card-title-row`, `.project-avatar` (lines 986–1005)
- `.project-date-row` (lines 1039–1046)

[VERIFIED: go-backend/internal/web/ui/app.css line counts verified by reading the file]

### Mapping `ui.IconButton` to project-card context

The go-backend uses `.borderless-icon-button` as a class modifier. The backend's `ui.IconButton` generates classes like `.ui-icon-button-ghost.ui-icon-button-neutral`. The CSS overrides in go-backend (lines 945–963) target `.borderless-icon-button` — these must be adapted to target the Phase 13 class names instead.

```css
/* In app.css — adapt from go-backend lines 945-963 */
.project-card-top .ui-icon-button {
    padding: 0;
}
.project-card-top .ui-icon-button-ghost.ui-icon-button-neutral:hover {
    color: var(--color-surface-muted-inverse);
}
.project-card-top .ui-icon-button-ghost.ui-icon-button-danger:hover {
    color: var(--color-status-danger-icon-hover);
}
```

[ASSUMED: exact Phase 13 `IconButtonClass()` output needs to be confirmed against `backend/internal/web/ui/variants.go` before writing final CSS]

---

## State of the Art

| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| Top-nav `Layout` shell | Sidebar `AppLayout` shell | Phase 15 (this phase) | All authenticated pages must use `AppLayout` |
| Raw HTML empty state | `@ui.EmptyState(...)` component | Phase 13 (already done) | TablosEmptyState must be replaced |
| Raw icon buttons | `@ui.IconButton(...)` | Phase 13 (already done) | TabloCard delete button already uses ui.Button; switch to IconButton |

---

## Assumptions Log

| # | Claim | Section | Risk if Wrong |
|---|-------|---------|---------------|
| A1 | Derive `[]sqlc.Tablo` from already-fetched `TabloCardView.Tablo` fields rather than a second DB query | Code Examples / Pattern 5 | Low — fallback is to call `ListTablosByUser` separately; minor perf difference only |
| A2 | Phase 13 `IconButtonClass()` generates `.ui-icon-button-ghost` and `.ui-icon-button-neutral` / `.ui-icon-button-danger` as class names | Code Examples — CSS mapping | Medium — if class names differ, project-card-top icon hover styles won't work; fix by reading `variants.go` |
| A3 | HTMX edit interaction for TabloCard uses a modal or page-level swap (not inline zone swap) | Pattern 4 | Medium — the edit path in go-backend uses full-page swap; the backend edit handler may differ; check `handlers_tablos.go` TablosEditHandler |

---

## Open Questions (RESOLVED)

1. **Does TabloDetailPage also switch to AppLayout in Phase 15?** [RESOLVED]
   - What we know: CONTEXT.md says "All authenticated dashboard pages use AppLayout" (D-L01). `TabloDetailPage` is a dashboard page.
   - What's unclear: CONTEXT.md phase boundary says "Does NOT include: tablo detail restyling (Phase 16)". This is ambiguous — does the layout shell switch happen in Phase 15 or Phase 16?
   - Resolution: Switch `TabloDetailPage` to `AppLayout` in Phase 15 (layout shell only, no restyling of the detail content). This avoids a regression where the sidebar disappears on the detail page after Phase 15 goes live. Phase 16 reskins the detail content, not the layout shell.

2. **What is the exact HTMX edit interaction for TabloCard in the current backend?** [RESOLVED]
   - What we know: `TabloDeleteButtonFragment` uses inline zone swap. Current `tabloCardBody` has only a "View" link, no edit button.
   - Resolution (verified against `backend/templates/tablos.templ`): The existing `TabloCard` uses two separate inline edit routes — `hx-get="/tablos/{id}/edit-title"` (outerHTML swap on the title zone) and `hx-get="/tablos/{id}/edit-desc"` (outerHTML swap on the description zone). There is NO single `/tablos/{id}/edit` route. The edit icon on `TabloProjectCard` must use `hx-get="/tablos/{id}/edit-title"` with `hx-target` matching the card's title zone, consistent with the existing `TabloCard` behavior. D-C04's reference to `hx-get=/tablos/{id}/edit` in CONTEXT.md was imprecise; the real route is `/edit-title`.

3. **How does CSS enter the `/static/tailwind.css` bundle?** [RESOLVED]
   - What we know: `auth.css` was added in Phase 14 and works. The bundle entry point is somewhere in `backend/internal/web/ui/`.
   - Resolution: The entry point is a single CSS file with `@import` directives. Check how `auth.css` was imported in Phase 14 (grep for `@import.*auth` in `backend/internal/web/ui/`) and add `app.css` the same way. Confirmed pattern from Phase 14 SUMMARY.

---

## Environment Availability

Step 2.6: SKIPPED — phase is purely code/template/CSS changes; no new external tools, services, or CLIs required. All Go tooling (`templ`, `sqlc`, `go build`) already confirmed available in previous phases.

---

## Validation Architecture

### Test Framework

| Property | Value |
|----------|-------|
| Framework | Go `testing` + `net/http/httptest` (existing) |
| Config file | none — standard Go test runner |
| Quick run command | `cd backend && go test ./internal/web/... -run TestTablos -v` |
| Full suite command | `cd backend && go test ./...` |

### Phase Requirements → Test Map

| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|--------|----------|-----------|-------------------|-------------|
| DASH-01 | Sidebar renders brand, nav items, tablo list, footer in GET / response | integration | `go test ./internal/web/... -run TestTablosDashboard_Sidebar` | ❌ Wave 0 |
| DASH-02 | GET / renders `.project-card` elements for each tablo | integration | `go test ./internal/web/... -run TestTablosDashboard_ProjectCards` | ❌ Wave 0 |
| DASH-03 | GET / with zero tablos renders `.ui-empty-state` | integration | `go test ./internal/web/... -run TestTablosDashboard_EmptyState` | ❌ Wave 0 |
| (regression) | Existing TABLO-01..06 handler tests pass unchanged | integration | `go test ./internal/web/... -run TestTablos` | ✅ `handlers_tablos_test.go` |

### Sampling Rate

- **Per task commit:** `cd backend && go test ./internal/web/... -run TestTablos -count=1`
- **Per wave merge:** `cd backend && go test ./... -count=1`
- **Phase gate:** Full suite green before `/gsd-verify-work`

### Wave 0 Gaps

- [ ] `backend/internal/web/handlers_tablos_test.go` — add `TestTablosDashboard_Sidebar`, `TestTablosDashboard_ProjectCards`, `TestTablosDashboard_EmptyState` test functions
- [ ] Tests should assert HTML structure using `strings.Contains` on response body (matching existing pattern in `handlers_auth_test.go`)

---

## Security Domain

### Applicable ASVS Categories

| ASVS Category | Applies | Standard Control |
|---------------|---------|-----------------|
| V2 Authentication | No | Phase 15 is UI-only; auth unchanged |
| V3 Session Management | No | No new session logic |
| V4 Access Control | No | No new routes or access rules |
| V5 Input Validation | No | No new form inputs |
| V6 Cryptography | No | No new crypto |

No new security surface in Phase 15 — it is a pure visual restyle of existing authenticated routes. All existing CSRF, session, and auth middleware remain unchanged. [VERIFIED: phase boundary confirms no new routes or form handlers]

---

## Sources

### Primary (HIGH confidence)

- `go-backend/internal/web/views/dashboard_components.templ` — complete sidebar + project-card templ structure (read in this session)
- `go-backend/internal/web/ui/app.css` — complete CSS source for all sidebar + project-card styles (read in this session)
- `go-backend/internal/web/views/home.go` — helper functions: `sidebarNavItemClass`, `isActivePath`, `sidebarNavItemID`, `sidebarPrimaryNavItems`, `sidebarProjectItems` (read in this session)
- `backend/templates/tablos.templ` — current template being replaced (read in this session)
- `backend/templates/layout.templ` — current layout being superseded (read in this session)
- `backend/templates/auth_layout.templ` — Phase 14 pattern for AppLayout to follow (read in this session)
- `backend/templates/discussion_forms.go` — `TabloCardView` type and `TabloCardsFromUnreadRows` (read in this session)
- `backend/internal/web/ui/empty_state.templ` — `ui.EmptyState` component signature (read in this session)
- `backend/internal/web/ui/icon_button.templ` — `ui.IconButton` component signature (read in this session)
- `backend/internal/web/handlers_tablos.go` — current handler call sites (read in this session)
- `backend/internal/db/sqlc/tablos.sql.go` — `ListTablosByUser` query exists (verified via grep)

### Secondary (MEDIUM confidence)

- `backend/internal/web/handlers_planning.go` — planning handler structure; will need tablos fetch added (read in this session)

---

## Metadata

**Confidence breakdown:**
- Standard stack: HIGH — no new packages; all dependencies already in project
- Architecture: HIGH — canonical reference (go-backend) read directly; exact CSS lines identified
- Pitfalls: HIGH — verified against actual codebase code patterns
- Handler changes: MEDIUM — edit interaction HTMX target for TabloCard open question (Open Question #2)

**Research date:** 2026-05-16
**Valid until:** 2026-06-16 (stable CSS-only domain; unlikely to change)
