diff --git a/.planning/phases/04-ux-improvements/04-CONTEXT.md b/.planning/phases/04-ux-improvements/04-CONTEXT.md
new file mode 100644
index 0000000..35bee78
--- /dev/null
+++ b/.planning/phases/04-ux-improvements/04-CONTEXT.md
@@ -0,0 +1,118 @@
+# Phase 4: UX Improvements - Context
+
+**Gathered:** 2026-03-24
+**Status:** Ready for planning
+
+
+## Phase Boundary
+
+Deliver UX features that make the dashboard genuinely usable at scale: bulk dismiss (all + per-group), search and filter across updates, new-update indicators (badge, tab title, toast, highlight), and accessibility fixes (theme toggle, always-visible drag handle). No new database tables — bulk dismiss adds Store methods; search/filter is client-side; indicators use localStorage.
+
+
+
+
+## Implementation Decisions
+
+### Bulk dismiss (BULK-01, BULK-02)
+- **D-01:** Add two new Store methods: `AcknowledgeAll() (count int, err error)` and `AcknowledgeByTag(tagID int) (count int, err error)` — consistent with existing `AcknowledgeUpdate(image)` pattern
+- **D-02:** Two new API endpoints: `POST /api/updates/acknowledge-all` and `POST /api/updates/acknowledge-by-tag` (with `tag_id` in body) — returning the count of dismissed items
+- **D-03:** UI placement: "Dismiss All" button in the header/stats area; "Dismiss Group" button in each TagSection header next to the existing delete button
+- **D-04:** Confirmation: modal/dialog confirmation for dismiss-all (high-impact action); inline confirm pattern (matching existing tag delete) for per-group dismiss
+
+### Search and filter (SRCH-01 through SRCH-04)
+- **D-05:** Client-side filtering only — all data is already in memory from polling, no new API endpoints needed
+- **D-06:** Filter bar placed above the sections list, below the stats row
+- **D-07:** Controls: text search input (filters by image name), status dropdown (all/pending/acknowledged), tag dropdown (all/specific tag/untagged), sort dropdown (date/name/registry)
+- **D-08:** Filters do not persist across page reloads — reset on each visit (dashboard is a quick-glance tool)
+
+### New-update indicators (INDIC-01 through INDIC-04)
+- **D-09:** Pending update badge/counter displayed in the Header component next to the "Diun Dashboard" title — always visible
+- **D-10:** Browser tab title reflects pending count: `"DiunDash (N)"` when N > 0, `"DiunDash"` when zero
+- **D-11:** Toast notification when new updates arrive during polling — auto-dismiss after 5 seconds with manual dismiss button; non-stacking (latest update replaces previous toast)
+- **D-12:** "New since last visit" detection via localStorage timestamp — store `lastVisitTimestamp` on page unload; updates with `received_at` after that timestamp get a visual highlight
+- **D-13:** Highlight style: subtle left border accent (e.g., `border-l-4 border-amber-500`) on ServiceCard for new-since-last-visit items
+
+### Accessibility and theme (A11Y-01, A11Y-02)
+- **D-14:** Light/dark theme toggle placed in the Header bar next to the refresh button — icon button (sun/moon)
+- **D-15:** Theme preference persisted in localStorage; on first visit, respects `prefers-color-scheme` media query; removes the hardcoded `classList.add('dark')` from `main.tsx`
+- **D-16:** Drag handle on ServiceCard always visible at reduced opacity (`opacity-40`), full opacity on hover — removes the current `opacity-0 group-hover:opacity-100` pattern
+
+### Claude's Discretion
+- Toast component implementation (custom or shadcn/ui Sonner)
+- Exact filter bar layout and responsive breakpoints
+- Animation/transition details for theme switching
+- Whether to show a count in the per-group dismiss button (e.g., "Dismiss 3")
+- Sort order default (most recent first vs alphabetical)
+
+
+
+
+## Canonical References
+
+**Downstream agents MUST read these before planning or implementing.**
+
+### Store interface and handler patterns
+- `pkg/diunwebhook/store.go` -- Store interface (9 methods; new bulk methods extend this)
+- `pkg/diunwebhook/sqlite_store.go` -- SQLiteStore implementation (pattern for new methods)
+- `pkg/diunwebhook/postgres_store.go` -- PostgresStore implementation (must also get new methods)
+- `pkg/diunwebhook/server.go` -- Server struct and handler registration (new endpoints go here)
+
+### Frontend components affected
+- `frontend/src/App.tsx` -- Root component (filter state, bulk dismiss wiring, layout changes)
+- `frontend/src/hooks/useUpdates.ts` -- Polling hook (toast detection, bulk dismiss callbacks, tab title)
+- `frontend/src/components/Header.tsx` -- Header (badge counter, theme toggle, dismiss-all button)
+- `frontend/src/components/TagSection.tsx` -- Tag sections (per-group dismiss button)
+- `frontend/src/components/ServiceCard.tsx` -- Service cards (new-update highlight, drag handle fix)
+- `frontend/src/main.tsx` -- Entry point (theme initialization logic change)
+
+### Requirements
+- `.planning/REQUIREMENTS.md` -- BULK-01, BULK-02, SRCH-01-04, INDIC-01-04, A11Y-01, A11Y-02
+
+
+
+
+## Existing Code Insights
+
+### Reusable Assets
+- `Button` component (`frontend/src/components/ui/button.tsx`): use for dismiss-all and per-group dismiss buttons
+- `Badge` component (`frontend/src/components/ui/badge.tsx`): use for pending count badge in header
+- `cn()` utility (`frontend/src/lib/utils.ts`): conditional class composition for highlight styles
+- `timeAgo()` utility (`frontend/src/lib/time.ts`): already used in ServiceCard, relevant for toast messages
+- `AcknowledgeButton` component: existing per-item dismiss pattern to follow for bulk buttons
+
+### Established Patterns
+- `useUpdates` hook: centralized data fetching + state management -- extend with bulk dismiss, toast detection, and tab title side effects
+- Optimistic updates: used for tag assignment -- apply same pattern for bulk dismiss (update UI immediately, fire API call)
+- Polling at 5s intervals: toast detection can diff previous vs current poll results
+- Dark mode via Tailwind `class` strategy: theme toggle adds/removes `dark` class on `document.documentElement`
+- No global state library: filter state lives in `App.tsx` via `useState`, passed as props
+
+### Integration Points
+- `cmd/diunwebhook/main.go`: register 2 new routes on the mux
+- `store.go`: add `AcknowledgeAll` and `AcknowledgeByTag` to Store interface
+- `sqlite_store.go` + `postgres_store.go`: implement new Store methods in both dialects
+- `server.go`: add handler methods for bulk acknowledge endpoints
+- `App.tsx`: add filter state, wire filter bar component, pass bulk dismiss callbacks
+- `Header.tsx`: add pending count badge, theme toggle button, dismiss-all button
+- `main.tsx`: replace hardcoded dark mode with localStorage + prefers-color-scheme logic
+
+
+
+
+## Specific Ideas
+
+No specific requirements -- open to standard approaches. The existing shadcn/ui + Tailwind dark mode setup provides the foundation for theme toggling.
+
+
+
+
+## Deferred Ideas
+
+None -- discussion stayed within phase scope.
+
+
+
+---
+
+*Phase: 04-ux-improvements*
+*Context gathered: 2026-03-24 via auto mode*
diff --git a/.planning/phases/04-ux-improvements/04-DISCUSSION-LOG.md b/.planning/phases/04-ux-improvements/04-DISCUSSION-LOG.md
new file mode 100644
index 0000000..d719e31
--- /dev/null
+++ b/.planning/phases/04-ux-improvements/04-DISCUSSION-LOG.md
@@ -0,0 +1,181 @@
+# Phase 4: UX Improvements - Discussion Log
+
+> **Audit trail only.** Do not use as input to planning, research, or execution agents.
+> Decisions are captured in CONTEXT.md -- this log preserves the alternatives considered.
+
+**Date:** 2026-03-24
+**Phase:** 04-ux-improvements
+**Areas discussed:** Bulk dismiss scope, Search/filter architecture, New-update detection, Theme toggle behavior
+**Mode:** auto (all decisions auto-selected)
+
+---
+
+## Bulk Dismiss Scope
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| New Store methods + dedicated endpoints | Add AcknowledgeAll and AcknowledgeByTag to Store interface with new HTTP endpoints | ✓ |
+| Batch image list from frontend | Frontend sends list of image names to a generic bulk-dismiss endpoint | |
+| Reuse existing single-dismiss in loop | Frontend calls existing PATCH /api/updates/{image} for each item | |
+
+**User's choice:** [auto] New Store methods + dedicated endpoints (recommended default)
+**Notes:** Consistent with existing per-image dismiss pattern. Server-side bulk is more efficient and keeps frontend simple.
+
+---
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Tag ID parameter for per-group dismiss | Server looks up which images belong to the tag | ✓ |
+| Send list of images from frontend | Frontend determines which images are in the group | |
+
+**User's choice:** [auto] Tag ID parameter (recommended default)
+**Notes:** Server already knows tag-image relationships, fewer bytes over the wire.
+
+---
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Dismiss-all in header, dismiss-group in TagSection header | Natural placement near existing controls | ✓ |
+| All bulk actions in a separate toolbar | Dedicated action bar for bulk operations | |
+
+**User's choice:** [auto] Dismiss-all in header area, dismiss-by-group in each TagSection header (recommended default)
+
+---
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Confirmation dialog for all, inline for per-group | Dismiss-all gets modal; per-group matches existing tag-delete pattern | ✓ |
+| No confirmation for either | Fast but risky | |
+| Confirmation for both | Consistent but slower workflow | |
+
+**User's choice:** [auto] Yes, confirmation dialog for dismiss-all; inline confirm for per-group (recommended default)
+
+---
+
+## Search/Filter Architecture
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Client-side filtering | All data already in memory from polling; filter in React state | ✓ |
+| Server-side with query params | Add filter params to GET /api/updates endpoint | |
+| Hybrid (client with server fallback) | Client-side now, server-side when data grows | |
+
+**User's choice:** [auto] Client-side filtering (recommended default)
+**Notes:** All data is fetched via 5s polling. No need for server-side filtering at this scale.
+
+---
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Filter bar above sections, below stats | Standard placement, visible without scrolling | ✓ |
+| Collapsible sidebar filters | More space but hidden by default | |
+| Inline per-section filters | Distributed, harder to use across groups | |
+
+**User's choice:** [auto] Filter bar above the sections list (recommended default)
+
+---
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Text search + status + tag + sort dropdowns | Covers all SRCH requirements | ✓ |
+| Text search only | Minimal, doesn't cover SRCH-02/03/04 | |
+
+**User's choice:** [auto] Search text input + status dropdown + tag dropdown + sort dropdown (recommended default)
+
+---
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| No persistence (reset on reload) | Simpler, dashboard is quick-glance tool | ✓ |
+| Persist in URL params | Shareable/bookmarkable filters | |
+| Persist in localStorage | Remembers across visits | |
+
+**User's choice:** [auto] No persistence -- reset on reload (recommended default)
+
+---
+
+## New-Update Detection
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| localStorage timestamp | Store last visit time client-side, compare with received_at | ✓ |
+| Server-side last-seen tracking | Track per-user last-seen on server | |
+| Session-only (no persistence) | Only detect new items arriving during current session | |
+
+**User's choice:** [auto] localStorage timestamp (recommended default)
+**Notes:** Single-user tool, no server changes needed. Simple and effective.
+
+---
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Auto-dismiss after 5s with dismiss button | Non-intrusive, doesn't pile up | ✓ |
+| Sticky until manually dismissed | Persistent but can pile up | |
+| No toast, badge only | Minimal notification | |
+
+**User's choice:** [auto] Auto-dismiss after 5 seconds with dismiss button (recommended default)
+
+---
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Header badge + tab title | Always visible, covers INDIC-01 and INDIC-02 | ✓ |
+| Stats card only | Already partially exists | |
+
+**User's choice:** [auto] In the header next to title + browser tab title (recommended default)
+
+---
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Subtle left border accent | Visible but not overwhelming | ✓ |
+| Background color change | More prominent | |
+| Pulsing dot indicator | Animated, attention-grabbing | |
+
+**User's choice:** [auto] Subtle left border accent on ServiceCard (recommended default)
+
+---
+
+## Theme Toggle Behavior
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Header bar, next to refresh button | Compact, always accessible | ✓ |
+| Footer | Less prominent | |
+| Settings page | Requires new page | |
+
+**User's choice:** [auto] Header bar, next to refresh button (recommended default)
+
+---
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| localStorage with prefers-color-scheme fallback | Standard pattern, no server involvement | ✓ |
+| Cookie-based | SSR-friendly but not needed here | |
+| No persistence | Resets every visit | |
+
+**User's choice:** [auto] localStorage with prefers-color-scheme fallback (recommended default)
+
+---
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Always visible at reduced opacity | Accessible without cluttering UI | ✓ |
+| Always fully visible | More prominent but noisier | |
+| Keep hover-only | Current behavior, accessibility issue | |
+
+**User's choice:** [auto] Always visible at reduced opacity, full opacity on hover (recommended default)
+
+---
+
+## Claude's Discretion
+
+- Toast component implementation (custom or shadcn/ui Sonner)
+- Exact filter bar layout and responsive breakpoints
+- Animation/transition details for theme switching
+- Whether to show a count in the per-group dismiss button
+- Sort order default
+
+## Deferred Ideas
+
+None -- discussion stayed within phase scope.