diff --git a/IMPLEMENTATION_COLLECTION_FIX.md b/IMPLEMENTATION_COLLECTION_FIX.md
new file mode 100644
index 0000000..04d72e5
--- /dev/null
+++ b/IMPLEMENTATION_COLLECTION_FIX.md
@@ -0,0 +1,608 @@
+# Implementation Plan: Collection Detail Page Fix + library_id Filter
+
+## Overview
+
+Fix the broken `/collections/:id` page (inline JS bug) and add library_id support to the search API. This uses a hybrid approach: minimal TypeScript for client-only features, HTMX-like patterns for CRUD operations.
+
+---
+
+## Files to Modify
+
+| File | Changes |
+|------|---------|
+| `templates/collections.templ` | Remove inline JS, add data attributes, add toggle UI |
+| `web/src/collections.ts` | Add minimal TypeScript functions (~60 lines) |
+| `internal/handlers/media.go` | Add library_id param to search handler |
+| `internal/router/frontend.go` | (No changes needed - existing modals work) |
+
+---
+
+## Step 1: Update Search API to Accept library_id
+
+### File: `internal/handlers/media.go`
+
+**Find** (around line 1418-1442):
+```go
+// SearchMediaItems handles GET /api/media-items/search
+func (mh *MediaHandler) SearchMediaItems(c echo.Context) error {
+ query := c.QueryParam("q")
+ userID := c.Get("user_id").(string)
+
+ if query == "" {
+ return c.JSON(http.StatusBadRequest, map[string]string{"error": "query parameter 'q' is required"})
+ }
+
+ userUUID, err := uuid.Parse(userID)
+ if err != nil {
+ return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user id"})
+ }
+
+ limit := int32(50)
+ offset := int32(0)
+
+ searchPattern := "%" + query + "%"
+
+ partialResults, err := mh.db.SearchMediaItems(c.Request().Context(), database.SearchMediaItemsParams{
+ SearchPattern: pgtype.Text{String: searchPattern, Valid: true},
+ UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
+ Limit: pgtype.Int4{Int32: limit, Valid: true},
+ Offset: pgtype.Int4{Int32: offset, Valid: true},
+ })
+```
+
+**Replace with**:
+```go
+// SearchMediaItems handles GET /api/media-items/search
+func (mh *MediaHandler) SearchMediaItems(c echo.Context) error {
+ query := c.QueryParam("q")
+ userID := c.Get("user_id").(string)
+ libraryID := c.QueryParam("library_id") // Optional: filter by library
+
+ if query == "" {
+ return c.JSON(http.StatusBadRequest, map[string]string{"error": "query parameter 'q' is required"})
+ }
+
+ userUUID, err := uuid.Parse(userID)
+ if err != nil {
+ return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user id"})
+ }
+
+ // Validate library_id if provided
+ var libUUID *uuid.UUID
+ if libraryID != "" {
+ lib, err := uuid.Parse(libraryID)
+ if err != nil {
+ return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid library_id"})
+ }
+ libUUID = &lib
+ }
+
+ limit := int32(50)
+ offset := int32(0)
+
+ searchPattern := "%" + query + "%"
+
+ // Build params - conditionally add library_id filter
+ params := database.SearchMediaItemsParams{
+ SearchPattern: pgtype.Text{String: searchPattern, Valid: true},
+ UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
+ Limit: pgtype.Int4{Int32: limit, Valid: true},
+ Offset: pgtype.Int4{Int32: offset, Valid: true},
+ }
+
+ // If library_id provided, add to query params
+ if libUUID != nil {
+ params.LibraryID = pgtype.UUID{Bytes: *libUUID, Valid: true}
+ }
+
+ partialResults, err := mh.db.SearchMediaItems(c.Request().Context(), params)
+```
+
+**Note**: You need to check if `SearchMediaItemsParams` in `database.SearchMediaItemsParams` already has a `LibraryID` field. If not, you'll need to add it to the SQL query.
+
+---
+
+## Step 2: Add library_id to SQL Query (if needed)
+
+### File: `internal/database/queries.sql`
+
+**Find** the `SearchMediaItems` query:
+```sql
+-- name: SearchMediaItems :many
+SELECT ...
+FROM media_items mi
+WHERE ...
+```
+
+**Add** library_id filter (if not present):
+```sql
+-- name: SearchMediaItems :many
+SELECT mi.id, mi.user_id, mi.title, mi.author, mi.cover_image_path, mi.library_id, ...
+FROM media_items mi
+WHERE ...
+ AND ($1::uuid IS NULL OR mi.library_id = $1::uuid) -- Add this filter
+```
+
+**Update** the `SearchMediaItemsParams` struct in `queries.sql.go` if needed to include LibraryID.
+
+---
+
+## Step 3: Remove Inline JS from Template
+
+### File: `templates/collections.templ`
+
+**Find** the inline script block (lines 255-520):
+```go
+
+```
+
+**Replace with** a hidden data element (add after the `
` tag or where appropriate):
+```go
+
+
+
+
+
+
+```
+
+**Note**: You need to pass `libraryID` to the template. Check the handler in `frontend.go` that renders `CollectionDetail` and add `libraryID` to the template data.
+
+---
+
+## Step 4: Add Toggle UI to Add Books Modal
+
+### File: `templates/collections.templ`
+
+**Find** the Add Books modal (around line 157):
+```go
+
+```
+
+**Add** the toggle after this button:
+```go
+
+ {# Toggle only shows when library_id is present - handled by JS #}
+```
+
+**Find** the Add Books modal content (around line 339 in generated, find in source):
+```go
+
+ ...
+
Search and select books to add to this collection.
+
+
+
+
+```
+
+**Replace with**:
+```go
+
+ ...
+
Search and select books to add to this collection.
+
+
+
+
+
+
+
+
+
+
+```
+
+---
+
+## Step 5: Add Minimal TypeScript Functions
+
+### File: `web/src/collections.ts`
+
+**Add** at the end of the file:
+
+```typescript
+// ============================================================================
+// Collection Detail Page - Minimal TypeScript (~60 lines)
+// ============================================================================
+
+interface BookItem {
+ id: string;
+ title: string;
+ author: string;
+ cover: string;
+}
+
+let collectionId = "";
+let libraryId = "";
+let selectedBooks = new Set();
+let booksToRemove = new Set();
+
+// Initialize from data attributes (called on page load)
+function initCollectionDetail(): void {
+ const dataEl = document.getElementById("collection-data");
+ if (dataEl) {
+ collectionId = dataEl.dataset.id || "";
+ libraryId = dataEl.dataset.libraryId || "";
+
+ // Show/hide library filter toggle based on whether library_id is present
+ const filterContainer = document.getElementById("library-filter-container");
+ if (filterContainer) {
+ if (libraryId) {
+ filterContainer.classList.remove("hidden");
+ // Default: checked (filter by library)
+ const checkbox = document.getElementById("filter-by-library") as HTMLInputElement;
+ if (checkbox) checkbox.checked = true;
+ } else {
+ filterContainer.classList.add("hidden");
+ }
+ }
+ }
+}
+
+// Get current library filter setting
+function getLibraryFilterParam(): string {
+ if (!libraryId) return "";
+
+ const checkbox = document.getElementById("filter-by-library") as HTMLInputElement;
+ if (checkbox && checkbox.checked) {
+ return `&library_id=${libraryId}`;
+ }
+ return "";
+}
+
+// Modal functions (called from onclick attributes)
+function showAddBooksModal(): void {
+ const modal = document.getElementById("add-books-modal");
+ if (modal) modal.classList.remove("hidden");
+ selectedBooks.clear();
+
+ const results = document.getElementById("book-results");
+ if (results) {
+ results.innerHTML = '
Enter at least 2 characters to search.
';
+ }
+}
+
+function hideAddBooksModal(): void {
+ const modal = document.getElementById("add-books-modal");
+ if (modal) modal.classList.add("hidden");
+
+ const searchInput = document.getElementById("book-search") as HTMLInputElement;
+ if (searchInput) searchInput.value = "";
+
+ const results = document.getElementById("book-results");
+ if (results) results.innerHTML = "";
+
+ selectedBooks.clear();
+}
+
+// Search books - now includes library filter
+async function searchBooks(): Promise {
+ const searchInput = document.getElementById("book-search") as HTMLInputElement;
+ const container = document.getElementById("book-results");
+ if (!searchInput || !container) return;
+
+ const searchTerm = searchInput.value;
+ if (searchTerm.length < 2) {
+ container.innerHTML = '