diff --git a/.planning/phases/38-admin-tag-management/38-UI-SPEC.md b/.planning/phases/38-admin-tag-management/38-UI-SPEC.md
new file mode 100644
index 0000000..b49afa8
--- /dev/null
+++ b/.planning/phases/38-admin-tag-management/38-UI-SPEC.md
@@ -0,0 +1,258 @@
+---
+phase: 38
+slug: admin-tag-management
+status: draft
+shadcn_initialized: false
+preset: none
+created: 2026-04-19
+---
+
+# Phase 38 — UI Design Contract
+
+> Visual and interaction contract for Phase 38: Admin Tag Management.
+> Generated by gsd-ui-researcher. All values extracted from existing codebase — no user questions required.
+
+---
+
+## Design System
+
+| Property | Value |
+|----------|-------|
+| Tool | none — hand-crafted Tailwind v4 |
+| Preset | not applicable |
+| Component library | none (custom components only) |
+| Icon library | Lucide via `LucideIcon` from `src/client/lib/iconData` |
+| Font | system default (no custom font declared in app.css) |
+
+Source: `src/client/app.css` (single `@import "tailwindcss"` — no preset), existing admin routes.
+
+---
+
+## Spacing Scale
+
+Declared values (multiples of 4):
+
+| Token | Value | Usage |
+|-------|-------|-------|
+| xs | 4px | Icon gaps (`gap-1`), inline badge padding (`px-1.5 py-0.5`) |
+| sm | 8px | Tag chip gaps (`gap-2`), compact row padding (`py-2`) |
+| md | 16px | Default field spacing (`mt-4`), grid gaps (`gap-4`) |
+| lg | 24px | Section padding (`p-6`), page section breaks (`pt-6`) |
+| xl | 32px | Form bottom margin (`mt-8`) |
+| 2xl | 48px | Not used in admin shell |
+| 3xl | 64px | Not used in admin shell |
+
+Exceptions:
+- Tree row indent: 20px per depth level (value between `sm` and `md` — closest Tailwind class: `pl-5` per level). Source: CONTEXT.md specifics, "16-24px per level".
+- Chevron toggle button: 28px click target minimum (`p-1` around 16px icon = 24px; acceptable for desktop-only admin tool).
+
+---
+
+## Typography
+
+| Role | Size | Weight | Line Height |
+|------|------|--------|-------------|
+| Body | 14px (text-sm) | 400 (regular) | 1.5 |
+| Label | 12px (text-xs) | 600 (semibold) | 1.4 |
+| Heading | 18px (text-lg) | 600 (semibold) | 1.2 |
+| Display | 12px (text-xs) | 600 (semibold, uppercase, tracking-wide) | 1.4 |
+
+Source: `src/client/routes/admin/items.tsx` — `text-lg font-semibold text-gray-900`, `text-xs font-semibold text-gray-400 uppercase tracking-wide`, `text-sm`.
+
+Display role = table column headers (uppercase, tracked, muted gray-400). Not a separate heading level.
+
+---
+
+## Color
+
+| Role | Value | Usage |
+|------|-------|-------|
+| Dominant (60%) | `#f9fafb` (gray-50) | Main content area background (`bg-gray-50`) |
+| Secondary (30%) | `#ffffff` (white) | Cards, sidebar, modal dialogs, form containers (`bg-white`) |
+| Accent (10%) | `#2563eb` (blue-600) | Primary save/submit buttons, active input ring |
+| Destructive | `#dc2626` (red-600) | Delete button background, delete confirmation button only |
+
+Accent reserved for:
+- "Save Changes" / "Add Tag" primary submit button (`bg-blue-600 hover:bg-blue-700 text-white`)
+- Input focus ring (`focus:ring-2 focus:ring-blue-500/20 focus:border-blue-300`)
+- Active tag filter chip in existing items list (`bg-blue-50 text-blue-600 border border-blue-200`)
+
+Destructive reserved for:
+- Delete button on edit page (`border-red-200 text-red-600 hover:bg-red-50`)
+- Confirm delete button inside modal (`bg-red-600 hover:bg-red-700 text-white`)
+
+All other interactive elements (back links, chevron toggles, row hovers) use gray-scale only.
+
+Source: `src/client/routes/admin/items.$itemId.tsx` — extracted exact Tailwind classes.
+
+---
+
+## Component Inventory
+
+### Existing (reuse without modification)
+
+| Component | File | Usage in Phase 38 |
+|-----------|------|-------------------|
+| Admin shell layout | `src/client/routes/admin.tsx` | Unchanged — enable Tags sidebar Link |
+| Tags sidebar Link | `src/client/routes/admin.tsx` L43-52 | Change disabled `
` → `
` |
+| Input class pattern | `items.$itemId.tsx` L177 | Reuse `inputClass` constant verbatim |
+| Label class pattern | `items.$itemId.tsx` L178 | Reuse `labelClass` constant verbatim |
+| Section divider class | `items.$itemId.tsx` L179 | Reuse `sectionClass` constant verbatim |
+| Delete confirmation modal | `items.$itemId.tsx` L397-432 | Extend with child count copy; same structural pattern |
+| Back navigation button | `items.$itemId.tsx` L212-218 | Replicate with "← Tags" label |
+| Skeleton loader rows | `items.tsx` L139-148 | Reuse `animate-pulse` skeleton pattern |
+| Error state | `items.tsx` L105-109 | Replicate for tag list error state |
+
+### New (build for Phase 38)
+
+| Component | Description | Visual Spec |
+|-----------|-------------|-------------|
+| `TagTreeRow` | Single row in the collapsible tree | See Tree Row spec below |
+| `TagTreeView` | Container rendering all tree rows from flat list | Renders inside `rounded-xl border border-gray-100 bg-white` card |
+| `TagQuickAddForm` | Inline form above the tree | Name input + parent picker + "Add Tag" button |
+| `TagParentPicker` | Searchable dropdown for parent selection | Reuse `inputClass` for trigger; popover with filtered options |
+
+---
+
+## Tree Row Visual Spec
+
+Each row in the collapsible tag tree:
+
+```
+[ chevron ] [ tag name ] [ item count ] [ Edit ]
+ indent per level
+```
+
+### Structure
+
+- Indent: `pl-5` (20px) per depth level starting at depth 1. Root tags (depth 0) have no indent.
+- Chevron: `LucideIcon name="chevron-right"` (collapsed) / `LucideIcon name="chevron-down"` (expanded), `size={14}`, color `text-gray-400`. Click area: `p-1 rounded hover:bg-gray-100`.
+- Leaf nodes (no children): no chevron rendered; indent pad replaced with `w-6` spacer to align name column.
+- Row hover: `hover:bg-gray-50 transition-colors` on the full row `
`.
+- Tag name: `text-sm font-medium text-gray-900`.
+- Item count: `text-sm text-gray-400` — e.g. "12 items" or "0 items".
+- Edit action: `text-xs text-gray-400 hover:text-gray-600` link-style button → navigates to `/admin/tags/$tagId`.
+- Row height: `py-2.5` (10px top/bottom) giving ~36px per row.
+
+### Tree Expand/Collapse
+
+- State: local `useState>` of expanded tag IDs. All parent IDs populated on mount (start expanded per D-03).
+- Toggle: clicking chevron button calls `toggle(tagId)` — no row-click collapse (chevron-only per Claude's discretion).
+- Hidden children: `display: none` equivalent — conditionally render child rows when parent ID is in expanded set.
+
+### Search / Filter Behavior (D-05)
+
+- Non-matching leaf rows: not rendered.
+- Parent rows with matching children: rendered even if parent name does not match.
+- Parent rows whose name matches: rendered with all their children visible (show context).
+- Search input: `w-64 rounded-lg border border-gray-200 px-3 py-2 text-sm` — identical to items list.
+
+---
+
+## Quick-Add Form Spec (D-07)
+
+Position: above the tree view card, below the page header.
+
+```
+[ Name input (flex-1) ] [ Parent picker (w-48) ] [ Add Tag button ]
+```
+
+- Name input: `inputClass` styling, placeholder "Tag name...".
+- Parent picker: native `