diff --git a/.gsd/milestones/M011/M011-ROADMAP.md b/.gsd/milestones/M011/M011-ROADMAP.md
index d03b48a..097c0e7 100644
--- a/.gsd/milestones/M011/M011-ROADMAP.md
+++ b/.gsd/milestones/M011/M011-ROADMAP.md
@@ -6,7 +6,7 @@ Transform Chrysopedia from functionally adequate to engaging and accessible. Add
## Slice Overview
| ID | Slice | Risk | Depends | Done | After this |
|----|-------|------|---------|------|------------|
-| S01 | Interaction Delight & Discovery | medium | — | ⬜ | Cards animate on hover with scale+shadow. Card grids stagger entrance. Featured technique has glow treatment. Random Technique button navigates to a random page. |
+| S01 | Interaction Delight & Discovery | medium | — | ✅ | Cards animate on hover with scale+shadow. Card grids stagger entrance. Featured technique has glow treatment. Random Technique button navigates to a random page. |
| S02 | Topics, Creator Stats & Card Polish | medium | — | ⬜ | Topics page loads collapsed, expands with animation. Creator stats show colored topic pills. Cards limit tags to 4 with +N more. Empty subtopics show Coming soon. |
| S03 | Global Search & Mobile Navigation | medium | — | ⬜ | Compact search bar in nav on all pages. Cmd+K focuses it. Mobile viewport shows hamburger menu. |
| S04 | Accessibility & SEO Fixes | low | — | ⬜ | Every page has single H1, skip-to-content link, AA contrast text, and descriptive browser tab title. |
diff --git a/.gsd/milestones/M011/slices/S01/S01-SUMMARY.md b/.gsd/milestones/M011/slices/S01/S01-SUMMARY.md
new file mode 100644
index 0000000..4a45a23
--- /dev/null
+++ b/.gsd/milestones/M011/slices/S01/S01-SUMMARY.md
@@ -0,0 +1,106 @@
+---
+id: S01
+parent: M011
+milestone: M011
+provides:
+ - Card hover animation pattern (scale+shadow) on all card types
+ - Stagger entrance animation utility class
+ - GET /api/v1/techniques/random endpoint
+ - Random Technique button on homepage
+requires:
+ []
+affects:
+ []
+key_files:
+ - frontend/src/App.css
+ - frontend/src/pages/Home.tsx
+ - frontend/src/pages/TopicsBrowse.tsx
+ - frontend/src/pages/CreatorDetail.tsx
+ - frontend/src/pages/SubTopicPage.tsx
+ - frontend/src/pages/SearchResults.tsx
+ - backend/routers/techniques.py
+ - frontend/src/api/public-client.ts
+key_decisions:
+ - CSS stagger pattern uses --stagger-index custom property with calc() delay rather than nth-child selectors — works with dynamic lists
+ - Dedicated /random endpoint returning just {slug} rather than reusing sort=random on list endpoint — cleaner API surface
+ - /random route placed before /{slug} to avoid FastAPI slug capture
+patterns_established:
+ - card-stagger utility class: add className='card-stagger' and style={{ '--stagger-index': i }} to any .map() loop for entrance animations
+ - Featured section visual treatment: gradient border-image + double box-shadow glow
+observability_surfaces:
+ - none
+drill_down_paths:
+ - .gsd/milestones/M011/slices/S01/tasks/T01-SUMMARY.md
+ - .gsd/milestones/M011/slices/S01/tasks/T02-SUMMARY.md
+duration: ""
+verification_result: passed
+completed_at: 2026-03-31T08:25:58.040Z
+blocker_discovered: false
+---
+
+# S01: Interaction Delight & Discovery
+
+**Added card hover animations (scale+shadow) on all 6 card types, staggered entrance animations across 5 page components, gradient-border glow on featured technique, and a Random Technique button with backend endpoint.**
+
+## What Happened
+
+Two tasks delivered CSS interaction polish and a random discovery feature.
+
+T01 added `transform: scale(1.02)` hover transitions with `will-change: transform` to all 6 card types (recent-card, creator-technique-card, subtopic-technique-card, search-result-card, topic-card, nav-card). Created a `@keyframes cardEnter` animation (opacity 0→1, translateY 12px→0, 300ms ease-out) with a `.card-stagger` utility class driven by a `--stagger-index` CSS custom property. Applied stagger indices via JSX style props across Home.tsx, TopicsBrowse.tsx, CreatorDetail.tsx, SubTopicPage.tsx, and SearchResults.tsx. The featured technique section got a gradient `border-image` and double box-shadow glow treatment.
+
+T02 added a `GET /api/v1/techniques/random` backend endpoint (placed before the `/{slug}` route to avoid capture) that returns `{slug}` via `ORDER BY random() LIMIT 1`, with 404 when no techniques exist. Frontend gets `fetchRandomTechnique()` in the API client and a 🎲 Random Technique button on the homepage with loading and error states.
+
+Both tasks verified with TypeScript (`tsc --noEmit`) and Vite production build (50 modules). All grep checks confirm features landed in expected files.
+
+## Verification
+
+All slice-level checks pass:
+- `npx tsc --noEmit` — exit 0, no type errors
+- `npm run build` — exit 0, 50 modules, 806ms build
+- `grep cardEnter App.css` — found
+- `grep card-stagger App.css` — found
+- `grep fetchRandomTechnique public-client.ts` — found
+- `grep /random techniques.py` — found
+- `grep Random Home.tsx` — found
+- `grep stagger-index` across all 5 page components — found in each (Home:3, TopicsBrowse:1, CreatorDetail:1, SubTopicPage:1, SearchResults:1)
+
+## Requirements Advanced
+
+- R016 — All 6 card types have scale(1.02) hover with smooth 200ms transition. 5 page components use staggered cardEnter animation.
+- R017 — Featured technique section has gradient border-image and double box-shadow glow, visually distinct from regular cards.
+- R018 — Random Technique button on homepage calls GET /random endpoint and navigates to result. Loading and error states implemented.
+
+## Requirements Validated
+
+None.
+
+## New Requirements Surfaced
+
+None.
+
+## Requirements Invalidated or Re-scoped
+
+None.
+
+## Deviations
+
+SearchResultCard needed a staggerIndex prop threaded through (not anticipated in plan). topic-card had no existing :hover rule — one was added. border-image removes border-radius on the featured card (CSS limitation) but the glow box-shadow still provides the visual treatment.
+
+## Known Limitations
+
+border-image CSS property strips border-radius on the featured technique card. The glow effect (box-shadow) still provides visual distinction but the card corners are square rather than rounded.
+
+## Follow-ups
+
+None.
+
+## Files Created/Modified
+
+- `frontend/src/App.css` — Added card hover scale(1.02) transitions, @keyframes cardEnter, .card-stagger utility, .home-featured glow treatment, .btn--random and .home-random styles
+- `frontend/src/pages/Home.tsx` — Added stagger indices to nav-cards and recent cards, Random Technique button with loading/error states
+- `frontend/src/pages/TopicsBrowse.tsx` — Added card-stagger class and stagger-index to topic cards
+- `frontend/src/pages/CreatorDetail.tsx` — Added card-stagger class and stagger-index to creator technique cards
+- `frontend/src/pages/SubTopicPage.tsx` — Added card-stagger class and stagger-index to subtopic technique cards
+- `frontend/src/pages/SearchResults.tsx` — Added card-stagger class and stagger-index to search result cards, threaded staggerIndex prop
+- `backend/routers/techniques.py` — Added GET /random endpoint before /{slug} route
+- `frontend/src/api/public-client.ts` — Added fetchRandomTechnique() API client function
diff --git a/.gsd/milestones/M011/slices/S01/S01-UAT.md b/.gsd/milestones/M011/slices/S01/S01-UAT.md
new file mode 100644
index 0000000..51acf4c
--- /dev/null
+++ b/.gsd/milestones/M011/slices/S01/S01-UAT.md
@@ -0,0 +1,77 @@
+# S01: Interaction Delight & Discovery — UAT
+
+**Milestone:** M011
+**Written:** 2026-03-31T08:25:58.041Z
+
+# S01 UAT: Interaction Delight & Discovery
+
+## Preconditions
+- Chrysopedia running at http://ub01:8096
+- At least 1 technique page exists in the database
+- Modern browser (Chrome/Firefox/Safari)
+
+## Test Cases
+
+### TC-01: Card Hover Animation — Homepage Recent Cards
+1. Navigate to http://ub01:8096
+2. Hover over any card in the "Recently Added" section
+3. **Expected:** Card smoothly scales up (~2%) with enhanced shadow over ~200ms
+4. Move mouse away from card
+5. **Expected:** Card smoothly returns to original size
+
+### TC-02: Card Hover Animation — All Card Types
+1. Navigate to Topics page → hover a topic card
+2. Navigate to any topic → hover a subtopic technique card
+3. Navigate to Creators page → click a creator → hover a creator technique card
+4. Use search → hover a search result card
+5. On homepage → hover a nav card (Topics/Creators)
+6. **Expected:** All 5 card types exhibit the same scale+shadow hover effect
+
+### TC-03: Staggered Entrance Animation — Homepage
+1. Hard refresh http://ub01:8096 (Ctrl+Shift+R)
+2. Observe the nav cards and recently added cards
+3. **Expected:** Cards fade in and slide up sequentially (not all at once). Each card appears ~60ms after the previous one.
+
+### TC-04: Staggered Entrance — Other Pages
+1. Navigate to Topics page — cards should stagger in
+2. Navigate to a Creator detail page — technique cards should stagger in
+3. Navigate to a SubTopic page — technique cards should stagger in
+4. Perform a search — result cards should stagger in
+5. **Expected:** All pages show sequential card entrance, not simultaneous
+
+### TC-05: Featured Technique Glow
+1. Navigate to homepage
+2. Scroll to the "Featured Technique" section
+3. **Expected:** The featured technique card has a visible gradient border and soft glow (cyan-tinted box shadow) that distinguishes it from regular cards
+4. **Note:** Corners may be square due to border-image CSS limitation — this is known
+
+### TC-06: Random Technique Button — Happy Path
+1. Navigate to homepage
+2. Locate the 🎲 "Random Technique" button (between nav cards and featured section)
+3. Click the button
+4. **Expected:** Button shows brief loading state, then navigates to a technique page
+5. Use browser back, click the button again
+6. **Expected:** May navigate to a different technique (randomized server-side)
+
+### TC-07: Random Technique Button — Loading State
+1. On homepage, open browser DevTools Network tab
+2. Throttle network to "Slow 3G"
+3. Click the Random Technique button
+4. **Expected:** Button shows a loading indicator while the request is in flight
+5. Remove throttle
+
+### TC-08: Random Technique API — Direct
+1. Open http://ub01:8096/api/v1/techniques/random in browser or curl
+2. **Expected:** JSON response `{"slug": "some-technique-slug"}` with 200 status
+3. Refresh multiple times
+4. **Expected:** Different slugs returned (assuming multiple techniques exist)
+
+### Edge Cases
+
+### TC-09: Card Stagger with Few Items
+1. If a creator has only 1 technique, navigate to their detail page
+2. **Expected:** Single card still animates in (no delay needed for index 0)
+
+### TC-10: Featured Technique Glow in Light/Contrast
+1. On homepage, inspect the featured technique section
+2. **Expected:** Glow is subtle — visible but not overpowering (cyan tint, not neon)
diff --git a/.gsd/milestones/M011/slices/S01/tasks/T02-VERIFY.json b/.gsd/milestones/M011/slices/S01/tasks/T02-VERIFY.json
new file mode 100644
index 0000000..93ecc2e
--- /dev/null
+++ b/.gsd/milestones/M011/slices/S01/tasks/T02-VERIFY.json
@@ -0,0 +1,42 @@
+{
+ "schemaVersion": 1,
+ "taskId": "T02",
+ "unitId": "M011/S01/T02",
+ "timestamp": 1774945478573,
+ "passed": false,
+ "discoverySource": "task-plan",
+ "checks": [
+ {
+ "command": "cd frontend",
+ "exitCode": 0,
+ "durationMs": 5,
+ "verdict": "pass"
+ },
+ {
+ "command": "npx tsc --noEmit",
+ "exitCode": 1,
+ "durationMs": 720,
+ "verdict": "fail"
+ },
+ {
+ "command": "npm run build",
+ "exitCode": 254,
+ "durationMs": 96,
+ "verdict": "fail"
+ },
+ {
+ "command": "grep -q 'fetchRandomTechnique' src/api/public-client.ts",
+ "exitCode": 2,
+ "durationMs": 7,
+ "verdict": "fail"
+ },
+ {
+ "command": "grep -q '/random' ../backend/routers/techniques.py",
+ "exitCode": 2,
+ "durationMs": 6,
+ "verdict": "fail"
+ }
+ ],
+ "retryAttempt": 1,
+ "maxRetries": 2
+}
diff --git a/.gsd/milestones/M011/slices/S02/S02-PLAN.md b/.gsd/milestones/M011/slices/S02/S02-PLAN.md
index f05bc9b..070c70c 100644
--- a/.gsd/milestones/M011/slices/S02/S02-PLAN.md
+++ b/.gsd/milestones/M011/slices/S02/S02-PLAN.md
@@ -1,6 +1,125 @@
# S02: Topics, Creator Stats & Card Polish
-**Goal:** Improve information density and visual clarity on Topics and Creator pages
+**Goal:** Topics page loads collapsed with smooth expand/collapse animation. Creator stats show colored topic pills. Cards limit tags to 4 with +N overflow. Empty subtopics show "Coming soon" badge.
**Demo:** After this: Topics page loads collapsed, expands with animation. Creator stats show colored topic pills. Cards limit tags to 4 with +N more. Empty subtopics show Coming soon.
## Tasks
+- [x] **T01: Topics page loads with all categories collapsed; expand/collapse uses smooth CSS grid-template-rows animation instead of conditional render** — Change TopicsBrowse.tsx to start with all categories collapsed and add smooth CSS grid-template-rows animation for expand/collapse transitions.
+
+## Steps
+
+1. In `frontend/src/pages/TopicsBrowse.tsx`, change the `useEffect` that calls `setExpanded(new Set(data.map((c) => c.name)))` to `setExpanded(new Set())` so all categories start collapsed.
+
+2. Refactor the subtopics rendering from conditional `{isExpanded && (
...)}` to always-rendered with a CSS grid animation wrapper. Use the `display: grid; grid-template-rows: 0fr / 1fr` technique:
+ - Wrap the subtopics in a `
` that has `display: grid; grid-template-rows: 0fr; transition: grid-template-rows 300ms ease;`
+ - When `data-expanded="true"`, set `grid-template-rows: 1fr`
+ - The inner `
` gets `overflow: hidden; min-height: 0;`
+ - Always render both wrapper and inner div (remove the conditional render)
+
+3. Add the CSS in `frontend/src/App.css` near the existing `.topic-subtopics` rule (~line 2318):
+ - `.topic-subtopics-wrapper { display: grid; grid-template-rows: 0fr; transition: grid-template-rows 300ms ease; }`
+ - `.topic-subtopics-wrapper[data-expanded="true"] { grid-template-rows: 1fr; }`
+ - `.topic-subtopics { overflow: hidden; min-height: 0; }` (modify existing rule)
+
+4. Verify: `cd frontend && npx tsc --noEmit && npm run build`
+ - Estimate: 30m
+ - Files: frontend/src/pages/TopicsBrowse.tsx, frontend/src/App.css
+ - Verify: cd frontend && npx tsc --noEmit && npm run build
+- [ ] **T02: Creator stats topic-colored pill badges** — Replace the run-on text stats on CreatorDetail page with colored pill badges using existing badge CSS classes.
+
+## Steps
+
+1. In `frontend/src/pages/CreatorDetail.tsx`, add import: `import { catSlug } from "../utils/catSlug";`
+
+2. Find the stats section (~line 108-126) that renders `` with dot separators. Replace the entire `.map(([cat, count], i) => ...)` block with pills:
+ ```tsx
+ .map(([cat, count]) => (
+
+ {cat}: {count}
+
+ ))
+ ```
+ Remove the dot separator spans (`queue-card__separator`) between them.
+
+3. Wrap the topic stat pills in a flex container: `` and add CSS in `frontend/src/App.css`:
+ ```css
+ .creator-detail__topic-pills {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.375rem;
+ align-items: center;
+ }
+ ```
+
+4. Remove the old `.creator-detail__topic-stat` class from App.css if it exists (or leave it — no harm).
+
+5. Verify: `cd frontend && npx tsc --noEmit && npm run build`
+ - Estimate: 20m
+ - Files: frontend/src/pages/CreatorDetail.tsx, frontend/src/App.css
+ - Verify: cd frontend && npx tsc --noEmit && npm run build
+- [ ] **T03: TagList component, tag overflow limit, and empty subtopic handling** — Create a shared TagList component for tag overflow (R027), apply it across all 5 tag-rendering sites, and add empty subtopic handling in TopicsBrowse (R028).
+
+## Steps
+
+1. Create `frontend/src/components/TagList.tsx`:
+ ```tsx
+ interface TagListProps {
+ tags: string[];
+ max?: number;
+ }
+ export default function TagList({ tags, max = 4 }: TagListProps) {
+ const visible = tags.slice(0, max);
+ const overflow = tags.length - max;
+ return (
+ <>
+ {visible.map(tag => {tag})}
+ {overflow > 0 && +{overflow} more}
+ >
+ );
+ }
+ ```
+
+2. Add `.pill--overflow` CSS in `frontend/src/App.css`:
+ ```css
+ .pill--overflow {
+ background: var(--color-surface-2);
+ color: var(--color-text-secondary);
+ font-style: italic;
+ }
+ ```
+
+3. Replace tag rendering in all 5 sites with ``:
+ - `frontend/src/pages/Home.tsx` line ~201 (featured technique tags) — replace `featured.topic_tags.map(...)` with ``
+ - `frontend/src/pages/Home.tsx` line ~247 (recent technique card tags) — replace `t.topic_tags.map(...)` with ``
+ - `frontend/src/pages/SearchResults.tsx` line ~145 — replace `item.topic_tags.map(...)` with ``
+ - `frontend/src/pages/SubTopicPage.tsx` line ~149 — replace `t.topic_tags.map(...)` with ``
+ - `frontend/src/pages/CreatorDetail.tsx` line ~157 — replace `t.topic_tags.map(...)` with ``
+ Each site: add `import TagList from "../components/TagList";` and replace the `.map(tag => {tag})` with ``.
+
+4. In `frontend/src/pages/TopicsBrowse.tsx`, update the subtopic rendering inside the `.topic-subtopics` div. For subtopics with `st.technique_count === 0`:
+ - Render as a `` (not ``) with class `topic-subtopic topic-subtopic--empty` instead of a clickable link
+ - Add a small "Coming soon" badge: `Coming soon`
+ - Keep the subtopic name visible so users know the topic exists
+ Use a conditional: `st.technique_count === 0 ? ... : ...`
+
+5. Add CSS for empty subtopic in `frontend/src/App.css`:
+ ```css
+ .topic-subtopic--empty {
+ opacity: 0.5;
+ cursor: default;
+ }
+ .topic-subtopic--empty:hover {
+ background: transparent;
+ }
+ .pill--coming-soon {
+ font-size: 0.65rem;
+ background: var(--color-surface-2);
+ color: var(--color-text-secondary);
+ font-style: italic;
+ }
+ ```
+
+6. Verify: `cd frontend && npx tsc --noEmit && npm run build`
+ - Estimate: 40m
+ - Files: frontend/src/components/TagList.tsx, frontend/src/pages/Home.tsx, frontend/src/pages/SearchResults.tsx, frontend/src/pages/SubTopicPage.tsx, frontend/src/pages/CreatorDetail.tsx, frontend/src/pages/TopicsBrowse.tsx, frontend/src/App.css
+ - Verify: cd frontend && npx tsc --noEmit && npm run build
diff --git a/.gsd/milestones/M011/slices/S02/S02-RESEARCH.md b/.gsd/milestones/M011/slices/S02/S02-RESEARCH.md
new file mode 100644
index 0000000..ad5997c
--- /dev/null
+++ b/.gsd/milestones/M011/slices/S02/S02-RESEARCH.md
@@ -0,0 +1,96 @@
+# S02 Research: Topics, Creator Stats & Card Polish
+
+## Summary
+
+Straightforward frontend-only changes across 5–6 files. All patterns (CSS color system, catSlug utility, badge classes, expand/collapse state) already exist. No new dependencies, no backend changes, no unfamiliar technology.
+
+## Targeted Requirements
+
+| Req | Description | Status |
+|-----|-------------|--------|
+| R019 | Topics page default-collapsed with expand animation | Active — this slice |
+| R026 | Creator stats topic-colored pills | Active — this slice |
+| R027 | Tag overflow limit (4 + "+N more") | Active — this slice |
+| R028 | Empty subtopic handling | Active — this slice |
+
+## Implementation Landscape
+
+### 1. Topics Default-Collapsed (R019)
+
+**File:** `frontend/src/pages/TopicsBrowse.tsx`
+
+- **Current:** Line ~42: `setExpanded(new Set(data.map((c) => c.name)))` — starts all-expanded.
+- **Change:** `setExpanded(new Set())` — starts all-collapsed.
+- **Animation:** The subtopics container (`div.topic-subtopics`) currently renders conditionally (`{isExpanded && ...}`). For smooth expand/collapse, replace the conditional render with CSS `max-height` + `overflow: hidden` + `transition`. Use a wrapper with `max-height: 0` when collapsed and `max-height: px` when expanded, or use CSS `grid-template-rows: 0fr / 1fr` technique (better, no fixed height needed).
+- **CSS file:** `frontend/src/App.css` — `.topic-subtopics` starts at line 2318. Add transition properties.
+
+**Recommendation:** Use the CSS `display: grid; grid-template-rows: 0fr → 1fr` pattern for the smoothest transition without needing to know content height. Wrap subtopics in a grid container that transitions `grid-template-rows`.
+
+### 2. Creator Stats Colored Pills (R026)
+
+**File:** `frontend/src/pages/CreatorDetail.tsx`
+
+- **Current:** Lines ~97–110: Stats render as `Cat: N` with dot separators.
+- **Change:** Replace with `{cat}: {count}` using `catSlug()` from `../utils/catSlug`.
+- **Existing CSS:** `.badge--cat-sound-design`, `.badge--cat-mixing`, etc. already defined in App.css with bg/text color pairs for all 7 categories.
+- **Import needed:** `import { catSlug } from "../utils/catSlug";` (not currently imported in CreatorDetail.tsx).
+- **Layout:** Replace the run-on text with a flex-wrap container holding pill badges.
+
+### 3. Tag Overflow Limit (R027)
+
+**Files affected (5 tag-rendering sites):**
+
+| File | Line | Context |
+|------|------|---------|
+| `frontend/src/pages/Home.tsx` | ~201 | Featured technique tags |
+| `frontend/src/pages/Home.tsx` | ~247 | Recent technique card tags |
+| `frontend/src/pages/SearchResults.tsx` | ~143 | Search result card tags |
+| `frontend/src/pages/SubTopicPage.tsx` | ~147 | Sub-topic technique card tags |
+| `frontend/src/pages/CreatorDetail.tsx` | ~108 | Creator technique card tags |
+
+**Pattern:** All 5 sites use the same idiom:
+```tsx
+{t.topic_tags.map(tag => {tag})}
+```
+
+**Change:** Extract a shared helper or inline the pattern:
+```tsx
+{tags.slice(0, 4).map(tag => {tag})}
+{tags.length > 4 && +{tags.length - 4} more}
+```
+
+**Recommendation:** Create a small `TagList` component in `frontend/src/components/TagList.tsx` to DRY up the 5 sites. Props: `tags: string[]`, `max?: number` (default 4). The component renders pills with overflow. This avoids repeating the slice/overflow logic 5 times.
+
+### 4. Empty Subtopic Handling (R028)
+
+**File:** `frontend/src/pages/TopicsBrowse.tsx`
+
+- **Current:** All subtopics render as clickable links regardless of `technique_count`.
+- **Change:** When `st.technique_count === 0`, render as a non-clickable element with a "Coming soon" badge instead of a link. Keep it visible (don't hide) so users know the topic exists.
+- **CSS:** Add a `.topic-subtopic--empty` modifier class with muted styling and a small "Coming soon" badge.
+
+## Key Observations
+
+1. **CSS color system is complete** — all 7 category color pairs exist as CSS custom properties and badge modifier classes. No new colors needed for creator stats pills.
+2. **`catSlug()` utility exists** — converts category names to CSS-safe slugs (lowercase, spaces→hyphens). Already used in TopicsBrowse.tsx.
+3. **S01 added `card-stagger` animation** — `@keyframes cardEnter` and `.card-stagger` class with `--stagger-index` CSS variable. Already applied to topic cards and creator technique cards.
+4. **No existing expand/collapse animation** — subtopics use conditional rendering (`{isExpanded && ...}`), not CSS transitions. Needs refactoring for smooth animation.
+5. **`grid-template-rows` transition** is well-supported (Chrome 57+, Firefox 66+, Safari 16+) and doesn't require knowing content height.
+
+## Natural Task Seams
+
+1. **Topics collapse + animation (R019)** — Self-contained in TopicsBrowse.tsx + App.css. Change default state, add CSS grid transition.
+2. **Creator stats pills (R026)** — Self-contained in CreatorDetail.tsx + App.css. Import catSlug, replace text with badges.
+3. **Tag overflow + empty subtopics (R027, R028)** — Create TagList component, update 5 files for tag overflow, update TopicsBrowse.tsx for empty subtopics. These can be one task since TagList is the shared piece.
+
+**Risk ordering:** Topics collapse/animation is slightly riskier (CSS transition refactor), so do it first. The others are mechanical.
+
+## Verification
+
+- `cd frontend && npx tsc --noEmit` — TypeScript compiles clean
+- `cd frontend && npm run build` — Vite production build succeeds
+- Browser verification at http://ub01:8096:
+ - Topics page loads collapsed, click expands with animation
+ - Creator detail shows colored pills for topic stats
+ - Cards with >4 tags show exactly 4 + "+N more"
+ - Empty subtopics show "Coming soon" badge
diff --git a/.gsd/milestones/M011/slices/S02/tasks/T01-PLAN.md b/.gsd/milestones/M011/slices/S02/tasks/T01-PLAN.md
new file mode 100644
index 0000000..922c5b9
--- /dev/null
+++ b/.gsd/milestones/M011/slices/S02/tasks/T01-PLAN.md
@@ -0,0 +1,40 @@
+---
+estimated_steps: 13
+estimated_files: 2
+skills_used: []
+---
+
+# T01: Topics page default-collapsed with expand/collapse animation
+
+Change TopicsBrowse.tsx to start with all categories collapsed and add smooth CSS grid-template-rows animation for expand/collapse transitions.
+
+## Steps
+
+1. In `frontend/src/pages/TopicsBrowse.tsx`, change the `useEffect` that calls `setExpanded(new Set(data.map((c) => c.name)))` to `setExpanded(new Set())` so all categories start collapsed.
+
+2. Refactor the subtopics rendering from conditional `{isExpanded && (
...)}` to always-rendered with a CSS grid animation wrapper. Use the `display: grid; grid-template-rows: 0fr / 1fr` technique:
+ - Wrap the subtopics in a `
` that has `display: grid; grid-template-rows: 0fr; transition: grid-template-rows 300ms ease;`
+ - When `data-expanded="true"`, set `grid-template-rows: 1fr`
+ - The inner `
` gets `overflow: hidden; min-height: 0;`
+ - Always render both wrapper and inner div (remove the conditional render)
+
+3. Add the CSS in `frontend/src/App.css` near the existing `.topic-subtopics` rule (~line 2318):
+ - `.topic-subtopics-wrapper { display: grid; grid-template-rows: 0fr; transition: grid-template-rows 300ms ease; }`
+ - `.topic-subtopics-wrapper[data-expanded="true"] { grid-template-rows: 1fr; }`
+ - `.topic-subtopics { overflow: hidden; min-height: 0; }` (modify existing rule)
+
+4. Verify: `cd frontend && npx tsc --noEmit && npm run build`
+
+## Inputs
+
+- `frontend/src/pages/TopicsBrowse.tsx`
+- `frontend/src/App.css`
+
+## Expected Output
+
+- `frontend/src/pages/TopicsBrowse.tsx`
+- `frontend/src/App.css`
+
+## Verification
+
+cd frontend && npx tsc --noEmit && npm run build
diff --git a/.gsd/milestones/M011/slices/S02/tasks/T01-SUMMARY.md b/.gsd/milestones/M011/slices/S02/tasks/T01-SUMMARY.md
new file mode 100644
index 0000000..f07d149
--- /dev/null
+++ b/.gsd/milestones/M011/slices/S02/tasks/T01-SUMMARY.md
@@ -0,0 +1,76 @@
+---
+id: T01
+parent: S02
+milestone: M011
+provides: []
+requires: []
+affects: []
+key_files: ["frontend/src/pages/TopicsBrowse.tsx", "frontend/src/App.css"]
+key_decisions: ["Used CSS grid-template-rows 0fr/1fr technique for smooth height animation without JS measurement"]
+patterns_established: []
+drill_down_paths: []
+observability_surfaces: []
+duration: ""
+verification_result: "TypeScript type check (npx tsc --noEmit) and production build (npm run build) both pass cleanly with exit code 0."
+completed_at: 2026-03-31T08:30:49.223Z
+blocker_discovered: false
+---
+
+# T01: Topics page loads with all categories collapsed; expand/collapse uses smooth CSS grid-template-rows animation instead of conditional render
+
+> Topics page loads with all categories collapsed; expand/collapse uses smooth CSS grid-template-rows animation instead of conditional render
+
+## What Happened
+---
+id: T01
+parent: S02
+milestone: M011
+key_files:
+ - frontend/src/pages/TopicsBrowse.tsx
+ - frontend/src/App.css
+key_decisions:
+ - Used CSS grid-template-rows 0fr/1fr technique for smooth height animation without JS measurement
+duration: ""
+verification_result: passed
+completed_at: 2026-03-31T08:30:49.224Z
+blocker_discovered: false
+---
+
+# T01: Topics page loads with all categories collapsed; expand/collapse uses smooth CSS grid-template-rows animation instead of conditional render
+
+**Topics page loads with all categories collapsed; expand/collapse uses smooth CSS grid-template-rows animation instead of conditional render**
+
+## What Happened
+
+Changed the useEffect in TopicsBrowse.tsx to initialize the expanded set as empty instead of pre-filling all category names. Replaced the conditional {isExpanded && (...)} render with an always-rendered wrapper div using the grid-template-rows: 0fr/1fr animation technique. Added CSS rules for the wrapper, expanded state, and updated the inner div with overflow: hidden and min-height: 0 for smooth clipping.
+
+## Verification
+
+TypeScript type check (npx tsc --noEmit) and production build (npm run build) both pass cleanly with exit code 0.
+
+## Verification Evidence
+
+| # | Command | Exit Code | Verdict | Duration |
+|---|---------|-----------|---------|----------|
+| 1 | `cd frontend && npx tsc --noEmit && npm run build` | 0 | ✅ pass | 3700ms |
+
+
+## Deviations
+
+None.
+
+## Known Issues
+
+None.
+
+## Files Created/Modified
+
+- `frontend/src/pages/TopicsBrowse.tsx`
+- `frontend/src/App.css`
+
+
+## Deviations
+None.
+
+## Known Issues
+None.
diff --git a/.gsd/milestones/M011/slices/S02/tasks/T02-PLAN.md b/.gsd/milestones/M011/slices/S02/tasks/T02-PLAN.md
new file mode 100644
index 0000000..ffb5410
--- /dev/null
+++ b/.gsd/milestones/M011/slices/S02/tasks/T02-PLAN.md
@@ -0,0 +1,52 @@
+---
+estimated_steps: 23
+estimated_files: 2
+skills_used: []
+---
+
+# T02: Creator stats topic-colored pill badges
+
+Replace the run-on text stats on CreatorDetail page with colored pill badges using existing badge CSS classes.
+
+## Steps
+
+1. In `frontend/src/pages/CreatorDetail.tsx`, add import: `import { catSlug } from "../utils/catSlug";`
+
+2. Find the stats section (~line 108-126) that renders `` with dot separators. Replace the entire `.map(([cat, count], i) => ...)` block with pills:
+ ```tsx
+ .map(([cat, count]) => (
+
+ {cat}: {count}
+
+ ))
+ ```
+ Remove the dot separator spans (`queue-card__separator`) between them.
+
+3. Wrap the topic stat pills in a flex container: `` and add CSS in `frontend/src/App.css`:
+ ```css
+ .creator-detail__topic-pills {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.375rem;
+ align-items: center;
+ }
+ ```
+
+4. Remove the old `.creator-detail__topic-stat` class from App.css if it exists (or leave it — no harm).
+
+5. Verify: `cd frontend && npx tsc --noEmit && npm run build`
+
+## Inputs
+
+- `frontend/src/pages/CreatorDetail.tsx`
+- `frontend/src/App.css`
+- `frontend/src/utils/catSlug.ts`
+
+## Expected Output
+
+- `frontend/src/pages/CreatorDetail.tsx`
+- `frontend/src/App.css`
+
+## Verification
+
+cd frontend && npx tsc --noEmit && npm run build
diff --git a/.gsd/milestones/M011/slices/S02/tasks/T03-PLAN.md b/.gsd/milestones/M011/slices/S02/tasks/T03-PLAN.md
new file mode 100644
index 0000000..bae0e6a
--- /dev/null
+++ b/.gsd/milestones/M011/slices/S02/tasks/T03-PLAN.md
@@ -0,0 +1,94 @@
+---
+estimated_steps: 56
+estimated_files: 7
+skills_used: []
+---
+
+# T03: TagList component, tag overflow limit, and empty subtopic handling
+
+Create a shared TagList component for tag overflow (R027), apply it across all 5 tag-rendering sites, and add empty subtopic handling in TopicsBrowse (R028).
+
+## Steps
+
+1. Create `frontend/src/components/TagList.tsx`:
+ ```tsx
+ interface TagListProps {
+ tags: string[];
+ max?: number;
+ }
+ export default function TagList({ tags, max = 4 }: TagListProps) {
+ const visible = tags.slice(0, max);
+ const overflow = tags.length - max;
+ return (
+ <>
+ {visible.map(tag => {tag})}
+ {overflow > 0 && +{overflow} more}
+ >
+ );
+ }
+ ```
+
+2. Add `.pill--overflow` CSS in `frontend/src/App.css`:
+ ```css
+ .pill--overflow {
+ background: var(--color-surface-2);
+ color: var(--color-text-secondary);
+ font-style: italic;
+ }
+ ```
+
+3. Replace tag rendering in all 5 sites with ``:
+ - `frontend/src/pages/Home.tsx` line ~201 (featured technique tags) — replace `featured.topic_tags.map(...)` with ``
+ - `frontend/src/pages/Home.tsx` line ~247 (recent technique card tags) — replace `t.topic_tags.map(...)` with ``
+ - `frontend/src/pages/SearchResults.tsx` line ~145 — replace `item.topic_tags.map(...)` with ``
+ - `frontend/src/pages/SubTopicPage.tsx` line ~149 — replace `t.topic_tags.map(...)` with ``
+ - `frontend/src/pages/CreatorDetail.tsx` line ~157 — replace `t.topic_tags.map(...)` with ``
+ Each site: add `import TagList from "../components/TagList";` and replace the `.map(tag => {tag})` with ``.
+
+4. In `frontend/src/pages/TopicsBrowse.tsx`, update the subtopic rendering inside the `.topic-subtopics` div. For subtopics with `st.technique_count === 0`:
+ - Render as a `` (not ``) with class `topic-subtopic topic-subtopic--empty` instead of a clickable link
+ - Add a small "Coming soon" badge: `Coming soon`
+ - Keep the subtopic name visible so users know the topic exists
+ Use a conditional: `st.technique_count === 0 ? ... : ...`
+
+5. Add CSS for empty subtopic in `frontend/src/App.css`:
+ ```css
+ .topic-subtopic--empty {
+ opacity: 0.5;
+ cursor: default;
+ }
+ .topic-subtopic--empty:hover {
+ background: transparent;
+ }
+ .pill--coming-soon {
+ font-size: 0.65rem;
+ background: var(--color-surface-2);
+ color: var(--color-text-secondary);
+ font-style: italic;
+ }
+ ```
+
+6. Verify: `cd frontend && npx tsc --noEmit && npm run build`
+
+## Inputs
+
+- `frontend/src/pages/Home.tsx`
+- `frontend/src/pages/SearchResults.tsx`
+- `frontend/src/pages/SubTopicPage.tsx`
+- `frontend/src/pages/CreatorDetail.tsx`
+- `frontend/src/pages/TopicsBrowse.tsx`
+- `frontend/src/App.css`
+
+## Expected Output
+
+- `frontend/src/components/TagList.tsx`
+- `frontend/src/pages/Home.tsx`
+- `frontend/src/pages/SearchResults.tsx`
+- `frontend/src/pages/SubTopicPage.tsx`
+- `frontend/src/pages/CreatorDetail.tsx`
+- `frontend/src/pages/TopicsBrowse.tsx`
+- `frontend/src/App.css`
+
+## Verification
+
+cd frontend && npx tsc --noEmit && npm run build
diff --git a/frontend/src/App.css b/frontend/src/App.css
index d0f58f5..4aa9948 100644
--- a/frontend/src/App.css
+++ b/frontend/src/App.css
@@ -2315,7 +2315,19 @@ a.app-footer__repo:hover {
/* ── Sub-topics inside card ───────────────────────────────────────────────── */
+.topic-subtopics-wrapper {
+ display: grid;
+ grid-template-rows: 0fr;
+ transition: grid-template-rows 300ms ease;
+}
+
+.topic-subtopics-wrapper[data-expanded="true"] {
+ grid-template-rows: 1fr;
+}
+
.topic-subtopics {
+ overflow: hidden;
+ min-height: 0;
border-top: 1px solid var(--color-border);
}
diff --git a/frontend/src/pages/TopicsBrowse.tsx b/frontend/src/pages/TopicsBrowse.tsx
index 044b918..97a5ca1 100644
--- a/frontend/src/pages/TopicsBrowse.tsx
+++ b/frontend/src/pages/TopicsBrowse.tsx
@@ -34,8 +34,8 @@ export default function TopicsBrowse() {
const data = await fetchTopics();
if (!cancelled) {
setCategories(data);
- // All expanded by default
- setExpanded(new Set(data.map((c) => c.name)));
+ // Start collapsed
+ setExpanded(new Set());
}
} catch (err) {
if (!cancelled) {
@@ -151,7 +151,7 @@ export default function TopicsBrowse() {