diff --git a/.planning/phases/04-ui-polish-mobile-ux/04-RESEARCH.md b/.planning/phases/04-ui-polish-mobile-ux/04-RESEARCH.md
new file mode 100644
index 0000000..5c6e791
--- /dev/null
+++ b/.planning/phases/04-ui-polish-mobile-ux/04-RESEARCH.md
@@ -0,0 +1,565 @@
+# Phase 04: UI Polish + Mobile UX — Research
+
+**Researched:** 2026-05-11
+**Domain:** Responsive CSS layout, vanilla JS UI polish, mobile UX patterns
+**Confidence:** HIGH
+
+## Summary
+
+This phase is a frontend-only CSS/HTML/JS polish pass on the existing `static/index.html`, `static/style.css`, and `static/app.js` files. No new backend capabilities, no new WebSocket messages, no new libraries or dependencies. All 18 user decisions (D-01 through D-18) are locked and specific — the research job is to verify that the specified CSS/JS techniques are well-supported across modern mobile browsers, and to identify the existing code integration points that the planner must reference.
+
+**Primary recommendation:** Implement all changes as CSS-only responsive adaptations under a single `@media (max-width: 480px)` breakpoint, extend the existing `showToast()` with a tier parameter, and add arrow-button reorder with optimistic DOM swaps following the existing vote-toggle pattern. All needed CSS features (`-webkit-line-clamp`, flexbox, CSS transitions, `@keyframes`) have 95%+ global browser support — no polyfills or fallbacks needed.
+
+## Architectural Responsibility Map
+
+| Capability | Primary Tier | Secondary Tier | Rationale |
+|------------|-------------|----------------|-----------|
+| Mobile responsive layout | Browser / Client | — | Pure CSS `@media` queries — no server involvement |
+| Progress bar + time display | Browser / Client | — | Client-side `width` CSS update driven by existing `server.time_update` WebSocket data |
+| Loading/placeholder states | Browser / Client | — | Client-side DOM manipulation on `client.add`; morphs on `server.queue_update` or `server.error` |
+| Toast feedback system | Browser / Client | — | Extends existing client-side `showToast()`; no server changes needed |
+| Touch reorder (↑/↓ arrows) | Browser / Client | — | Optimistic DOM swap + `client.reorder` WebSocket message (already exists) |
+| Disconnection error panel | Browser / Client | — | Client-side visibility toggle on `ws.onclose` / `ws.onopen` |
+
+All capabilities are Browser/Client tier. No server, database, or CDN involvement in this phase.
+
+
+## User Constraints (from CONTEXT.md)
+
+### Locked Decisions
+
+- **D-01:** Queue items stack to 2-row layout at ≤480px. Row 1: track title (full width, wraps to 2 lines then truncates with ellipsis). Row 2: vote button + count badge + duration + remove button — all 44px touch targets, size unchanged from desktop.
+- **D-02:** Drag handle (`☰`) hidden on mobile (`display: none` at ≤480px). HTML5 drag-and-drop doesn't work on touch — reorder moves to dedicated ↑/↓ buttons.
+- **D-03:** Settings section stacks vertically at ≤480px — label/description above slider/control rather than side-by-side columns.
+- **D-04:** Single 480px breakpoint for all mobile adaptations. CSS-only (`@media (max-width: 480px)`), no JavaScript breakpoint detection.
+- **D-05:** Header gets minimal changes at ≤480px: h1 size 1.5rem (from 1.75rem), slightly tighter padding. Nickname row already wraps naturally.
+- **D-06:** Thin horizontal progress bar (4px height, `border-radius: 2px`) in Now Playing section. Fill uses `#e94560`, track uses `#1a1a2e` at 40% opacity. Smooth CSS `transition: width 0.3s ease`.
+- **D-07:** Time display below bar: "1:23 / 4:20" left, remaining "-2:57" right. Both Small (14px) typography. Elapsed updated ~1s from `server.time_update`.
+- **D-08:** Progress bar fill width via CSS `width: X%` where `X = (playback_time / total_duration) * 100`. CSS transition smooths between 1s updates.
+- **D-09:** yt-dlp resolution shows a placeholder `` with pulsing "Resolving..." text and shimmer animation. Add button disables during resolution.
+- **D-10:** Resolution failure converts placeholder to error state: red-tinted `` with error text + dismiss ✕ button. Simultaneously fires error toast.
+- **D-11:** Unified 3-tier toast system (success/error/info) reusing existing `#error-toast` element, extended with CSS classes.
+- **D-12:** Add `#2ecc71` (green) to color palette as a success-only color. Used exclusively for success toast backgrounds.
+- **D-13:** Toast fires for **every** user action that modifies state.
+- **D-14:** Connection error panel below Now Playing when WebSocket closed during reconnect backoff. Replaces bare "reconnecting..." text.
+- **D-15:** ↑/↓ arrow buttons (U+2191/U+2193, 44px touch targets, stacked vertically) on each queue item — visible only to host. Sends `client.reorder`.
+- **D-16:** Arrow buttons present on both desktop and mobile at all widths. On desktop, drag-and-drop remains available alongside arrows.
+- **D-17:** Arrows positioned in action column. Color `#666` default, `#e94560` on hover. 44px touch targets.
+- **D-18:** First queue item hides ↑, last queue item hides ↓. Buttons are `display: none` at edges, not disabled.
+
+### the agent's Discretion
+
+No "you decide" areas — all decisions were explicitly made.
+
+### Deferred Ideas (OUT OF SCOPE)
+
+None — discussion stayed within phase scope.
+
+
+
+## Phase Requirements
+
+| ID | Description | Research Support |
+|----|-------------|------------------|
+| AC-02 | Web UI is responsive and usable on mobile phone browsers | All CSS features used (`-webkit-line-clamp`, flexbox, CSS transitions, `@media`) have 95%+ global browser support. Single 480px breakpoint covers iPhone SE (320px) through iPhone 14 Plus (428px) with safe margins. 44px touch targets preserved at all widths. |
+
+
+## Standard Stack
+
+### Core
+| Library | Version | Purpose | Why Standard |
+|---------|---------|---------|--------------|
+| Vanilla CSS3 | N/A (browser-native) | Responsive layout, animations, transitions | No build step. The project already uses vanilla CSS. All needed features (`@media`, flexbox, `@keyframes`, `transition`, `-webkit-line-clamp`) are Baseline Widely Available. |
+| Vanilla JavaScript (ES5 compatible) | N/A (browser-native) | DOM manipulation, WebSocket handlers, toast system | The project already uses ES5-compatible JS (var declarations, no arrow functions, no template literals outside innerHTML). Consistent with Phase 1-3 style. |
+| Unicode Glyphs | N/A (font-native) | Icons for arrows (↑↓), dismiss (✕), info (ℹ) | No icon library needed. All glyphs are in system fonts. The project already uses this pattern (△/▲ vote, ☰ drag, ✕ remove, ⏸▶⏭ host controls, ✎ edit). |
+
+### Supporting
+No supporting libraries needed — this is a pure frontend polish phase.
+
+### Alternatives Considered
+| Instead of | Could Use | Tradeoff |
+|------------|-----------|----------|
+| Vanilla CSS + JS | Tailwind CSS | Would require build step, contradicts project's no-build-step simplicity. Overkill for a single-file stylesheet. |
+| Unicode glyphs | Font Awesome / SVG icons | Adds HTTP requests or inline bloat. Unicode glyphs are instantly available, already used in the project, and sufficient for all needed icons. |
+| CSS-only line-clamp | JavaScript text truncation | JS-based truncation requires re-measuring on resize, adds complexity. `-webkit-line-clamp` is well-supported (95.94% global) and CSS-only. |
+
+**Installation:**
+No installation needed. All work is in existing `static/` files served by the FastAPI server.
+
+### CSS Feature Verification
+
+All CSS features used in this phase were verified against caniuse.com and MDN (retrieved 2026-05-11):
+
+| CSS Feature | Global Support | Mobile Safari | Chrome Android | Verified |
+|-------------|---------------|---------------|----------------|----------|
+| `@media (max-width)` | 100% baseline | ✅ iOS 3.2+ | ✅ 2.1+ | [VERIFIED: caniuse.com] |
+| `display: flex` | 99.2% | ✅ iOS 7+ | ✅ 4.4+ | [VERIFIED: caniuse.com] |
+| `-webkit-line-clamp` | 95.94% | ✅ iOS 5+ | ✅ 2.3+ | [VERIFIED: caniuse.com] |
+| `@keyframes` | 98.3% | ✅ iOS 7+ | ✅ 4.4+ | [VERIFIED: caniuse.com] |
+| `transition` | 98.6% | ✅ iOS 7+ | ✅ 4.4+ | [VERIFIED: caniuse.com] |
+| `position: fixed` | 100% baseline | ✅ iOS 3.2+ | ✅ 2.1+ | [VERIFIED: caniuse.com] |
+
+No polyfills, vendor prefixes (beyond `-webkit-line-clamp`'s standard co-dependency), or fallbacks needed. The target mobile browsers (Safari iOS 15+, Chrome Android 100+) support all features natively.
+
+## Architecture Patterns
+
+### System Architecture Diagram
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ Browser (Client) │
+│ │
+│ ┌──────────────────────────────────────────────────────┐ │
+│ │ Static Files: index.html, style.css, app.js │ │
+│ │ │ │
+│ │ index.html │ │
+│ │ ├── Header (h1 + nickname row) │ │
+│ │ ├── #error-toast (fixed position, 3-tier) │ │
+│ │ ├── #now-playing (title, duration, progress bar, │ │
+│ │ │ host controls, skip vote row, progress time) │ │
+│ │ ├── #disconnect-panel (conditional, below Now │ │
+│ │ │ Playing) │ │
+│ │ ├── #queue-section (queue items with vote, arrows, │ │
+│ │ │ drag handle, title, duration, remove) │ │
+│ │ ├── #add-song (URL input + Add button) │ │
+│ │ ├── #share (QR code + URL) │ │
+│ │ ├── #host-settings (toggle, slider, session mgmt) │ │
+│ │ └── #stop-server-modal (conditional overlay) │ │
+│ └──────────────────────────────────────────────────────┘ │
+│ │ │
+│ WebSocket │
+│ │ │
+│ ┌──────────────────────────────────────────────────────┐ │
+│ │ Data Flow: │ │
+│ │ │ │
+│ │ Server → Client (inbound messages): │ │
+│ │ server.sync ──────→ renderFullState() │ │
+│ │ server.time_update → updateTime() (progress bar) │ │
+│ │ server.queue_update → applyQueueDelta() │ │
+│ │ server.playback_update → updatePlayback() │ │
+│ │ server.error ──────→ showToast(tier="error") │ │
+│ │ server.votes_update → update vote counts/badges │ │
+│ │ server.skip_update → update skip badge/state │ │
+│ │ server.queue_sort → re-render queue order │ │
+│ │ server.settings_update → applySettingsToUI() │ │
+│ │ server.session_saved → showToast(tier="success") │ │
+│ │ server.session_loaded → showToast(tier="success") │ │
+│ │ server.nickname_error → showToast(tier="error") │ │
+│ │ server.shutdown ───→ showToast(tier="error") │ │
+│ │ │ │
+│ │ Client → Server (outbound messages, unchanged): │ │
+│ │ client.add, client.vote, client.unvote, │ │
+│ │ client.skip_vote, client.reorder, client.remove, │ │
+│ │ client.host.*, client.nickname_set │ │
+│ └──────────────────────────────────────────────────────┘ │
+│ │
+│ ┌──────────────────────────────────────────────────────┐ │
+│ │ CSS Responsive Layer: │ │
+│ │ │ │
+│ │ Desktop (>480px): │ │
+│ │ Queue: single-row flex, all controls inline │ │
+│ │ Drag handle: visible, grab cursor │ │
+│ │ Settings: 2-col grid (label left, control right) │ │
+│ │ h1: 1.75rem │ │
+│ │ │ │
+│ │ Mobile (≤480px): │ │
+│ │ Queue: 2-row stacked (title row + actions row) │ │
+│ │ Drag handle: display: none │ │
+│ │ Settings: single col (label above control) │ │
+│ │ h1: 1.5rem │ │
+│ │ Arrow buttons: remain visible as sole reorder │ │
+│ └──────────────────────────────────────────────────────┘ │
+└─────────────────────────────────────────────────────────────┘
+ │
+ HTTP / WS
+ │
+┌─────────────────────────────────────────────────────────────┐
+│ FastAPI Server │
+│ (UNCHANGED — no new messages, no new endpoints) │
+│ │
+│ Serves: static/, /api/qrcode, /api/queue, /ws │
+│ Broadcasts: all existing server.* messages unchanged │
+└─────────────────────────────────────────────────────────────┘
+ │
+ mpv IPC socket
+ │
+┌─────────────────────────────────────────────────────────────┐
+│ mpv Process (UNCHANGED) │
+│ Separate OS process — unaffected by this phase │
+└─────────────────────────────────────────────────────────────┘
+```
+
+### Recommended Project Structure
+
+The phase only modifies existing files. No new files or directories:
+
+```
+static/
+├── index.html # Add: progress bar markup, disconnect panel, 2-row wrappers in queue items
+├── style.css # Add: @media (max-width: 480px) block, progress bar CSS, shimmer keyframes,
+│ # toast tier variants (.success/.error/.info), arrow button styles,
+│ # disconnect panel styles, button disabled state
+└── app.js # Modify: renderQueueItem() (arrow buttons + 2-row wrappers),
+ # updateTime() (progress bar + time labels),
+ # showToast() (tier parameter + durations),
+ # applyQueueDelta() (placeholder logic),
+ # add-form submit handler (placeholder render + button disable),
+ # connectWs() (disconnect panel visibility),
+ # All click handlers (add toast calls),
+ # queue-list click handler (arrow button delegated events)
+```
+
+### Pattern 1: Optimistic DOM Update with Server Confirmation
+
+**What:** The existing pattern (used for vote toggle, skip vote toggle): instantly update the DOM on user action, send WebSocket message, server confirms via broadcast. Arrow button reorder follows this exact same pattern.
+
+**When to use:** All user interactions in this phase: arrow reorder, vote toggle (already exists, add toast), skip vote (already exists, add toast), remove (already exists, add toast), settings changes (add toast), save/load session (add toast).
+
+**Example (arrow reorder, following existing vote pattern):**
+```javascript
+// Source: existing vote toggle pattern in app.js line 716-756
+// Arrow button click handler (NEW — follows same pattern):
+document.getElementById("queue-list").addEventListener("click", function (e) {
+ var arrowBtn = e.target.closest(".arrow-up, .arrow-down");
+ if (arrowBtn) {
+ var li = arrowBtn.closest("li");
+ var isUp = arrowBtn.classList.contains("arrow-up");
+
+ // Optimistic DOM swap
+ var targetLi = isUp ? li.previousElementSibling : li.nextElementSibling;
+ if (!targetLi) return;
+ if (isUp) {
+ li.parentNode.insertBefore(li, targetLi);
+ } else {
+ li.parentNode.insertBefore(targetLi, li);
+ }
+
+ // Build new order array from DOM
+ var items = document.querySelectorAll("#queue-list li[data-track-id]");
+ var newOrder = [];
+ items.forEach(function (item) {
+ newOrder.push(item.getAttribute("data-track-id"));
+ });
+
+ // Update arrow visibility for edges
+ updateArrowVisibility();
+
+ // Send to server
+ if (ws && ws.readyState === WebSocket.OPEN) {
+ ws.send(JSON.stringify({ type: "client.reorder", payload: { order: newOrder } }));
+ }
+
+ // Show toast
+ showToast("Queue reordered", "info");
+ }
+});
+```
+
+### Pattern 2: CSS-Only Responsive Breakpoint
+
+**What:** A single `@media (max-width: 480px)` block in `style.css` handles all mobile adaptations. No JavaScript detection, no class toggling, no `window.matchMedia`.
+
+**When to use:** All responsive behavior in this phase: 2-row queue layout, hidden drag handle, vertical settings stacking, h1 size reduction.
+
+**Example:**
+```css
+@media (max-width: 480px) {
+ /* Queue items become 2-row stacked */
+ #queue-list li {
+ flex-direction: column;
+ align-items: stretch;
+ gap: 6px;
+ padding: 12px 8px;
+ }
+
+ /* Title: 2-line wrap with ellipsis */
+ #queue-list li .track-title {
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: normal;
+ width: 100%;
+ }
+
+ /* Drag handle hidden */
+ .drag-handle {
+ display: none;
+ }
+
+ /* Settings stack vertically */
+ .settings-row {
+ flex-direction: column;
+ }
+}
+```
+
+### Pattern 3: Tiered Toast with Auto-Dismiss
+
+**What:** Extend the existing `showToast()` function to accept a tier parameter (`"success"`, `"error"`, `"info"`). Each tier has its own background color, auto-dismiss duration, and CSS class.
+
+**When to use:** Every user action that modifies state per D-13.
+
+**Example:**
+```javascript
+function showToast(message, tier) {
+ tier = tier || "error"; // Backward compatible — existing calls without tier default to error
+ var toast = document.getElementById("error-toast");
+ toast.textContent = message;
+ toast.className = ""; // Clear all classes
+ toast.classList.add(tier);
+ toast.style.display = "block";
+
+ clearTimeout(toast._timer);
+
+ var durations = { success: 2000, error: 5000, info: 3000 };
+ toast._timer = setTimeout(function () {
+ toast.style.display = "none";
+ }, durations[tier] || 5000);
+}
+```
+
+### Anti-Patterns to Avoid
+- **Over-engineering responsive breakpoints:** Don't add multiple breakpoints or JavaScript resize listeners. D-04 mandates a single `@media (max-width: 480px)` CSS-only approach. Adding `window.matchMedia` or `resize` event handlers contradicts the locked decision.
+- **Building a toast queue:** Don't implement toast stacking or queuing. The UI-SPEC says "only one toast visible at a time — new toast replaces the current one immediately (no queueing)."
+- **Using `disabled` attribute for hidden arrows:** D-18 says `display: none` at edges, never `disabled`. Don't show grayed-out arrow buttons at boundaries.
+
+## Don't Hand-Roll
+
+| Problem | Don't Build | Use Instead | Why |
+|---------|-------------|-------------|-----|
+| Multi-line text truncation | JavaScript text measurement + string truncation | `-webkit-line-clamp: 2` with `display: -webkit-box` | JS-based truncation requires re-measuring on every resize, layout shift, and font load. `-webkit-line-clamp` is declarative CSS with 95.94% global support, including all target mobile browsers. |
+| Progress bar animation | `requestAnimationFrame` loop for smooth animation | CSS `transition: width 0.3s ease` | `requestAnimationFrame` adds unnecessary JS overhead. The CSS transition naturally interpolates between `server.time_update` values arriving every ~1s. No need for client-side interpolation. |
+| Touch drag-and-drop | Custom touch event handlers (touchstart/touchmove/touchend) with hit-testing | Use ↑/↓ buttons for touch reorder instead | HTML5 drag-and-drop doesn't work on touch (D-02). Building a touch drag system from scratch involves coordinate tracking, scroll locking, ghost elements, and accessibility concerns. Arrow buttons are simpler, unambiguous, and consistent across desktop+mobile. |
+| Toast animation | JavaScript animation with `setInterval` | CSS `@keyframes slideIn` + `transition` | CSS animations are GPU-accelerated, don't block the main thread, and are trivially maintainable. The project already has a `slideIn` keyframe. |
+
+**Key insight:** The existing codebase already provides all the infrastructure needed. The progress bar is a CSS `width` update driven by data already arriving every ~1s. The toast system is an extension of an existing `showToast()` function. Arrow reorder follows the existing optimistic-DOM-update pattern. Almost nothing needs to be built from scratch — this phase is wiring existing infrastructure to new UI elements.
+
+## Common Pitfalls
+
+### Pitfall 1: `-webkit-line-clamp` without `-webkit-box-orient: vertical`
+
+**What goes wrong:** The text doesn't actually clamp — it flows to full height without truncation. The ellipsis appears but the text isn't hidden.
+
+**Why it happens:** `-webkit-line-clamp` only works as part of a specific combination: `display: -webkit-box` + `-webkit-box-orient: vertical`. Missing any one of these three properties silently fails (no error, just no clamping).
+
+**How to avoid:** Always use the three-property pattern together in the mobile title CSS:
+```css
+.queue-title {
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+}
+```
+**Warning signs:** Text runs to full height on mobile. Inspect computed styles — check that all three properties are present.
+
+### Pitfall 2: Toast Flicker on Rapid Actions
+
+**What goes wrong:** Multiple rapid actions (e.g., voting on several songs quickly) cause the toast to flicker as each new toast replaces the previous one mid-slide.
+
+**Why it happens:** The "replace immediately" behavior (D-11 design spec) means a new `showToast()` call clears the existing timeout and restarts the animation. With a 300ms slide-in animation and rapid actions, the toast never stabilizes.
+
+**How to avoid:** Accept this as intended behavior (the design spec explicitly says "new toast replaces the current one immediately"). Use short durations for frequent actions (success=2s, info=3s) so toasts don't linger. The flicker is mild because the toast content and position don't change — only the background color and text do.
+
+**Warning signs:** Users report motion discomfort. If necessary, add a 150ms minimum display time before a new toast can replace the current one. But don't implement a full queue — that contradicts the locked spec.
+
+### Pitfall 3: Arrow Button Order Array Desync
+
+**What goes wrong:** The optimistic DOM swap produces a different order than what the server computes (e.g., if another user's action arrives between the swap and the server's response).
+
+**Why it happens:** WebSocket is asynchronous. Between the optimistic DOM swap and the server processing `client.reorder`, the server may have processed another user's `client.vote` or `client.remove` that changed the queue. The server's `server.queue_sort` broadcast will have the true order.
+
+**How to avoid:** Follow the existing pattern from drag-and-drop: the client optimistically reorders, but always trusts the server's `server.queue_sort` broadcast when it arrives. If the server order matches the optimistic order (track IDs in same sequence), no DOM change. If mismatched, re-render to server order. The existing `handleWsMessage` for `server.queue_sort` (line 268-289) already handles this.
+
+**Warning signs:** Queue jumps after arrow clicks. This indicates the optimistic and server orders diverged. The server's order must always win.
+
+### Pitfall 4: Progress Bar Jumps at Track Change
+
+**What goes wrong:** When a track ends and the next begins, the progress bar jumps from ~100% back to 0%. The CSS `transition: width 0.3s ease` makes this visible as a brief slide-back.
+
+**Why it happens:** The server sends a `server.playback_update` with the new track (duration resets to 0), and the next `server.time_update` sets `width: 0%`. The CSS transition makes 100% → 0% visible as a smooth slide rather than an instant reset.
+
+**How to avoid:** Remove the transition temporarily during track change. When `updatePlayback()` fires for a new track (new `current_track`), temporarily set `transition: none` on `.progress-fill`, set `width: 0%`, then restore the transition on the next `updateTime()` call. Or use `requestAnimationFrame` to batch the transition removal/restoration.
+
+**Warning signs:** Progress bar slides backward after each track. Check that transitions are disabled during the reset frame.
+
+### Pitfall 5: Placeholder `
` Not Cleaned Up on Network Error Before Response
+
+**What goes wrong:** If the WebSocket disconnects between sending `client.add` and receiving the response, the placeholder `` remains in the queue indefinitely with no way to resolve.
+
+**Why it happens:** The placeholder is added optimistically before the WebSocket message is sent. If the connection drops before the server processes `client.add`, neither `server.queue_update` (success) nor `server.error` (failure) arrives to morph or remove the placeholder.
+
+**How to avoid:** In `ws.onclose`, scan the queue for `.queue-placeholder` elements and convert them to an error state ("Connection lost — try again"), or remove them. The placeholder represents an unconfirmed action that can never be resolved once the connection drops.
+
+**Warning signs:** A "Resolving..." placeholder persists through a reconnect cycle. Check `ws.onclose` cleanup logic.
+
+## Code Examples
+
+### Progress Bar Width Update
+
+```javascript
+// Source: existing updateTime() at app.js:200 — extended
+// [CITED: integration point from 04-CONTEXT.md code_context section]
+function updateTime(data) {
+ var t = data.payload.playback_time;
+ var total = data.payload.total_duration; // Server must include this (verify during implementation)
+
+ // Existing fallback text
+ document.getElementById("progress-time").textContent = formatSeconds(t);
+
+ // Progress bar fill
+ var fill = document.querySelector(".progress-fill");
+ var elapsedEl = document.querySelector(".time-elapsed");
+ var remainingEl = document.querySelector(".time-remaining");
+ var totalEl = document.querySelector(".time-total");
+
+ if (fill && total) {
+ var pct = Math.min(100, (t / total) * 100);
+ fill.style.width = pct + "%";
+ }
+
+ if (elapsedEl) elapsedEl.textContent = formatSeconds(t);
+ if (totalEl) totalEl.textContent = "/ " + formatSeconds(total);
+ if (remainingEl) remainingEl.textContent = "-" + formatSeconds(Math.max(0, total - t));
+}
+```
+
+### Shimmer Animation CSS
+
+```css
+/* Source: MDN @keyframes docs [VERIFIED: developer.mozilla.org] */
+/* [VERIFIED: 04-UI-SPEC.md animation inventory] */
+@keyframes shimmer {
+ 0% { opacity: 0.4; }
+ 50% { opacity: 0.8; }
+ 100% { opacity: 0.4; }
+}
+
+.queue-placeholder.resolving {
+ animation: shimmer 1.2s ease-in-out infinite;
+ background: rgba(26, 26, 46, 0.6);
+ color: #888;
+ cursor: default;
+}
+
+.queue-placeholder.error {
+ animation: none;
+ background: rgba(233, 69, 96, 0.1);
+ color: #e94560;
+}
+```
+
+### Toast Tier CSS Variants
+
+```css
+/* Source: existing #error-toast at style.css:181 — extended */
+/* [VERIFIED: 04-CONTEXT.md D-11, D-12] */
+#error-toast.success {
+ background: #2ecc71;
+ color: #ffffff;
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
+}
+
+#error-toast.error {
+ background: #e94560;
+ color: #ffffff;
+}
+
+#error-toast.info {
+ background: #0f3460;
+ color: #ffffff;
+}
+
+.toast-dismiss {
+ position: absolute;
+ right: 12px;
+ top: 50%;
+ transform: translateY(-50%);
+ min-width: 44px;
+ min-height: 44px;
+ background: transparent;
+ border: none;
+ color: rgba(255, 255, 255, 0.7);
+ font-size: 16px;
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.toast-dismiss:hover {
+ color: #ffffff;
+ opacity: 1;
+}
+```
+
+## State of the Art
+
+| Old Approach | Current Approach | When Changed | Impact |
+|--------------|------------------|--------------|--------|
+| Single-line queue item truncation (desktop-only) | 2-line wrap with CSS `-webkit-line-clamp` on mobile | Phase 4 (May 2026) | Song titles get readable multi-line display on narrow screens |
+| Bare "reconnecting..." text on disconnect | Structured disconnection panel with heading, body, and manual Reconnect button | Phase 4 (May 2026) | Users get clear guidance during connection loss instead of cryptic status text |
+| Error-only toast system | 3-tier toast (success/error/info) with tier-specific durations and colors | Phase 4 (May 2026) | All state mutations get visible confirmation; success vs error vs neutral are visually distinct |
+| HTML5 drag-and-drop only for reorder | Arrow ↑/↓ buttons as universal reorder alongside drag-and-drop on desktop | Phase 4 (May 2026) | Touch devices and keyboard users can reorder; desktop gets both methods |
+
+**Deprecated/outdated:**
+- HTML5 drag-and-drop as sole reorder mechanism: Replaced by arrow buttons as the universal method, with drag-and-drop remaining as a desktop bonus per D-16.
+- Bare `ws.onclose` "reconnecting..." text: Replaced by the structured disconnection panel per D-14.
+
+## Assumptions Log
+
+| # | Claim | Section | Risk if Wrong |
+|---|-------|---------|---------------|
+| A1 | `server.time_update` payload currently includes (or will be extended to include) `total_duration` — the progress bar calculation needs `playback_time / total_duration` | Code Examples | LOW risk — if total_duration is missing, it can be derived from the `#current-duration` data attribute or the now-playing track metadata. The server can easily add this field. |
+| A2 | The `formatSeconds()` helper handles `null`/`undefined` gracefully (returns "--:--") — progress bar time labels will use this same function | Code Examples | LOW risk — `formatSeconds` at line 128 already handles null → "--:--". Verified in existing code. |
+| A3 | The `#queue-list` click handler for arrow buttons can be added as a delegated event alongside the existing vote-btn and remove-btn handlers | Architecture Patterns | LOW risk — existing code at line 716 already uses delegated events on `#queue-list`. Adding arrow button logic to the same handler is a standard pattern. |
+| A4 | The `isHost` guard for rendering arrow buttons works the same as the existing host controls guard — both check the same `isHost` variable | Architecture Patterns | LOW risk — `isHost` is set in `renderFullState()` (line 103) and used for host controls rendering (line 119). Arrow buttons follow the same guard pattern. |
+
+## Open Questions
+
+1. **Does `server.time_update` include `total_duration` in its payload?**
+ - What we know: The `updateTime()` handler currently only accesses `data.payload.playback_time`. The progress bar needs total duration too.
+ - What's unclear: Whether the server already sends this field or needs a one-line addition.
+ - Recommendation: During implementation, check the actual `server.time_update` payload shape. If `total_duration` is absent, request a server-side addition (one field in the broadcast dict). Fallback: read from `data-track-duration` attribute on the now-playing element.
+
+2. **Should the `showToast()` tier parameter be a second argument or a config object?**
+ - What we know: All existing calls use `showToast(message)` with a single string argument. Adding a second argument maintains backward compatibility.
+ - What's unclear: Whether future phases will need more options (icon, action button, custom duration).
+ - Recommendation: Use a simple second argument `showToast(message, tier)`. Default to `"error"` for backward compatibility. This keeps the API simple while covering all current needs.
+
+3. **What happens to the progress bar when there is no current track?**
+ - What we know: D-06 says the progress bar lives in the Now Playing section. When queue is empty, there's no track.
+ - What's unclear: Should the bar be hidden entirely, show at 0%, or show a grayed-out state?
+ - Recommendation: Hide the progress bar and time labels entirely when `#current-title` shows "—" (no track). The `#progress-time` fallback continues showing "--:--". This avoids a meaningless empty progress bar.
+
+## Environment Availability
+
+Step 2.6: SKIPPED. No external dependencies identified. This phase is purely code/config changes in existing `static/` files — no tools, databases, services, CLIs, or package managers needed beyond what the project already uses (browser for rendering, FastAPI server for serving static files). The FastAPI server and mpv are already verified from Phases 1-3.
+
+## Sources
+
+### Primary (HIGH confidence)
+- [04-CONTEXT.md] — All 18 user decisions (D-01 through D-18), integration points, existing code patterns
+- [04-UI-SPEC.md] — Design system, spacing, typography, color palette, component inventory, interaction contracts, state inventory, animation inventory, layout specs, responsive behavior
+- [caniuse.com — CSS line-clamp] — Verified 95.94% global support, working on all mobile browsers since iOS 5+ and Chrome Android 2.3+ [VERIFIED: caniuse.com, fetched 2026-05-11]
+- [MDN Web Docs — line-clamp CSS property] — Confirmed three-property co-dependency: `display: -webkit-box` + `-webkit-box-orient: vertical` + `-webkit-line-clamp: N` [VERIFIED: developer.mozilla.org, last modified 2026-04-20]
+- [02-UI-SPEC.md] — Baseline design system (carry-forward). Colors, typography, spacing, animation inventory, layout specs
+- [Existing code: static/index.html, style.css, app.js] — Verified integration points match CONTEXT.md code_context claims [VERIFIED: codebase read]
+
+### Secondary (MEDIUM confidence)
+- [MDN Web Docs — display CSS property] — General reference for flex layout patterns [CITED: developer.mozilla.org, last modified 2026-04-20]
+
+### Tertiary (LOW confidence)
+None — all claims verified against caniuse.com, MDN, existing codebase, or explicit CONTEXT.md decisions.
+
+## Metadata
+
+**Confidence breakdown:**
+- Standard stack: HIGH — No libraries to version-check. All CSS features verified against caniuse.com and MDN (retrieved within 24h). Feature compatibility is stable (these are mature Baseline features).
+- Architecture: HIGH — The integration points are documented explicitly in 04-CONTEXT.md, verified against existing code. Line numbers match. The patterns (optimistic UI, CSS-only breakpoint, delegated events) are already used in the existing codebase.
+- Pitfalls: HIGH — Derived from official docs (MDN line-clamp docs), CSS spec analysis (transition interference at track boundary), and the existing code patterns (order array desync from drag-and-drop precedent).
+
+**Research date:** 2026-05-11
+**Valid until:** 2026-06-11 (30 days — CSS feature compatibility is stable; no fast-moving libraries)