+ );
+}
+```
+
+- [ ] **Step 2: Verify icon imports resolve**
+
+```
+cd D:/Programmieren/ChatApp-Electron/chat-app
+grep -n "export const PhoneIcon\|export function PhoneIcon\|export const SpinnerIcon\|export function SpinnerIcon\|export const XIcon\|export function XIcon" apps/desktop/src/components/icons.tsx
+```
+
+Expected: all three icons exported. (Earlier tasks confirmed `PhoneIcon`, `SpinnerIcon`; `XIcon` is also used by other modals — verify.) If `XIcon` is missing under that name, swap the import to whatever the close-icon export is in `icons.tsx` (e.g. `CloseIcon`) and report the substitution.
+
+- [ ] **Step 3: Typecheck**
+
+```
+pnpm --filter @chat-app/desktop typecheck 2>&1 | tail -10
+```
+
+Expected: zero errors. (After the encryption-UX rollout the desktop typecheck is clean.)
+
+- [ ] **Step 4: Commit**
+
+```bash
+cd D:/Programmieren/ChatApp-Electron/chat-app
+git add apps/desktop/src/components/CallPreviewPanel.tsx
+git commit -m "feat(desktop): add CallPreviewPanel — Discord-DM-style join surface"
+```
+
+---
+
+## Task 2: Wire the new panel in `ConversationPage.tsx` and delete the old rail
+
+**Files:**
+- Modify: `apps/desktop/src/pages/ConversationPage.tsx`
+- Delete: `apps/desktop/src/components/VoiceChannelRail.tsx`
+
+- [ ] **Step 1: Swap the import**
+
+In `apps/desktop/src/pages/ConversationPage.tsx`, replace this line (around line 26):
+
+```ts
+import { VoiceChannelRail } from '../components/VoiceChannelRail';
+```
+
+with:
+
+```ts
+import { CallPreviewPanel } from '../components/CallPreviewPanel';
+```
+
+- [ ] **Step 2: Swap the JSX usage**
+
+In the same file, replace the JSX line (around line 657):
+
+```tsx
+{conversation && !incomingHere && }
+```
+
+with:
+
+```tsx
+{conversation && !incomingHere && }
+```
+
+- [ ] **Step 3: Delete the dead component**
+
+```
+rm apps/desktop/src/components/VoiceChannelRail.tsx
+```
+
+- [ ] **Step 4: Sweep for stragglers**
+
+```
+grep -rn "VoiceChannelRail" apps/desktop/src
+```
+
+Expected: no hits. If anything remains, clean it up.
+
+- [ ] **Step 5: Typecheck**
+
+```
+pnpm --filter @chat-app/desktop typecheck 2>&1 | tail -10
+```
+
+Expected: zero errors.
+
+- [ ] **Step 6: Build the desktop renderer once to surface any tailwind/runtime issues**
+
+```
+pnpm --filter @chat-app/desktop build 2>&1 | tail -15
+```
+
+Expected: build succeeds. Tailwind classes used here (`bg-surface-3/70`, `border-line`, `bg-emerald-600`, `bg-surface-2`, `text-fg`, `text-fg-muted`, `aspect-square`, `line-clamp-1`, `grid-cols-2 md:grid-cols-3`) are all already used elsewhere in the desktop app and should be in the existing tailwind config.
+
+- [ ] **Step 7: Commit**
+
+```bash
+cd D:/Programmieren/ChatApp-Electron/chat-app
+git add apps/desktop/src/pages/ConversationPage.tsx
+git rm apps/desktop/src/components/VoiceChannelRail.tsx
+git commit -m "refactor(desktop): replace VoiceChannelRail with CallPreviewPanel
+
+Drops the always-on 'Sprach-Channel' banner. The preview panel renders
+only when peers are in the active call (1:1 and group identical).
+Calls are still started via the topbar phone icon."
+```
+
+---
+
+## Task 3: Manual smoke pass (USER)
+
+This task is for the human operator after the previous two land. Run a fresh dev build and walk through the spec's smoke checklist:
+
+- [ ] Open a 1:1 chat with no active call → panel not rendered.
+- [ ] Open a group chat with no active call → panel not rendered (regression test for the bug).
+- [ ] Have a peer start a call → panel appears, peer's avatar tile + "Beitreten" visible.
+- [ ] Click "Beitreten" → joins the call; panel disappears (you're now `iAmIn`).
+- [ ] Hang up → panel reappears with peer still in.
+- [ ] Peer hangs up too → panel disappears.
+- [ ] Resize the window narrow → grid collapses to 2 columns.
+- [ ] Group call with 9 participants → 7 avatar tiles + one `+2` overflow tile.
+- [ ] Click the `✕` in the panel header → tiles collapse, header stays. Click again → tiles re-expand.
+
+If any step fails, capture the exact behavior and reopen the relevant task.
+
+---
+
+## Self-Review
+
+**1. Spec coverage:**
+- Visibility rule (`!iAmIn && others.length > 0`, no `|| isGroup`) — Task 1.
+- Discord-DM tile layout, ~120×120px tiles, 2/3-column responsive grid, `+N` overflow at 8 — Task 1.
+- "Aktiver Anruf · n im Channel" header reusing `app:call.active_in_conv` — Task 1.
+- Centered "Beitreten" CTA, disabled when busy, `SpinnerIcon` while connecting — Task 1.
+- Optional collapse via `useState`, default expanded — Task 1.
+- Mount point unchanged (`!incomingHere &&` gate retained) — Task 2.
+- Old empty state and `Channel öffnen` removed by deleting the rail — Task 2.
+- Manual smoke list — Task 3.
+
+**2. Placeholder scan:** Clean. Every code block is complete; every command has expected output; the only `defaultValue:` strings are in the i18n calls (not placeholders, just fallback copy).
+
+**3. Type consistency:** `Props { conversation: ConversationSummary }` matches the type used by the deleted rail and consumed by `ConversationPage.tsx`. `useCallPresence` returns `string[]` per `apps/desktop/src/lib/useCallPresence.ts`. `useCall().state.kind` values (`idle`, `connecting`, `connected`, `reconnecting`) match the existing `CallContext` discriminated union. `joinActiveCall(convId, 'audio')` matches the existing `useCall` API.
+
+No gaps; plan is complete.