Both duel modes still ended a game the old way: PvE popped a blocking modal over the finished board, PvP swapped in an inline overlay with its own separate markup, and neither looked like the [inline recap](/posts/the-score-that-almost-got-away) the Daily Challenge had just gotten. A wayfinder map, [duel-mode end-of-game recap](https://github.com/ken-guru/the-words-are-snake/issues/605), charted the fix from research through a three-way prototype before any real component got touched.

Three variants went up behind a dev-only harness that could flip between PvE and PvP mock contexts, and between a normal collision ending and a PvP forfeit: an image-first inline card, a scoreboard-first layout with no image, and a compact version of today's blocking overlay restyled to match. Image-first won outright, reviewed across both modes and both ending types. The blocking overlay went away entirely; a duel game now ends the same way the daily one does, an `aria-live` header replacing the board and controls in place rather than covering them.

One `DuelRecap.svelte` serves both modes, taking the viewer-perspective game state plus a small envelope describing what differs between them:

```typescript
{ mode: 'pve' | 'pvp', playerName?, opponentName?, outcome, reasonText?, language, forfeit }
```

PvE keeps its familiar You/Them labels until the player submits initials, then the image itself re-renders with the submitted name in place of You. PvP already has real names on both sides, so it skips the initials form entirely and shows a rematch hint and leaderboard link instead; a forfeit ending draws the board with its own caption rather than pretending a collision happened, and the winner is always read from the server-authoritative viewer state, never recomputed from the engine.

The share image is a sibling to the daily one rather than a rewrite: `renderDuelShareImage` draws from the same board-painting primitives `screenshot.ts` already had, extended to place both snakes side by side with a two-player score panel. Pulling those primitives out surfaced a second bug along the way, the player and opponent snake colours in every share image had drifted from the live board's actual palette:

```typescript
export const SNAKE_PALETTES: Record<'player' | 'opponent', SnakePalette> = {
  player:   { head: '#C8203C', body: '#1B7D92' },
  opponent: { head: '#CC6000', body: '#5A3000' }
};
```

Re-syncing it here fixed the daily share image too, since both renderers now read from the one constant. The share button itself picked up a `ref` parameter, `duel-pve-score` and `duel-pvp-score` alongside the daily image's own value, so a later look at referral traffic can tell which mode actually brought a new player in.