diff --git a/.planning/phases/20-fab-full-screen-catalog-search/20-RESEARCH.md b/.planning/phases/20-fab-full-screen-catalog-search/20-RESEARCH.md
new file mode 100644
index 0000000..f912d4c
--- /dev/null
+++ b/.planning/phases/20-fab-full-screen-catalog-search/20-RESEARCH.md
@@ -0,0 +1,403 @@
+# Phase 20: FAB & Full-Screen Catalog Search - Research
+
+**Researched:** 2026-04-06
+**Domain:** React UI components (FAB menu, full-screen overlay, tag filtering), Hono API endpoint
+**Confidence:** HIGH
+
+## Summary
+
+Phase 20 is a client-heavy UI phase with a small server addition (GET /api/tags). The work involves: (1) upgrading the existing single-action FAB in `__root.tsx` to a mini menu with multiple actions, (2) building a full-screen catalog search overlay managed by UIStore, and (3) adding tag chip filtering connected to the existing `GET /api/global-items?q=...&tags=...` endpoint.
+
+All patterns needed already exist in the codebase. The FAB lives in `__root.tsx` (lines 257-278), the overlay pattern is established in `CreateThreadModal` (fixed inset-0 z-50), the search with debounce exists in `global-items/index.tsx`, the card pattern is in `GlobalItemCard.tsx`, and Framer Motion is already a dependency for animations. The only net-new server work is a lightweight tags endpoint and registering the existing `global-items` route (currently unregistered in index.ts).
+
+**Primary recommendation:** Extend existing patterns -- UIStore state slices, Framer Motion AnimatePresence for FAB menu animation, reuse debounce/search/card patterns from global-items page. No new libraries needed.
+
+
+## User Constraints (from CONTEXT.md)
+
+### Locked Decisions
+- D-01: FAB globally visible on all pages (not just collection view). Position: fixed bottom-6 right-6
+- D-02: Tapping FAB opens mini menu: 2-3 labeled icon buttons fan out vertically above FAB. Backdrop dims slightly. Tapping backdrop or FAB again closes menu
+- D-03: Menu options: "Add to Collection" (package icon) + "Start New Thread" (search icon). On setups page only, third option "New Setup"
+- D-04: Both "Add to Collection" and "Start New Thread" open same full-screen catalog search overlay, with mode parameter ("collection" or "thread") stored in UIStore
+- D-05: "New Setup" triggers existing setup creation flow (no catalog search)
+- D-06: FAB should not appear on login page or public profile/setup pages (only authenticated routes)
+- D-07: Full-screen overlay (fixed inset-0 z-50) managed by UIStore state (catalogSearchOpen, catalogSearchMode), not a route change
+- D-08: Layout: back arrow (top-left, closes overlay) + large search input + context indicator text
+- D-09: Below search bar: horizontal scrollable row of tag chips for quick filtering
+- D-10: Results area: grid of compact cards showing brand + model, weight, price, owner count, and "Add" button
+- D-11: Search queries existing GET /api/global-items?q=...&tags=... endpoint
+- D-12: Empty state: helpful message when no results, with "Add Manually" link (Phase 22 wires this)
+- D-13: Loading state: skeleton cards matching result grid pattern
+- D-14: Tags fetched from new GET /api/tags endpoint (returns all tags, lightweight)
+- D-15: Horizontal scrollable row of rounded-full chips, tapping toggles active state, multiple tags (AND filtering)
+- D-16: Active chips: bg-blue-100 text-blue-700; inactive: bg-gray-100 text-gray-500
+- D-17: Quick-access chips show common/popular tags first
+- D-18: Reuse/adapt GlobalItemCard component pattern
+- D-19: Each card has "Add"/"+" button -- stub in Phase 20 (Phase 21 wires action)
+- D-20: Cards responsive: 1 column mobile, 2-3 on desktop
+- D-21: New UIStore state: catalogSearchOpen, catalogSearchMode ("collection" | "thread" | null)
+- D-22: New UIStore actions: openCatalogSearch(mode), closeCatalogSearch()
+- D-23: FAB menu state: fabMenuOpen, openFabMenu(), closeFabMenu()
+- D-24: New endpoint GET /api/tags returning { id, name }[]. Public, no auth
+
+### Claude's Discretion
+- Animation style for FAB menu (spring, ease, duration)
+- Exact tag chip ordering strategy
+- Card grid gap and sizing details
+- Whether to debounce search input (recommendation: yes, 300ms)
+- Skeleton card count during loading
+- Whether "Add Manually" link is visible in Phase 20 or deferred to Phase 22
+
+### Deferred Ideas (OUT OF SCOPE)
+- "Add Manually" link wiring -- Phase 22
+- Actual add-to-collection flow (confirmation step with category picker) -- Phase 21
+- Actual add-to-thread flow (instant candidate creation) -- Phase 21
+- Search result sorting/ordering options
+- Recent searches or search history
+- "Popular items" section when search is empty
+
+
+
+## Phase Requirements
+
+| ID | Description | Research Support |
+|----|-------------|------------------|
+| CATFLOW-01 | FAB shows mini menu with "Add to Collection" and "Start Thread" globally, plus "New Setup" on setups page | Existing FAB in __root.tsx (lines 257-278), UIStore pattern for state, Framer Motion for animations, useMatchRoute for setups page detection |
+| CATFLOW-02 | Full-screen catalog search with tag chip filtering | Existing overlay pattern (CreateThreadModal), debounce pattern (global-items/index.tsx), useGlobalItems hook, tags schema in DB, GlobalItemCard component |
+
+
+## Standard Stack
+
+### Core (already installed)
+| Library | Version | Purpose | Why Standard |
+|---------|---------|---------|--------------|
+| React | 19 | UI framework | Project standard |
+| framer-motion | ^12.38.0 | FAB menu animations, overlay transitions | Already used in collection/threads for AnimatePresence/motion |
+| zustand | (installed) | UIStore for FAB and overlay state | Project standard for UI state |
+| @tanstack/react-query | (installed) | Data fetching for tags and global items | Project standard |
+| hono | (installed) | Tags API endpoint | Project standard |
+| drizzle-orm | (installed) | Tags query | Project standard |
+
+### Supporting
+| Library | Version | Purpose | When to Use |
+|---------|---------|---------|-------------|
+| tailwind CSS v4 | (installed) | All styling | Project standard |
+
+### Alternatives Considered
+None -- all required libraries are already in the project.
+
+## Architecture Patterns
+
+### New Components
+```
+src/client/
+ components/
+ FabMenu.tsx # FAB + mini menu overlay (extracted from __root.tsx)
+ CatalogSearchOverlay.tsx # Full-screen search overlay
+ TagChips.tsx # Horizontal scrollable tag chip row
+ CatalogItemCard.tsx # Adapted GlobalItemCard with Add button
+ hooks/
+ useTags.ts # TanStack Query hook for GET /api/tags
+ stores/
+ uiStore.ts # Extended with FAB + catalog search state
+src/server/
+ routes/
+ tags.ts # New: GET /api/tags
+ services/
+ tag.service.ts # New: getAllTags()
+ index.ts # Register /api/tags AND /api/global-items routes
+```
+
+### Pattern 1: UIStore State Extension
+**What:** Add FAB menu and catalog search state slices to existing Zustand store
+**When to use:** This is the established project pattern for dialog/panel/overlay state
+**Example:**
+```typescript
+// Extend UIState interface
+fabMenuOpen: boolean;
+openFabMenu: () => void;
+closeFabMenu: () => void;
+
+catalogSearchOpen: boolean;
+catalogSearchMode: "collection" | "thread" | null;
+openCatalogSearch: (mode: "collection" | "thread") => void;
+closeCatalogSearch: () => void;
+```
+
+### Pattern 2: FAB Visibility via useMatchRoute
+**What:** Replace current collection-only FAB visibility with auth-aware global visibility
+**When to use:** The current FAB uses `matchRoute` to show only on collection gear tab. Phase 20 changes this to show on all authenticated routes except login and public pages.
+**Example:**
+```typescript
+// Current logic (to be replaced):
+const showFab = isCollection && (!collectionSearch || ...);
+
+// New logic:
+const isPublicRoute = location.pathname.startsWith("/users/") || location.pathname === "/login";
+const showFab = isAuthenticated && !isPublicRoute;
+```
+
+### Pattern 3: Full-Screen Overlay (CreateThreadModal pattern)
+**What:** Fixed inset-0 z-50 overlay managed by UIStore boolean
+**When to use:** Established pattern in CreateThreadModal
+**Key difference:** Catalog search overlay is full-screen white (not a centered modal with backdrop). Uses `bg-white` not `bg-black/50`.
+
+### Pattern 4: Debounced Search (global-items/index.tsx pattern)
+**What:** useState + useEffect timer for 300ms debounce on search input
+**When to use:** Exact pattern from existing catalog page, copy directly
+**Example:**
+```typescript
+const [searchInput, setSearchInput] = useState("");
+const [debouncedQuery, setDebouncedQuery] = useState("");
+
+useEffect(() => {
+ const timer = setTimeout(() => setDebouncedQuery(searchInput), 300);
+ return () => clearTimeout(timer);
+}, [searchInput]);
+```
+
+### Pattern 5: Framer Motion FAB Menu Animation
+**What:** AnimatePresence + motion.div for menu items fanning out vertically
+**When to use:** Framer Motion already imported in collection/threads
+**Example:**
+```typescript
+import { AnimatePresence, motion } from "framer-motion";
+
+// Menu items appear one by one with staggered animation
+
+ {fabMenuOpen && (
+
+ {/* menu items */}
+
+ )}
+
+```
+
+### Anti-Patterns to Avoid
+- **Route-based overlay:** Don't make the catalog search a route. It's managed via UIStore like all other dialogs in the project. Route changes would break the "open from anywhere" requirement.
+- **Global state for search query:** Don't store the search input in UIStore. Keep it local to CatalogSearchOverlay component (matches existing pattern in global-items/index.tsx).
+- **Custom debounce hook:** Don't create a generic `useDebounce` hook. The inline useState+useEffect pattern is already established and simple.
+
+## Don't Hand-Roll
+
+| Problem | Don't Build | Use Instead | Why |
+|---------|-------------|-------------|-----|
+| Animations | CSS keyframe animations | Framer Motion (AnimatePresence + motion) | Already used, handles enter/exit, spring physics |
+| Search debounce | Custom debounce utility | Inline useState+useEffect (300ms) | Established pattern, 5 lines, no abstraction needed |
+| Tag data fetching | Manual fetch+cache | TanStack Query `useQuery` hook | Project standard, handles caching/stale/refetch |
+| Overlay state management | useState in __root | Zustand UIStore slice | Project standard for cross-component dialog state |
+
+## Common Pitfalls
+
+### Pitfall 1: Global-Items Route Not Registered
+**What goes wrong:** The `GET /api/global-items` endpoint returns 404 because `globalItemRoutes` exists in `src/server/routes/global-items.ts` but is NOT registered in `src/server/index.ts`.
+**Why it happens:** Phase 19 created the route file but the route registration line `app.route("/api/global-items", globalItemRoutes)` is missing from index.ts.
+**How to avoid:** First task must register the global-items route in index.ts. Also register the new tags route.
+**Warning signs:** 404 errors when searching in the overlay.
+
+### Pitfall 2: Z-Index Layering Conflicts
+**What goes wrong:** FAB menu appears behind existing overlays, or catalog search overlay doesn't cover everything.
+**Why it happens:** Project uses layered z-indexes: z-20 (FAB), z-30 (backdrop), z-40 (panels), z-50 (modals).
+**How to avoid:** FAB stays at z-20, FAB menu backdrop at z-30, FAB menu items at z-40. Catalog search overlay at z-50 (same as modals -- it replaces the entire screen). When catalog search is open, FAB should be hidden.
+**Warning signs:** Visual stacking issues on mobile.
+
+### Pitfall 3: FAB Visible on Public Routes
+**What goes wrong:** Unauthenticated users or public profile visitors see the FAB.
+**Why it happens:** Current FAB visibility check is collection-only. New global visibility needs explicit exclusion of public routes and unauthenticated state.
+**How to avoid:** Check both `isAuthenticated` AND `!isPublicRoute`. Public routes: `/login`, `/users/*`.
+
+### Pitfall 4: Scroll Lock When Overlay Is Open
+**What goes wrong:** Background page scrolls while full-screen overlay is visible.
+**Why it happens:** Fixed overlay doesn't prevent body scroll on mobile.
+**How to avoid:** Add `overflow-hidden` to body when overlay is open, or use `overflow-y-auto` on the overlay container itself and ensure it captures all scroll events.
+
+### Pitfall 5: Tag Query Parameter Format
+**What goes wrong:** Tag filtering sends wrong format to the API.
+**Why it happens:** API expects `?tags=tag1,tag2` (comma-separated names). Easy to accidentally send IDs or wrong separator.
+**How to avoid:** Tags endpoint returns `{ id, name }[]`. When building the query, join selected tag names with commas. The existing `searchGlobalItems` service already parses this format.
+
+### Pitfall 6: Search Fires on Overlay Mount
+**What goes wrong:** Opening the overlay triggers an immediate search for all items with empty query.
+**Why it happens:** useGlobalItems fires on mount with no query, fetching the entire catalog.
+**How to avoid:** Either pass `enabled: false` until user types something, or accept the initial load as intentional (shows browsable catalog). The CONTEXT.md implies browse-first is acceptable since D-09 shows tag filtering.
+
+## Code Examples
+
+### Tags Service (new file)
+```typescript
+// src/server/services/tag.service.ts
+import { db as prodDb } from "../../db/index.ts";
+import { tags } from "../../db/schema.ts";
+
+type Db = typeof prodDb;
+
+export async function getAllTags(db: Db = prodDb) {
+ return db.select({ id: tags.id, name: tags.name }).from(tags);
+}
+```
+
+### Tags Route (new file)
+```typescript
+// src/server/routes/tags.ts
+import { Hono } from "hono";
+import { getAllTags } from "../services/tag.service.ts";
+
+type Env = { Variables: { db?: any } };
+const app = new Hono();
+
+app.get("/", async (c) => {
+ const db = c.get("db");
+ const allTags = await getAllTags(db);
+ return c.json(allTags);
+});
+
+export { app as tagRoutes };
+```
+
+### useTags Hook (new file)
+```typescript
+// src/client/hooks/useTags.ts
+import { useQuery } from "@tanstack/react-query";
+import { apiGet } from "../lib/api";
+
+interface Tag {
+ id: number;
+ name: string;
+}
+
+export function useTags() {
+ return useQuery({
+ queryKey: ["tags"],
+ queryFn: () => apiGet("/api/tags"),
+ staleTime: 5 * 60 * 1000, // Tags rarely change, cache 5 min
+ });
+}
+```
+
+### Extended useGlobalItems (tag support)
+```typescript
+// Update src/client/hooks/useGlobalItems.ts
+export function useGlobalItems(query?: string, tags?: string[]) {
+ const params = new URLSearchParams();
+ if (query) params.set("q", query);
+ if (tags && tags.length > 0) params.set("tags", tags.join(","));
+ const qs = params.toString();
+
+ return useQuery({
+ queryKey: ["global-items", query ?? "", tags ?? []],
+ queryFn: () =>
+ apiGet(`/api/global-items${qs ? `?${qs}` : ""}`),
+ });
+}
+```
+
+### Skeleton Card (loading state)
+```typescript
+// Reuse existing skeleton pattern from global-items/index.tsx
+function SkeletonCard() {
+ return (
+
+ );
+}
+```
+
+## State of the Art
+
+| Old Approach | Current Approach | When Changed | Impact |
+|--------------|------------------|--------------|--------|
+| FAB only on collection gear tab | FAB globally visible | Phase 20 | Must update visibility logic in __root.tsx |
+| Single-action FAB (opens add panel) | Multi-action FAB menu | Phase 20 | FAB click behavior changes from direct action to menu toggle |
+| No catalog search overlay | Full-screen overlay with tag filtering | Phase 20 | New UIStore state slices, new component tree |
+
+## Validation Architecture
+
+### Test Framework
+| Property | Value |
+|----------|-------|
+| Framework | bun:test (service/route) + Playwright (E2E) |
+| Config file | bunfig.toml (bun test) / playwright.config.ts (E2E) |
+| Quick run command | `bun test tests/routes/tags.test.ts` |
+| Full suite command | `bun test` |
+
+### Phase Requirements -> Test Map
+| Req ID | Behavior | Test Type | Automated Command | File Exists? |
+|--------|----------|-----------|-------------------|-------------|
+| CATFLOW-01 | FAB menu renders with correct options | E2E | `bun run test:e2e` | Wave 0 |
+| CATFLOW-01 | FAB visible on authenticated routes, hidden on public | E2E | `bun run test:e2e` | Wave 0 |
+| CATFLOW-02 | GET /api/tags returns all tags | unit | `bun test tests/routes/tags.test.ts` | Wave 0 |
+| CATFLOW-02 | Tag service returns tags from DB | unit | `bun test tests/services/tag.service.test.ts` | Wave 0 |
+| CATFLOW-02 | GET /api/global-items?tags=... filters correctly | unit | `bun test tests/routes/global-items.test.ts` | Exists (Phase 19) |
+| CATFLOW-02 | Search overlay opens/closes via UIStore | manual-only | Visual verification | N/A |
+
+### Sampling Rate
+- **Per task commit:** `bun test tests/routes/tags.test.ts && bun test tests/services/tag.service.test.ts`
+- **Per wave merge:** `bun test`
+- **Phase gate:** Full suite green before /gsd:verify-work
+
+### Wave 0 Gaps
+- [ ] `tests/services/tag.service.test.ts` -- covers tag retrieval
+- [ ] `tests/routes/tags.test.ts` -- covers GET /api/tags endpoint
+- [ ] Verify `tests/routes/global-items.test.ts` covers tag filtering (may already exist from Phase 19)
+
+## Open Questions
+
+1. **Global-items route registration**
+ - What we know: The route file exists at `src/server/routes/global-items.ts` but is NOT registered in `src/server/index.ts` (no `app.route("/api/global-items", ...)` line)
+ - What's unclear: Whether this was intentional (staged for Phase 20) or an oversight from Phase 19
+ - Recommendation: Register it as part of Phase 20 server setup task, alongside the new tags route
+
+2. **Owner count in search results**
+ - What we know: D-10 says cards show "owner count". The `searchGlobalItems` service returns basic fields but NOT owner count. Only `getGlobalItemWithOwnerCount` includes it (single item fetch).
+ - What's unclear: Whether to add owner count to search results (requires a join/subquery) or show it only on detail pages
+ - Recommendation: For Phase 20, omit owner count from search cards to keep search fast. The card already shows brand, model, weight, price, category. Owner count can be added in a follow-up if needed, or when clicking through to detail.
+
+## Project Constraints (from CLAUDE.md)
+
+- **Styling:** Tailwind CSS v4, tabs for indentation, double quotes (Biome)
+- **State management:** Zustand for UI state only, server data in React Query
+- **API pattern:** Hono routes with Zod validation, delegate to service functions
+- **Services:** Pure functions taking db instance, no HTTP awareness
+- **Route registration:** `app.route("/api/...", routes)` in `src/server/index.ts`
+- **Path alias:** `@/*` maps to `./src/*`
+- **Testing:** Bun test runner for unit/integration, Playwright for E2E
+- **Branching:** Feature branch off Develop, merge back when complete
+
+## Sources
+
+### Primary (HIGH confidence)
+- `src/client/routes/__root.tsx` -- Current FAB implementation, route matching patterns
+- `src/client/stores/uiStore.ts` -- Zustand store structure, all existing state slices
+- `src/client/components/CreateThreadModal.tsx` -- Full-screen overlay pattern
+- `src/client/components/GlobalItemCard.tsx` -- Card component with badge pattern
+- `src/client/routes/global-items/index.tsx` -- Debounce pattern, skeleton cards, search UI
+- `src/server/routes/global-items.ts` -- Existing search endpoint with tag support
+- `src/server/services/global-item.service.ts` -- Search service with ILIKE + tag AND filtering
+- `src/db/schema.ts` -- Tags and globalItemTags table definitions
+- `src/server/index.ts` -- Route registration (global-items NOT registered)
+- `package.json` -- framer-motion ^12.38.0 confirmed installed
+
+## Metadata
+
+**Confidence breakdown:**
+- Standard stack: HIGH -- all libraries already installed, no new dependencies
+- Architecture: HIGH -- all patterns exist in codebase, pure extension
+- Pitfalls: HIGH -- identified from direct code inspection (missing route registration, z-index layering, public route exclusion)
+
+**Research date:** 2026-04-06
+**Valid until:** 2026-05-06 (stable -- all internal code patterns)