---
name: scanner-desktop-layout
classification: feature
success_metric: |
  On md+ viewports, /scanner keeps the desktop app chrome (sidebar +
  top bar), shows a framed camera workstation with a real webcam
  device picker, Upload Image, Batch Scan, Auto-detect, Scanner
  Tips, a live match inspector, and a bottom strip with Recent
  Scans / Scan Queue / Duplicates — without regressing the mobile
  immersive checkout.
skip: []
status: open
created: 2026-08-15
depends_on:
  - scanner-mobile-checkout
  - scanner-rebuild
model_policy:
  default_session: auto
  roles:
    role-conductor: composer-2.5-fast
    role-architect: composer-2.5
    role-ia-architect: composer-2.5-fast
    role-ux-reviewer: composer-2.5-fast
    role-ui-designer: composer-2.5-fast
    role-implementer: composer-2.5-fast
    role-reviewer: cursor-grok-4.5-high
    role-security-auditor: gpt-5.6-terra-medium
    role-design-system-auditor: cursor-grok-4.5-high
    role-a11y-auditor: cursor-grok-4.5-high
    role-doc-writer: auto
  escalate_to: claude-sonnet-5-thinking-medium
  escalate_to_premium: claude-4.6-opus-high-thinking
  never_premium:
    - role-reviewer
    - role-security-auditor
    - role-design-system-auditor
    - role-a11y-auditor
    - role-ui-designer
    - role-doc-writer
design_direction:
  source: role-ui-designer
  skill: ui-ux-pro-max
  skill_version: "2.5.0"
  version: 1
  locked_at: 2026-08-15
  product_type: desktop trading card scanner workstation
  pattern: Feature-Rich Showcase (workstation variant)
  style: Liquid Glass / glassmorphism
  stack: nextjs
  layout_reference: image-1eeebe23-9983-4a96-a3e7-4e3cdfdceb5b.png
---

# Convoy: scanner-desktop-layout

Give `/scanner` a dedicated desktop workstation layout from the
attached dark/light mock (camera + live result + history strip),
while leaving the shipped mobile immersive checkout alone.

Worktree: `tcg-vault-worktrees/scanner-desktop-layout` on
`convoy/scanner-desktop-layout` (branched from `origin/main` @
`0d52858`). Layout reference:
`image-1eeebe23-9983-4a96-a3e7-4e3cdfdceb5b.png`.

Do **not** land this on `dashboard-home-realignment` — that convoy
owns sidebar IA + top-bar chrome. Do **not** land this on
`scanner-identify-upgrade` — that epic owns detect/identify accuracy.

## Why

`scanner-mobile-checkout` shipped the phone job: full-bleed camera,
local cart, checkout sheet. Desktop (`md+`) got the leftover
composition — the same camera chrome plus a 360px cart side panel
(`ScannerReview` `variant="side-panel"`). That is not a desk
workstation.

On a laptop the user wants to see the webcam, inspect the current
match (set, rarity, number, condition, foil, confidence), decide
what to do with it, and keep a history/queue in view — without
losing the app sidebar or search bar. The mock is that layout.
Today they get a phone overlay stretched into a column.

## Scope

### In scope

- **Desktop-only composition (`md+`).** Keep Layout sidebar +
  TopSearchBar. Stop treating desktop as an immersive camera page
  with a bolted-on cart. Mobile (`max-md`) stays
  `chrome="immersive"` + checkout sheet.
- **Framed camera viewport.** Large live feed with ember corner
  brackets (already in `ScannerCamera`), Auto-detect status, and
  desk controls under the frame. Camera is a panel in the page,
  not a full-bleed overlay.
- **Real webcam device picker.** `enumerateDevices` + `deviceId`
  in `useCameraScanner` (not facing-mode swap relabeled). Persist
  the last-used device for the tab if cheap. Empty-list / denied-
  permission fallback. Mobile keeps the existing facing-mode
  toggle — do not replace the phone chrome with a device `<select>`.
- **Upload Image + Batch Scan.** Upload Image stays single-file
  (existing gallery path). **Batch Scan** is a multi-file picker
  that runs each image through `identifyFromGalleryFile`
  **sequentially** (respect existing identify rate limits; no
  parallel Gemini storm, no new batch API). Progress and failures
  show in Scan Queue. Live multi-card on the webcam stays
  Auto-detect — do not build a new pile-in-one-frame detector.
- **Scanner Tips.** Header control from the mock. Glass popover
  or modal (use `<Modal>` / `GlassSurface`, no custom scrim) with
  short lighting / framing / foil / auto-detect guidance. No new
  route. Copy authored in this convoy (IA + UI Designer).
- **Live match inspector (right rail).** Current identify result:
  thumbnail, name, set, rarity, collector number, condition,
  foil, confidence, primary add, Rescan. This replaces the cart
  list as the primary right-hand surface.
- **Bottom history / queue strip.** Three tabs over the existing
  cart + ownership model: **Recent Scans** (session history),
  **Scan Queue** (uncommitted cart), **Duplicates** (already in
  My Collection via `ownershipMap`, and/or same-session
  name+set repeats from `mergeScannedCardEntry`). Badge counts
  on Queue and Duplicates. Clear + view-all as IA/UX refine.
- **Reuse, don't rewrite, the scan engine.** Same hooks:
  `useCameraScanner`, `useScannerIdentification`, `useScannerQueue`,
  `scanner-session`. Identify / OCR / Gemini stay out.

### Out of scope

- Identify / detection / Gemini (`lib/scanner-card-identify.js`,
  `pages/api/scan/identify.js`, OpenCV). Sibling:
  `scanner-identify-upgrade`.
- Mobile immersive checkout, scan peek, or checkout sheet — no
  visual or interaction regression below `md`.
- Layout nav IA, Daily Ember, notifications, profile cluster.
  Sibling: `dashboard-home-realignment`. Mock items Wishlist /
  Trades / Market / Events / Binders are **not** product nav.
- Wishlist as a feature (no table / API). Mock "Save to Wishlist"
  maps to **Add to List** or drops — IA decides.
- A new identify / batch-OCR API, parallel Gemini calls, or a
  pile-in-one-frame detector. Batch Scan is multi-file sequential
  identify on the existing path.
- Schema / new tables. Cart remains client session state.
- Changing the homepage visual-diff baseline except as a
  side-effect of `/scanner` desktop chrome (scanner surfaces only).

### Product-vocab lock (from mock → Deck Hearth)

| Mock copy | Ship as |
| --- | --- |
| Add to Collection | `VOCAB.ADD_TO_MY_COLLECTION` |
| Save to Wishlist | Add to List, or omit |
| Binders (nav) | Lists — not this convoy |
| Collection (nav) | My Collection — not this convoy |

## Roles invoked

1. `role-ia-architect` — desktop flow vs mobile cart; inspector
   vs queue commit; Duplicates membership (`ownershipMap` vs
   session repeat); Batch Scan progress / failure; Tips content
   outline; device-picker empty/denied states.
2. `role-ui-designer` — lock the md+ workstation against the
   attached mock using Liquid Glass tokens (`ui-ux-pro-max` is
   installed), including Tips popover, device `<select>`, Batch
   Scan progress, and the three-tab strip. Do **not** skip
   `ui-design`.
3. `role-ux-reviewer` — inspect-then-add vs scan-all-then-checkout
   on desktop; Auto-detect off; Rescan; empty inspector; leave
   with an uncommitted queue; sequential batch cancel; duplicate
   tab actions (increment qty vs skip vs still add).
4. `role-architect` — briefs. Likely: (1) page composition +
   Layout chrome split, (2) camera panel + `deviceId` picker,
   (3) result inspector, (4) history/queue/duplicates strip,
   (5) batch multi-file identify + Tips. `slice_dependencies`
   must mark what can run in parallel.
5. `role-implementer` — per brief.
6. Audit fan-out: reviewer + security-auditor + design-system-auditor
   + a11y-auditor.

## Todos

- [x] IA: desktop screen inventory + inspector-add vs cart-commit;
      Duplicates membership; Batch Scan + Tips content
- [x] UI Designer: lock md+ workstation (tokens, not hex) from
      the attached mock; light + dark; Tips, device picker,
      batch progress, three-tab strip
- [x] UX: Auto-detect off, Rescan, empty state, leave-with-queue,
      keyboard on desk controls, batch cancel, duplicate actions
- [x] Architect: briefs + `slice_dependencies`; confirm Layout
      is `chrome="default"` on md+ only
- [ ] Desktop composition in `pages/scanner.js` (do not hide
      sidebar / top bar at md+)
- [ ] Camera as a framed panel; `deviceId` picker + Upload +
      Batch Scan + Auto-detect; hide mobile overlay chrome at md+
- [ ] Live match inspector wired to the latest unprocessed
      identify (condition / foil already on the cart entry)
- [ ] Bottom strip: Recent Scans + Scan Queue + Duplicates over
      `useScannerQueue` / `ownershipMap` / `scanner-session`
- [ ] Scanner Tips popover/modal with convoy-authored copy
- [ ] Sequential multi-file Batch Scan through
      `identifyFromGalleryFile` (cancellable, queue progress)
- [ ] Tests for desktop composition (inspector + queue +
      duplicates + batch enqueue, no mobile sheet) and
      no-regression on checkout sheet at `max-width: 767px`
- [ ] Visual-diff: desktop `/scanner` surface; refresh Linux
      baselines if the page is in the visual suite

## What exists today (conductor survey)

`/scanner` is one route, two compositions, one engine.

| Layer | Files | Today |
| --- | --- | --- |
| Page | `pages/scanner.js` | Auth gate; `Layout chrome="immersive"` on **all** viewports; camera column + `md:` 360px `ScannerReview` cart; mobile-only `ScannerCheckoutSheet` |
| Layout | `components/Layout.js` | Immersive hides **mobile** nav + top bar (`max-md` only). Desktop sidebar + TopSearchBar already stay visible. |
| Camera chrome | `components/scanner/ScannerCamera.js` | Full-bleed video, overlay top bar (back / title / gallery), bottom bar (flash / facing / status / Review N), scan peek, disambiguation. Same chrome on desktop. |
| Cart | `lib/use-scanner-queue.js`, `lib/scanner-session.js` | Identify enqueues locally (`processed: false`). Commit via `commitSelectedToOwned` / `commitSelectedToCollection`. `sessionStorage` persist. `addSingleCardToOwned` already exists. |
| Identify | `lib/use-scanner-identification.js`, `lib/use-camera-scanner.js` | Facing-mode swap only — **no** `deviceId` / `enumerateDevices`. Auto-detect is always on unless `verificationPausedRef` (checkout / list picker / disambiguation). |
| Review | `components/scanner/ScannerReview.js` | Thin wrapper: desktop side panel titled "Cart" that mounts `ScannerCheckoutContent`. |

Mobile checkout decisions that still apply unless IA overturns them
for desktop only: stay on camera after commit (D1), skip Setup
(D3), My Collection + List only (D4), gallery in-scope (D5),
cart in `sessionStorage` (D7).

## Conductor notes (build shape)

Likely file ownership for Architect to refine:

| Area | Files |
| --- | --- |
| Viewport split | `pages/scanner.js` — `chrome` default on md+, immersive on mobile; desktop grid vs mobile overlay |
| Camera panel | `components/scanner/ScannerCamera.js` (desktop variant or `variant="workstation"`), `lib/use-camera-scanner.js` (`enumerateDevices` + `deviceId`) |
| Inspector | new `components/scanner/ScannerResultPanel.js` — latest cart entry + condition/foil + add/rescan |
| History strip | new `components/scanner/ScannerHistoryStrip.js` — Recent / Queue / Duplicates over `queue.scannedCards` + `ownershipMap` |
| Batch Scan | `lib/use-scanner-identification.js` (`identifyFromGalleryFile` loop), queue progress UI |
| Tips | new `components/scanner/ScannerTips.js` — `<Modal>` or popover, convoy copy |
| Cart reuse | `lib/use-scanner-queue.js`, `lib/scanner-session.js`, `ScannerCheckoutSheet.js` (mobile only) |
| Copy | `lib/collection-vocabulary.js` |

Do not rewrite identification. Prefer a desktop layout shell that
**hides** mobile overlay chrome at `md+` rather than forking the
camera hook.

## Decisions (post-conductor)

Locked 2026-08-15 from the parent session. IA / UX / Architect
treat these as settled.

| # | Decision |
| --- | --- |
| C1 | **Batch Scan is in.** Multi-file sequential identify via the existing gallery path. No new batch API, no parallel Gemini, no new pile detector. |
| C2 | **Duplicates tab is in.** Strip tab with a badge. Membership = already-owned (`ownershipMap`) and/or same-session name+set repeats. IA picks the exact rule and tab actions. |
| C3 | **Scanner Tips is in.** Header control → glass popover/modal. Copy in this convoy. |
| C4 | **Real webcam device picker is in.** `enumerateDevices` + `deviceId` on desktop. Mobile keeps facing-mode swap. |
| C5 | **Inspector can commit this card now** *and* the queue strip remains for multi-add (same cart, two commit surfaces). Overturn only if IA finds a conflict. |

## Open questions (IA / product)

1. **Duplicates membership + actions.** Owned-in-collection only,
   session repeats only, or both? From the tab, can the user still
   add (increment qty), skip, or jump the inspector to that row?
2. **Wishlist.** Out as a feature. Confirm "Save to Wishlist" →
   Add to List on the inspector, or omit the third action.
3. **Auto-detect toggle.** User-facing pause of identification
   (extend `verificationPausedRef`), or just a status badge?
4. **Batch Scan cancel / errors.** Mid-batch cancel: keep already-
   identified rows? Per-file failure: continue the rest and flag
   the row, or stop?
5. **Tips content.** Four or five short tips (lighting, frame the
   card, foil glare, hold still, auto-detect). IA drafts; UI
   Designer locks the surface.

## Multitask dispatch

Planning is serial: IA → UI Designer → UX → Architect.

After architect: implementer fan-out only if briefs have
`depends_on: []` and disjoint `files:`. Device picker
(`use-camera-scanner.js`) and Tips (`ScannerTips.js`) are the
best candidates to parallelize with the inspector if they do
not both own `pages/scanner.js`. Page composition likely
blocks the strip and Batch Scan wiring.

After PR draft: `/multitask` audit fan-out
`role-reviewer + role-security-auditor + role-design-system-auditor + role-a11y-auditor`
(group id: `audit-scanner-desktop-layout-<pr>`).

## IA

### Affected routes

- `/scanner` — **[modified]** Single route, two viewport compositions. Desktop (`md+`) switches to `Layout chrome="default"` (sidebar + TopSearchBar visible), framed camera workstation, live match inspector (right rail), and bottom history strip. Mobile (`max-md`) stays `chrome="immersive"` with checkout sheet — no regression.
- `/login` — **[impacted]** Existing `returnUrl=/scanner` auth gate unchanged; desktop users land on the workstation after sign-in.
- `/collections`, `/my-cards` — **[impacted]** Post-commit navigation targets only (Add to List picker, success flows). No route or nav IA changes in this convoy.

No new routes. No API route changes.

### User flow

```mermaid
flowchart LR
  A["/scanner (auth)"] --> B{"md+?"}
  B -->|Yes| C["Workstation"]
  B -->|No| D["Immersive mobile"]
  C --> E["Scan / Upload / Batch"]
  E --> F["Match inspector"]
  F --> G["Add or queue"]
  C --> H["Strip tabs"]
  H --> F
```

Desktop path: user opens `/scanner` with app chrome → scans via webcam, single Upload Image, or Batch Scan (sequential gallery identify) → latest match appears in the right-rail inspector → commits one card via inspector **or** batches via Scan Queue strip → Duplicates tab surfaces owned + session-repeat rows for review/increment. Mobile path unchanged: full-bleed camera → checkout sheet.

### Screen inventory

| Screen | Path | New/modified | Notes |
| --- | --- | --- | --- |
| Scanner Desktop Workstation | `/scanner` | modified | `md+` grid: framed camera panel (device picker, Upload, Batch Scan, Auto-detect toggle, Tips), right-rail inspector, bottom strip. Replaces 360px cart side panel as primary right-hand surface. |
| Scanner Mobile Immersive | `/scanner` | impacted (no regression) | `max-md`: `chrome="immersive"`, overlay camera chrome, `ScannerCheckoutSheet`. D1/D3/D4/D5/D7 decisions preserved. |
| Live Match Inspector | `/scanner` | new (sub-surface) | Right rail on desktop. Shows latest unprocessed identify: thumbnail, name, set, rarity, collector #, condition, foil, confidence. Actions: `VOCAB.ADD_TO_MY_COLLECTION`, `VOCAB.ADD_TO_LIST`, Rescan. Single-card commit without opening checkout sheet. |
| History / Queue Strip | `/scanner` | new (sub-surface) | Bottom strip on desktop. Tabs: **Recent Scans** (session history), **Scan Queue** (uncommitted cart, badge = unprocessed count), **Duplicates** (badge = duplicate row count). Row click focuses card in inspector. |
| Scanner Tips | `/scanner` | new (sub-surface) | Header control → glass `<Modal>` or popover. Five convoy-authored tips (see Content deltas). No route change. |
| List Picker | `/scanner` | impacted | Existing "Choose a List" `<Modal>`. Opened from inspector `VOCAB.ADD_TO_LIST` on desktop (and unchanged on mobile). |
| Leave Scanner | `/scanner` | impacted | Existing leave-with-uncommitted-queue modal. Applies to both viewports when navigating away with queue items. |

### Content / data model deltas

**Copy (ship from `lib/collection-vocabulary.js`):**

- Primary add: `VOCAB.ADD_TO_MY_COLLECTION` ("Add to My Collection").
- Secondary add: `VOCAB.ADD_TO_LIST` ("Add to List") — **not** "Save to Wishlist" (feature omitted).
- Strip tab labels: "Recent Scans", "Scan Queue", "Duplicates".
- Camera controls: "Upload Image", "Batch Scan", "Auto-detect" (toggle + status on/off), "Scanner Tips", "Rescan".
- Device picker: "Camera" or "Webcam" `<select>` label; empty/denied fallback copy (UI Designer).

**Scanner Tips content (draft for UI Designer):**

1. Good lighting — avoid glare on foil cards.
2. Fill the frame with one card; keep corners visible.
3. Hold still until Auto-detect locks the match.
4. Use Batch Scan for a pile of photos from your gallery.
5. Switch webcam if the image is dark or mirrored.

**No schema changes.** Cart remains client `sessionStorage` via `lib/scanner-session.js`. Duplicates derive from existing `ownershipMap` (already in My Collection) and `mergeScannedCardEntry` (same-session name+set repeats) — no new tables or API fields.

**IA decisions (locked — formerly open questions):**

| Topic | Decision |
| --- | --- |
| Duplicates membership | **Both** sources: `ownershipMap` (already-owned) **and** same-session name+set repeats via `mergeScannedCardEntry`. Badge = count of rows in the Duplicates set. Row click focuses that card in the inspector. User can still add (increment qty) from inspector or queue. |
| Wishlist | **Omit.** Inspector third action is `VOCAB.ADD_TO_LIST`, not wishlist. |
| Auto-detect | **Real pause toggle** on desktop. Extends `verificationPausedRef` (user off = paused identify). Status badge reflects on/off. Mobile behavior unchanged unless UX specifies otherwise. |
| Batch cancel / errors | Mid-batch **cancel keeps** already-identified rows. Per-file failure **continues** the rest and flags that row in Scan Queue — do not stop the batch. |
| Inspector vs queue commit | **Both surfaces** (C5): inspector commits one card now; Scan Queue strip handles multi-add / bulk commit. Same cart, two commit paths. |
| Webcam picker | Desktop only: `enumerateDevices` + `deviceId`. Mobile keeps facing-mode swap (C4). |

### Open IA questions

None — all five conductor questions resolved above.

## Design direction

Locked v1 against `image-1eeebe23-9983-4a96-a3e7-4e3cdfdceb5b.png`
(dark + light side-by-side). **Incremental redesign** of an existing
Liquid Glass surface — not a new palette. Generator run:
`desktop trading card scanner workstation camera inspector queue
glassmorphism` (`ui-ux-pro-max` v2.5.0).

### Summary

On `md+`, `/scanner` is a **desk workstation** inside normal app
chrome (sidebar + TopSearchBar): a framed live-camera panel with
ember corner brackets, a right-rail **live match inspector**, and a
bottom **history / queue strip** — inspect one card, commit from the
rail, or batch from the strip. Mobile stays immersive; this direction
applies only at `md+`.

### Pattern + style

| Field | Value |
| --- | --- |
| Product type | Desktop trading card scanner workstation |
| Landing / app pattern | Feature-Rich Showcase → **workstation grid** (camera + inspector + strip; not a marketing landing) |
| UI style | **Liquid Glass** (existing Deck Hearth DS) — translucent panels, rim-light, ember accent rings |
| Stack notes | nextjs · React 18 · Tailwind + CSS variables · `<Modal>` / `<GlassSurface>` / `<Button>` primitives |

**Generator vs repo (repo wins):**

| Generator | Locked override |
| --- | --- |
| Exaggerated Minimalism (oversized type, massive whitespace) | **Rejected** — match existing app density; page title `text-2xl` / `font-semibold`, subtitle `text-sm text-secondary` |
| Palette `#1E293B` / `#2563EB` scan blue | **Rejected** — use `--accent-ember`, `--accent-flame`, `--accent-gold`, `--bg-*`, `--text-*` from `styles/globals.css` |
| Inter typography | **Rejected** — keep system stack (`-apple-system, BlinkMacSystemFont, …`) |
| Feature-Rich Showcase sections | **Adapted** — three functional zones (camera, inspector, strip) replace marketing feature cards |

### Layout authority (`md+` only)

Viewport split at `md` (768px). Below `md`, no changes (immersive +
checkout sheet).

```
┌─────────────────────────────────────────────────────────────────┐
│ [Layout sidebar] │ TopSearchBar + profile                        │
├──────────────────┼──────────────────────────────────────────────┤
│                  │ Card Scanner          [Scanner Tips]          │
│                  │ <subtitle>                                    │
│                  ├──────────────────────────┬───────────────────┤
│                  │ ┌─ Auto-detect ON ─┐     │ Scan Result  98%  │
│                  │ │  [live video]    │     │ ┌────┐ metadata   │
│                  │ │  ember brackets  │     │ │thumb│ set/rarity │
│                  │ └──────────────────┘     │ condition ▾ foil  │
│                  │ [Camera ▾][Upload][Batch]  │ confidence bar    │
│                  │              [Auto-detect]│ + Add to My Coll. │
│                  │                          │ Rescan | Add List │
│                  ├──────────────────────────┴───────────────────┤
│                  │ Recent │ Queue (N) │ Dupes (N)    Clear All│
│                  │ [chip][chip][chip]…                         │
│                  │              [ View All Scans ]             │
└──────────────────┴──────────────────────────────────────────────┘
```

| Zone | Width / placement | Surface recipe |
| --- | --- | --- |
| Page header | Full content width above grid | Flat on `--bg-primary`; Tips = `<Button variant="ghost">` + lightbulb icon |
| Camera column | `flex-1` / `min-w-0`; ~60–65% | Viewport frame: `--glass-surface-low` + `--ember-rim-subtle` on outer frame; **ember corner brackets** on video (existing `ScannerCamera`) |
| Desk control bar | Directly under viewport | `--glass-surface-mid` bar, `rounded-xl`, horizontal flex wrap at `md` |
| Inspector rail | Fixed `w-[360px]` or `max-w-sm`; sticky top optional | `--glass-surface-low` panel, `--elevation-ambient`, `--rim-light-inner/outer` |
| Bottom strip | Full content width below main grid | `--glass-surface-mid` container; tab row + chip scroller |

**Page copy (header):**

- Title: **Card Scanner**
- Subtitle: *Identify cards with your webcam and add them to My
  Collection.*

### Colors

All values via CSS variables — **no hex in `.js`**.

| Role | Token | Usage on this surface |
| --- | --- | --- |
| Page background | `--bg-primary` | Workstation canvas behind glass panels |
| Panel fill | `--glass-surface-{low,mid,high}` | Camera frame (low), control bar + strip (mid), popovers (high) |
| Primary text | `--text-primary` | Titles, card names, tab labels |
| Secondary text | `--text-secondary` | Subtitle, set/rarity, timestamps |
| CTA / primary action | `--accent-ember` | **Add to My Collection** button; active tab underline; selected chip border |
| Accent highlight | `--accent-flame`, `--accent-gold` | Confidence bar fill; match badge when ≥90% |
| Interactive rim | `--ember-rim-subtle` / `--ember-rim-pronounced` | Camera frame rest; primary button hover/focus |
| Success / match | `--accent-gold` or existing success token | Auto-detect ON dot; high-confidence pill |
| Error / batch fail | `--accent-ember` at reduced opacity or danger variant | Failed queue row indicator |
| Border | `--border` | Control bar dividers, chip separators |

Light and dark both use the same token names; theme context switches
values. Mock's cool-navy dark is approximated by existing
`--bg-primary-dark` — do not introduce a new dark base.

### Typography

| Role | Font | Notes |
| --- | --- | --- |
| Page title | System stack, `text-2xl font-semibold` | "Card Scanner" |
| Subtitle | System stack, `text-sm`, `--text-secondary` | One line under title |
| Section labels | `text-sm font-medium uppercase tracking-wide` | "Scan Result", strip tab labels |
| Card name (inspector) | `text-lg font-semibold` | Primary identify headline |
| Metadata | `text-sm`, `--text-secondary` | Set · rarity · collector # |
| Chip / queue row | `text-sm` name, `text-xs` meta | Timestamps right-aligned |

### Camera viewport + desk controls

**Inside frame (overlay on video):**

- **Auto-detect badge** — top-left pill: green status dot +
  `Auto-detect ON` / `Auto-detect OFF` (reflects pause toggle).
- **Ember corner brackets** — four L-shaped corners on the card
  alignment region (ship existing `ScannerCamera` bracket styling).
- Optional alignment guide (mock dotted vertical line) — **omit in
  v1** unless implementer brief explicitly includes it; brackets +
  badge are required.

**Desk control bar (below frame, left → right):**

| Control | Component | Notes |
| --- | --- | --- |
| Camera | `<select>` or styled native picker, label **Camera** | `enumerateDevices` video inputs; desktop only |
| Upload Image | `<Button variant="secondary">` + upload icon | Single-file; existing gallery path |
| Batch Scan | `<Button variant="secondary">` + stack icon | Multi-file picker; triggers sequential identify |
| Auto-detect | Toggle switch + label **Auto-detect** | Right-aligned on wide screens; extends `verificationPausedRef` |

**Webcam device picker — fallback copy (locked):**

| State | Message | Action hint |
| --- | --- | --- |
| Loading | Detecting cameras… | Disable `<select>` until resolved |
| Empty list | No camera found | Connect a webcam or use **Upload Image**. |
| Permission denied | Camera access blocked | Allow camera in your browser settings, or use **Upload Image**. |
| Error (enumerate failed) | Couldn't list cameras | Use **Upload Image** or reload the page. |

When empty or denied, render the message inline below the disabled
`<select>` (`text-sm`, `--text-secondary`); do not use a blocking
modal for device errors.

### Live match inspector (right rail)

Header row: **Scan Result** + confidence pill (e.g. `98% Match`).

**Empty state** (no identify yet): centered illustration area +
*"Point your camera at a card or upload an image to see a match."*

**Populated state** (latest unprocessed cart entry):

- Thumbnail + name, set, rarity icon, collector number
- **Condition** — `<select>` (existing condition options)
- **Foil** — toggle switch
- Confidence — percentage + horizontal bar + caption (*Excellent match.*
  / *Good match.* / *Low confidence — verify before adding.*)
- Actions (top → bottom priority):
  1. **Primary:** `VOCAB.ADD_TO_MY_COLLECTION` (`<Button variant="primary">`, `+` icon optional)
  2. **Secondary row:** **Rescan** (ghost) · `VOCAB.ADD_TO_LIST` (ghost)
- **Never** ship "Add to Collection", "Save to Wishlist", or "Mark
  Owned".

### Bottom history / queue strip

**Tab row:** **Recent Scans** · **Scan Queue** `(N)` · **Duplicates**
`(N)` — badge = unprocessed count / duplicate row count per IA.
**Clear All** — text link, right-aligned (`text-sm`, destructive on
hover).

**Card chips** (horizontal scroll, `overflow-x-auto`):

- Thumbnail, name, set · rarity · condition snippet
- Status: confidence % + relative time (*Just now*, *2 min ago*)
- Active / focused row: `--ember-rim-pronounced` border (matches mock
  ember outline on leftmost chip)
- **No `backdrop-filter` on individual chips** — solid
  `--bg-secondary` per per-card grid performance budget
  (`docs/DESIGN_TOKENS.md` § Per-card grid performance budget)

**View All Scans** — full-width ghost button at strip bottom; opens
expanded queue view or scrolls strip — Architect brief decides;
visual = translucent bar button from mock.

**Row click** — focuses that card in the inspector (IA-locked).

### Batch Scan progress (Scan Queue tab)

When batch is running, **Scan Queue** tab auto-focuses (or shows an
inline banner):

- Progress line: **Scanning 3 of 12…** with determinate progress bar
  (`--accent-ember` fill)
- **Cancel** — ghost button; **keeps already-identified rows** (IA)
- Per-file failure — enqueue row with error flag, label **Identify
  failed**, `text-sm` reason if available; batch **continues** remaining
  files
- On complete — banner dismisses; failed rows stay in queue with
  visual error state (ember left border or warning icon)

### Scanner Tips (surface locked)

**`<Modal>`** — five tips exceed popover length; use
`components/ui/Modal` + modal panel recipe (`--glass-surface-low`,
`--modal-scrim`). Trigger: header **Scanner Tips** button (lightbulb
icon + label).

| # | Tip |
| --- | --- |
| 1 | **Good lighting** — avoid glare on foil cards. |
| 2 | **Fill the frame** with one card; keep corners visible. |
| 3 | **Hold still** until Auto-detect locks the match. |
| 4 | **Use Batch Scan** for a pile of photos from your gallery. |
| 5 | **Switch camera** if the image is dark or mirrored. |

Modal title: **Scanner Tips**. Dismiss via close control + Esc +
scrim click (standard `<Modal>` behavior).

### Effects + motion

- Panel transitions: **150–200ms** ease on hover/focus for buttons and
  chips (`docs/MOTION_SYSTEM.md` if defined; else `transition-colors
  duration-200`)
- Auto-detect badge dot: subtle pulse when ON; **respect
  `prefers-reduced-motion`** (static dot when reduced)
- Confidence bar: width transition 300ms on value change; no animation
  when reduced
- Tab switch: instant content swap (no slide); optional 150ms fade on
  chip row refresh
- Hover: `cursor-pointer` on all clickable chips, tabs, buttons;
  `--ember-rim-subtle` → `--ember-rim-pronounced` on primary hover

### Anti-patterns (do not ship)

- Full-bleed camera overlay on desktop (mobile-only chrome)
- Hex colors in `.js` / inline styles
- Generator slate/blue palette or Inter font import
- `backdrop-filter` on per-card strip chips or thumbnail tiles
- "Add to Collection", "Save to Wishlist", "Mark Owned", "All My
  Cards" in UI copy
- Custom modal scrim (must use `<Modal>` primitive)
- Popover for five tips (use Modal)
- Parallel batch identify UI implying concurrent Gemini calls
- Device `<select>` on mobile (facing-mode toggle stays)

### Pre-delivery checklist

- [ ] No emojis as icons (SVG: Lucide / Heroicons)
- [ ] `cursor-pointer` on clickable elements
- [ ] Hover/focus transitions 150–300ms
- [ ] Text contrast ≥ 4.5:1 on glass-over-flat surfaces
- [ ] Keyboard focus visible (`--ember-rim-pronounced` focus ring)
- [ ] `prefers-reduced-motion` respected (badge pulse, bar animate)
- [ ] Responsive: mobile immersive unchanged; workstation at 768 /
  1024 / 1440
- [ ] Light + dark verified against mock reference
- [ ] Copy from `lib/collection-vocabulary.js` for add actions
- [ ] Visual-diff: desktop `/scanner` if in suite

### Conflict rule

**Repo design tokens win** when they disagree with generator output.
This lock intentionally overrides `ui-ux-pro-max` palette and typography
only. Layout proportions follow the attached mock; nav items in the
mock that are not product routes (Wishlist, Trades, etc.) are **not**
in scope — existing Layout sidebar IA stands.

## UX

### 1. Existing components to reuse

| Component | Path | Use on desktop workstation |
| --- | --- | --- |
| `<Modal>` | `components/ui/Modal.js` | Leave-with-queue, List picker, **Scanner Tips** (five tips — no custom scrim) |
| `<Button>` | `components/ui/Button.js` | Upload Image, Batch Scan, Auto-detect toggle label area, inspector primary/ghost actions, batch Cancel, View All Scans |
| `<GlassSurface>` | `components/ui/GlassSurface.js` | Camera frame, desk control bar, inspector rail, strip container, strip chips (solid `--bg-secondary` fill inside — no per-chip blur) |
| `<Input>` | `components/ui/Input.js` | Not required v1; strip search deferred to Architect brief |
| `ScannerCamera` | `components/scanner/ScannerCamera.js` | Framed viewport, ember brackets, overlay Auto-detect badge; hide mobile overlay chrome at `md+` via variant prop |
| `ScannerDisambiguation` | `components/scanner/ScannerDisambiguation.js` | Unchanged multi-match picker; pauses identify via existing `verificationPausedRef` wiring |
| `ScannerToast` | `components/scanner/ScannerToast.js` | Post-commit confirmation, batch complete summary, identify errors |
| `ScannerCheckoutSheet` | `components/scanner/ScannerCheckoutSheet.js` | **Mobile only** (`max-md`) — do not mount at `md+` |
| `ScannerCountPill` | `components/scanner/ScannerCountPill.js` | **Mobile only** — desktop uses strip Queue badge instead |
| `ReviewCardItem` | `components/scanner/ReviewCardItem.js` | **Pattern donor** for condition `<select>`, foil toggle, confidence ring/color helpers, increment/decrement — lift constants (`CONDITION_OPTIONS`, `confidencePercent`, `confidenceRingColor`) into shared export or duplicate minimally in `ScannerResultPanel` |
| Leave modal + List picker | `pages/scanner.js` | Reuse verbatim markup and handlers; extend leave trigger to desktop back/nav paths |

**New surfaces** (`ScannerResultPanel`, `ScannerHistoryStrip`, `ScannerTips`) compose primitives only — no new modal shell, no hex in `.js`.

### 2. Design direction alignment

- **Liquid Glass tokens:** All panels use `--glass-surface-{low,mid,high}`, `--ember-rim-{subtle,pronounced}`, `--elevation-ambient` per Design direction layout authority table. Inspector rail = `GlassSurface` tint `low`; desk bar + strip = `mid`.
- **Ember accent:** Primary add, active tab underline, focused chip border, Auto-detect ON dot use `--accent-ember` / `--accent-flame` — not generator slate/blue.
- **Copy lock:** Inspector and strip actions import `VOCAB.ADD_TO_MY_COLLECTION` and `VOCAB.ADD_TO_LIST` from `lib/collection-vocabulary.js`. Wishlist omitted (IA-locked).
- **Performance budget:** Strip chips and inspector thumbnail tiles use solid `--bg-secondary` — **no `backdrop-filter` on per-card elements** (Design direction + `docs/DESIGN_TOKENS.md`).
- **Tips surface:** `<Modal size="md">` with glass panel recipe — matches Design direction "Popover for five tips" rejection.
- **Motion:** 150–200ms color/rim transitions; badge pulse and confidence bar width animate only when `prefers-reduced-motion: no-preference` (see §7 interaction patterns).

### 3. Existing patterns to follow

- **Viewport split:** `pages/scanner.js` already gates mobile checkout with `md:hidden` / `hidden md:flex` — extend to `Layout chrome="default"` at `md+` only; keep `chrome="immersive"` below `md`.
- **Pause identify:** `verificationPausedRef` in `pages/scanner.js` (lines 59–63) — extend desktop Auto-detect OFF to set ref; keep existing pauses for list picker + disambiguation + mobile checkout.
- **Single-card commit:** `queue.addSingleCardToOwned(card)` in `lib/use-scanner-queue.js` — inspector primary action; same API path as mobile bulk commit.
- **Leave with queue:** Existing `<Modal title="Leave scanner?">` + `clearScannerCartStorage()` — trigger from desktop back, sidebar nav away, and `router.back()` when `unprocessedCount > 0`.
- **List commit:** Existing List picker `<Modal>` + `handleListPick` — inspector `VOCAB.ADD_TO_LIST` opens same modal scoped to **focused inspector card** (single selection), not bulk strip selection.
- **Error alerts:** Match `ScannerCheckoutContent` / `pages/scanner.js` list picker — `role="alert"` div with `color-mix(in srgb, var(--color-error) …)` for commit and batch failures.
- **Confidence UX:** Reuse `ReviewCardItem` thresholds: `<70%` ember warning, `≥90%` gold/high — caption text from Design direction (*Excellent match.* / *Good match.* / *Low confidence — verify before adding.*).
- **Device picker fallbacks:** Inline `text-sm` `--text-secondary` messages below disabled `<select>` per Design direction table — not a blocking modal.
- **Focus rings:** `Button` / `ChromeIconButton` pattern — `focus-visible:ring-2` + `'--tw-ring-color': 'var(--accent-ember)'` (`components/scanner/ScannerCamera.js`).

### 4. A11y constraints

Hand to `role-a11y-auditor`:

- **1.4.3 Contrast (Minimum):** All `--text-primary` / `--text-secondary` on `--glass-surface-*` over `--bg-primary` must meet **4.5:1** for body text, **3:1** for large/bold card name (`text-lg font-semibold`). Confidence bar track vs fill must meet **3:1** non-text contrast.
- **1.4.10 Reflow:** At 768px width with 200% zoom, workstation grid stacks camera above inspector; strip tabs remain horizontally scrollable without two-dimensional scroll traps.
- **1.4.11 Focus Not Obscured (Minimum):** Sticky inspector rail and batch progress banner must not fully hide focused desk controls or strip chips.
- **2.1.1 Keyboard:** Every action reachable without pointer: desk controls, strip tabs (tablist), chip rows, inspector add/rescan, batch Cancel, Tips open/close.
- **2.4.3 Focus Order:** DOM order = visual order: page header (Tips) → camera frame → desk bar (Camera → Upload → Batch → Auto-detect) → inspector → strip tabs → chip scroller → View All.
- **2.4.7 Focus Visible:** Ember focus ring on all interactive elements; native `<select>` gets `focus-visible:ring-2` wrapper if browser default ring is suppressed.
- **2.4.11 Focus Not Obscured:** Modal open (Tips, leave, list picker) uses existing `useFocusTrap` — no focus escape to camera video underneath.
- **4.1.2 Name, Role, Value:**
  - Auto-detect toggle: `role="switch"`, `aria-checked={!verificationPausedRef}`, visible label **Auto-detect** associated via `htmlFor` / `aria-labelledby`.
  - Strip tabs: `role="tablist"` / `role="tab"` / `role="tabpanel"` with `aria-selected`, `aria-controls`, badge counts in `aria-label` (e.g. `Scan Queue, 3 unprocessed cards`).
  - Camera `<select>`: `<label>` **Camera** + `aria-describedby` pointing to empty/denied fallback text when present.
  - Inspector condition `<select>`: accessible name includes card name (`Condition for {card.name}`).
  - Foil toggle: `role="switch"`, `aria-checked`, label **Foil**.
- **4.1.3 Status Messages:** Auto-detect ON/OFF badge exposes `aria-live="polite"` region; batch progress (`Scanning 3 of 12…`) uses `aria-live="polite"`; commit success via `ScannerToast` with `role="status"`.
- **3.3.1 Error Identification:** Batch per-file failures show **Identify failed** + reason text on the queue row; commit errors in inspector use `role="alert"` (same pattern as checkout footer).
- **2.5.3 Label in Name:** Visible button text must appear in accessible name — e.g. **Upload Image**, not icon-only without `aria-label`.
- **Live region discipline:** Only one polite live region for batch progress; avoid duplicate announcements on tab auto-focus during batch.

### 5. Interaction patterns

#### Locked decisions (opinionated — one pattern each)

| Topic | Decision | Nielsen / rationale |
| --- | --- | --- |
| **Empty inspector** | Centered muted camera/scan illustration + single line: *"Point your camera at a card or upload an image to see a match."* No duplicate Upload/Batch CTAs in the rail (controls live in desk bar). | **H8** aesthetic minimalism; **H6** recognition — one place for actions. |
| **Leave with uncommitted queue** | Keep existing `<Modal title="Leave scanner?">` — **Keep scanning** (secondary) / **Leave** (danger). Same copy and `clearScannerCartStorage()` behavior on desktop nav-away. | **H3** user control; **H5** error prevention. |
| **Inspector add success** | **Advance inspector to next unprocessed queue entry** (or empty state if queue clear). Show `ScannerToast` (*Added to My Collection*). Committed card appears in **Recent Scans** with processed styling. Do **not** linger on committed card in inspector. | **H2** real-world scan rhythm; **H1** toast + strip update confirm status without blocking next decision. |
| **Auto-detect OFF** | Real pause via `verificationPausedRef` — no identify/OCR while OFF. Badge reads **Auto-detect OFF**; dot static gray. Toggle is independent of mobile checkout pause logic. | **H3** user control; **H1** badge reflects system state. |
| **Rescan** | Clears focus card's identify metadata and re-queues same physical capture path (webcam frame or re-read gallery source if batch row) — card stays in queue unprocessed. | **H3** undo/recover from bad match. |
| **Duplicate row click** | Focuses that card in inspector (scroll chip into view, `--ember-rim-pronounced` on active chip). **Add still allowed** — increment qty via inspector primary or existing queue increment if duplicate already unprocessed. No separate "Skip" action in v1. | **H6** recognition over recall; **H4** consistency with Queue tab. |
| **Batch cancel** | Ghost **Cancel** in Scan Queue tab progress banner. Cancels remaining files; **keeps** already-identified rows (IA-locked). Banner dismisses; toast *Batch scan stopped* (info). | **H3** user control; **H9** recover from long batch. |
| **Batch per-file failure** | Row stays in Scan Queue with ember left border + **Identify failed** label; batch **continues**. Row click focuses inspector for manual Rescan or remove. | **H9** graceful recovery; **H1** visible error on row. |
| **Strip row click (all tabs)** | Sets inspector focus to that card; does not auto-commit. Recent / Queue / Duplicates share one focus model. | **H4** consistency. |
| **Clear All** | Destructive text link — confirm via existing pattern or inline `window.confirm` only if Architect brief adds it; default: immediate clear with undo **not** required v1 (queue is session-scoped). Architect to confirm in brief. | **H5** prevent accidental loss — if no confirm, disable when batch running. |

#### Required

- **Loading — identify in flight (H1):** Inspector shows skeleton/thumbnail placeholder + *Identifying…* on desk bar Upload/Batch busy states disable repeat picks.
- **Loading — commit (H1):** Inspector primary `<Button loading>` during `addSingleCardToOwned`; disable Rescan and secondary actions while processing.
- **Empty strip tabs (H9):** Recent: *No scans yet this session.* Queue: *Scan queue is empty — matches appear here before you add them.* Duplicates: *No duplicates detected.*
- **Auto-detect ON feedback (H1):** Green pulsing dot in frame badge (respect reduced motion); optional success chime already in `ScannerCamera` — desktop may keep muted default.
- **Batch running (H1):** Auto-select **Scan Queue** tab; determinate progress bar; Cancel visible throughout.
- **Focus management (H3):** Opening Tips/list/leave modals traps focus; closing returns focus to trigger button.
- **Hover/focus (H4):** Chips and tabs: `--ember-rim-subtle` → `--ember-rim-pronounced` on hover/focus; `cursor-pointer` on all click targets.

#### Nice-to-have

- **Optimistic UI:** Inspector advance on commit can happen after API success only (no optimistic skip) — queue hook already marks `processed` post-success.
- **Keyboard shortcut:** `A` to trigger inspector **Add to My Collection** when inspector populated and not processing — defer unless Architect brief adds; not v1 blocker.
- **View All Scans:** Expanded modal or full-width strip — Architect decides; v1 may scroll chip row only.

### 6. Anti-patterns to avoid

- **Duplicate CTAs in empty inspector** — Upload/Batch already in desk bar (**H8** minimalist design).
- **Custom modal scrim for Tips or device errors** — must use `<Modal>` or inline text only (**H4** consistency).
- **Device `<select>` on mobile** — facing-mode swap stays (**H4**).
- **Popover for five Scanner Tips** — too long; Modal only (**H8**).
- **Linger on committed card in inspector after add** — blocks scan rhythm (**H2**).
- **Stop entire batch on one file failure** — IA forbids (**H9**).
- **Discard identified rows on batch cancel** — IA forbids (**H3**).
- **"Save to Wishlist" / "Mark Owned" / "Add to Collection"** copy — CI forbidden strings (**H4**).
- **`backdrop-filter` on strip chips or inspector thumbnail** — GPU budget violation.
- **Full-bleed camera overlay on desktop** — mobile-only chrome (**H2**).
- **Parallel batch progress implying concurrent API calls** — misleading (**H1** honest status).
- **Icon-only desk controls without `aria-label`** — fails **2.5.3** / **4.1.2**.
- **Auto-commit on strip row click** — user loses inspect step (**H3** user control).
- **Slide animation on tab switch** — Design direction specifies instant swap; fade optional ≤150ms only.

### 7. Mobile / responsive notes

**Below `md` (767px) — zero regression:**

- Keep `Layout chrome="immersive"`, `ScannerCamera` overlay chrome (back, gallery, flash, facing, Review N pill), `ScannerCheckoutSheet`, `ScannerCountPill`, scan peek, disambiguation sheet behavior unchanged.
- Do not render desktop workstation grid, inspector rail, history strip, device `<select>`, Batch Scan desk control, or Tips header button on mobile unless Tips is also added to mobile overlay (out of scope — **Tips = desktop header only v1**).
- `verificationPausedRef` mobile checkout pause unchanged (`isCheckoutOpen && max-width 767px`).

**At `md+` (768px+):**

- `Layout chrome="default"` — sidebar + `TopSearchBar` visible.
- Workstation grid: camera column `flex-1 min-w-0` (~60–65%) + inspector `w-[360px] max-w-sm` + full-width bottom strip.
- Hide mobile-only elements: checkout sheet, Review N pill, overlay top/bottom camera bars, facing-mode toggle (replace with Camera `<select>`).
- Desk control bar wraps at `md`; Auto-detect toggle right-aligned at `lg+`.
- Strip chip scroller: `overflow-x-auto`, `-webkit-overflow-scrolling: touch` for trackpad/touch on hybrid devices; min chip height **44px** touch target.
- **`prefers-reduced-motion`:** Disable Auto-detect badge pulse, confidence bar width transition, optional chip-row fade — use `@media (prefers-reduced-motion: reduce)` instant state swaps; keep color/rim hover transitions ≤150ms or disable per `docs/MOTION_SYSTEM.md` if defined.
- **Breakpoints to verify:** 768 / 1024 / 1440 — inspector sticky top optional; strip never collapses tabs into mystery meat menu v1.
- **Visual-diff:** Desktop `/scanner` only; mobile baseline must not change.

## Architecture

### File plan

| File | Action | Purpose |
| --- | --- | --- |
| `lib/use-camera-scanner.js` | modified | `enumerateDevices`, `deviceId` selection, picker status, sessionStorage persist |
| `test/lib/use-camera-scanner.test.js` | new | Mock `mediaDevices` — device list, constraint switch, persist |
| `components/scanner/ScannerTips.js` | new | Header trigger + five-tip `<Modal>` |
| `test/components/ScannerTips.test.js` | new | Open/close, copy lock |
| `components/scanner/ScannerResultPanel.js` | new | Right-rail live match inspector |
| `test/components/ScannerResultPanel.test.js` | new | Empty/populated, add loading, vocab |
| `components/scanner/ScannerHistoryStrip.js` | new | Recent / Queue / Duplicates strip + `getDuplicateCards` |
| `test/components/ScannerHistoryStrip.test.js` | new | Tabs, duplicates helper, batch banner |
| `lib/scanner-batch-identify.js` | new | Sequential `runSequentialGalleryIdentify` |
| `test/lib/scanner-batch-identify.test.js` | new | Progress, cancel, error continuation |
| `pages/scanner.js` | modified | md+ workstation grid, focus state, pause composite, chrome split |
| `components/scanner/ScannerCamera.js` | modified | `workstation` variant — framed viewport, hide mobile overlay at md+ |
| `lib/use-scanner-identification.js` | modified | Remove redundant `verificationPausedRef` overwrite effect |
| `lib/use-scanner-queue.js` | modified | Boolean return from `addSingleCardToOwned` / `addSingleCardToCollection` |
| `test/pages/scanner.test.js` | new | Mobile checkout regression + desktop composition smoke |
| `test/components/ScannerCamera.test.js` | new | Workstation variant hides overlay chrome |

**Not touched:** `components/Layout.js` (immersive already equals default at md+),
`lib/scanner-card-identify.js`, `pages/api/scan/identify.js`, `ScannerCheckoutSheet.js`
(mobile only), nav IA (`dashboard-home-realignment`).

### API surface

No new or modified API routes. Client cart remains `sessionStorage` via
`lib/scanner-session.js`. All commits reuse existing `scanner-route-api.js` helpers
(`addScannedCardToOwned`, `addScannedCardToCollection`).

### Schema diff

None.

### Test plan

| Area | File | What to lock |
| --- | --- | --- |
| Device picker hook | `test/lib/use-camera-scanner.test.js` | `deviceId` constraints, enumerate, persist, status messages |
| Batch helper | `test/lib/scanner-batch-identify.test.js` | Sequential order, cancel mid-batch, error continues |
| Tips modal | `test/components/ScannerTips.test.js` | Five tips, Modal primitive |
| Inspector | `test/components/ScannerResultPanel.test.js` | Empty state (no duplicate CTAs), `VOCAB` labels |
| History strip | `test/components/ScannerHistoryStrip.test.js` | `getDuplicateCards`, tab badges, batch banner |
| Camera variant | `test/components/ScannerCamera.test.js` | Overlay hidden for `workstation` at md+ |
| Page orchestration | `test/pages/scanner.test.js` | Checkout sheet at max-md; workstation surfaces at md+ |
| Mobile regression | `test/components/ScannerCheckoutSheet.test.js` | Existing suite stays green — do not weaken |
| Layout chrome | `test/components/Layout.test.js` | Existing immersive tests — no change expected |

**Visual-diff:** `tests/visual/` contains only `homepage.spec.ts` / `home.png` —
no `/scanner` baseline update required for this convoy.

### Risk list

| Risk | Mitigation |
| --- | --- |
| `verificationPausedRef` race between page and `useScannerIdentification` | Brief 6 deletes identification hook's direct ref assignment; page owns composite pause including `isAutoDetectPaused` |
| `addSingleCardToOwned` lacks success signal today | Brief 6 adds boolean return in `use-scanner-queue.js` |
| `chrome="immersive"` on all viewports today | Boot finding: Layout already shows sidebar at md+; page uses explicit `default` vs `immersive` via `matchMedia` for clarity |
| Batch identify failures invisible in queue | Page enqueues `identifyFailed` rows; strip shows ember border + label |
| Rescan without stored capture | v1: re-identify via `scanImageUrl` fetch; else toast to rescan from camera |
| Desktop/mobile class split regressions | Tests mock `matchMedia`; mobile paths keep `md:hidden` gates |
| Forbidden copy in new surfaces | Import `VOCAB` from `collection-vocabulary.js`; CI `forbidden-stale-strings` |
| Brief 6 LOC >400 | Accepted — single serializer for `pages/scanner.js`; components split across Briefs 2–4 |
| `getDuplicateCards` over-counts | Helper uses ownershipMap + session name/set + quantity>1; tune in strip tests |

### Decomposition

| Brief # | Title | Files | Depends on | Est. PR size |
| --- | --- | --- | --- | --- |
| 1 | Camera device picker hook | `use-camera-scanner.js`, test | — | S (~180 LOC) |
| 2 | Scanner Tips modal | `ScannerTips.js`, test | — | S (~100 LOC) |
| 3 | Live match inspector | `ScannerResultPanel.js`, test | — | M (~280 LOC) |
| 4 | History / queue strip | `ScannerHistoryStrip.js`, test | — | M (~300 LOC) |
| 5 | Sequential batch helper | `scanner-batch-identify.js`, test | — | S (~120 LOC) |
| 6 | Page workstation wiring | `scanner.js`, `ScannerCamera.js`, identification + queue hooks, tests | 1, 2, 3, 4, 5 | L (~450 LOC) |

**Estimated PRs:** 6 (Briefs 1–5 parallelizable; Brief 6 after merge or rebase).

### Slice dependencies (multitask-ready)

```yaml
slice_dependencies:
  - brief: 1
    depends_on: []
    files:
      - lib/use-camera-scanner.js
      - test/lib/use-camera-scanner.test.js
  - brief: 2
    depends_on: []
    files:
      - components/scanner/ScannerTips.js
      - test/components/ScannerTips.test.js
  - brief: 3
    depends_on: []
    files:
      - components/scanner/ScannerResultPanel.js
      - test/components/ScannerResultPanel.test.js
  - brief: 4
    depends_on: []
    files:
      - components/scanner/ScannerHistoryStrip.js
      - test/components/ScannerHistoryStrip.test.js
  - brief: 5
    depends_on: []
    files:
      - lib/scanner-batch-identify.js
      - test/lib/scanner-batch-identify.test.js
  - brief: 6
    depends_on: [1, 2, 3, 4, 5]
    files:
      - pages/scanner.js
      - components/scanner/ScannerCamera.js
      - lib/use-scanner-identification.js
      - lib/use-scanner-queue.js
      - test/pages/scanner.test.js
      - test/components/ScannerCamera.test.js
```

### Boot-the-brief findings (fixed in briefs)

1. **Layout chrome:** `chrome="immersive"` already renders sidebar + TopSearchBar at
   `md+` (`Layout.js` uses `max-md:hidden` only). Brief 6 uses `matchMedia` to pass
   `chrome="default"` on desktop for semantic clarity — no `Layout.js` edit.
2. **`verificationPausedRef` overwrite:** `useScannerIdentification` lines 62–66 set
   the ref to `Boolean(disambiguation)` only, racing page composite pause — Brief 6
   removes that effect.
3. **`addSingleCardToOwned` return:** No success boolean today — Brief 6 extends
   `use-scanner-queue.js` to return `true`/`false`.
4. **Visual suite:** `/scanner` not in `tests/visual/` — no Linux baseline refresh.
5. **No new packages** — dep-set check N/A; all primitives exist (`Modal`, `GlassSurface`, `Button`).
6. **Gallery batch path verified:** `identifyFromGalleryFile` exists in
   `use-scanner-identification.js` (line 326) — Brief 5 wraps it sequentially.
