---
convoy: scanner-mobile-checkout
brief_number: 3
depends_on: [2]
recommended_model: composer-2.5-fast
model_tier: fast
files:
  - components/scanner/ScannerCamera.js
  - components/scanner/ScannerCountPill.js
  - components/scanner/ScannerScanPeek.js
  - components/scanner/ScannerToast.js
  - lib/use-camera-scanner.js
  - lib/use-scanner-identification.js
cross_brief_commitments:
  - brief: 4
    description: |
      `ScannerCamera` accepts orchestration props wired by Brief 4:
      `onBack`, `onOpenCheckout`, `onGalleryIdentify`, `latestPeekCard`,
      `cartCount`, `isCheckoutOpen`, `verificationPausedRef`. Defaults/no-ops
      keep the component renderable before Brief 4 lands. Brief 4 owns sheet
      open state and back-guard modal.
  - brief: 4
    description: |
      `useScannerIdentification` exports `identifyFromGalleryFile(file)` —
      thin adapter only; does not modify `pages/api/scan/identify.js` or
      `lib/scanner-card-identify.js` server contract.
---

# Brief 3: Camera chrome, facing mode, scan peek, gallery adapter

## Goal (1 sentence)

Refactor `ScannerCamera` into full-bleed immersive glass chrome (top/bottom bars, flash, camera swap, Review N pill, scan peek, gallery) and add rear/front `facingMode` support in `useCameraScanner`.

## Files in scope (do not edit anything else)

- `components/scanner/ScannerCamera.js`
- `components/scanner/ScannerCountPill.js`
- `components/scanner/ScannerScanPeek.js` (new)
- `components/scanner/ScannerToast.js`
- `lib/use-camera-scanner.js`
- `lib/use-scanner-identification.js` (gallery adapter + pause contract only)

## Conventions to follow

- Glass surfaces: `<GlassSurface tint="mid">` per UX brief; migrate hardcoded `rgba(0,0,0,0.55)` in `ScannerCamera` to tokens.
- `ScannerToast`: replace `ICON_MAP` Unicode glyphs with inline SVG check/error (design-direction checklist).
- `ScannerCountPill`: evolve to gradient **Review {N}** pill; keep `aria-label={`Review ${count} scanned cards`}`.
- Flash: `flash.flashSupported && facingMode === 'environment'` (UX spec). Hide on front camera; turning to front must call `toggleFlash` off if torch was on.
- Peek: new `ScannerScanPeek.js` — 3s auto-dismiss, `pointer-events-none` wrapper, single child button `pointer-events-auto`, tap calls `onOpenCheckout`.
- Copy: toast message `"{card.name} added"` with `role="status"`.
- Do **not** mount checkout sheet here — call `onOpenCheckout` only.
- Identification pipeline files (`scanner-card-identify.js`, API routes) are out of scope.

## `useCameraScanner` shape (verified)

Current `startCamera` hardcodes `facingMode: 'environment'` (line ~119). Extend:

```js
export function useCameraScanner({ onError, onVerifyCard, verificationPausedRef, facingMode = 'environment' }) {
  const [activeFacingMode, setActiveFacingMode] = useState(facingMode);
  // startCamera uses activeFacingMode in getUserMedia constraints
  const switchFacingMode = useCallback(() => {
    setActiveFacingMode((prev) => (prev === 'environment' ? 'user' : 'environment'));
  }, []);
  // useEffect: when activeFacingMode changes and stream exists, stopCamera + startCamera
  return { /* existing */, facingMode: activeFacingMode, switchFacingMode };
}
```

`verificationPausedRef` early-return already exists at line ~91 in the tracking interval — no change needed.

## Gallery adapter (verified — no existing file-picker path)

Add to `useScannerIdentification` return object:

```js
const identifyFromGalleryFile = async (file) => {
  if (!file || verificationPausedRef?.current) return;
  const imageData = await readFileToImageData(file); // canvas draw; local helper in same file
  const authHeaders = getScanAuthHeaders();
  const result = await tryLayer1TextIdentify(imageData, authHeaders);
  const outcome = resolveIdentifyOutcome(result);
  const syntheticTracker = { id: `gallery-${Date.now()}`, status: 'verifying' };
  await applyIdentifyOutcome(syntheticTracker, imageData, outcome);
};
```

Import `tryLayer1TextIdentify`, `resolveIdentifyOutcome` from `./scanner-card-identify.js` (already exported — verified).

## `ScannerCamera` props contract (for Brief 4)

```js
export default function ScannerCamera({
  queue,
  camera,
  identification,
  onBack,
  onOpenCheckout,
  onGalleryIdentify, // (file) => identification.identifyFromGalleryFile(file)
  latestPeekCard,    // most recent unprocessed queue entry or null
  cartCount,
  isCheckoutOpen,
  verificationPausedRef,
}) { /* ... */ }
```

Remove: `deckMode`, `sessionDestination`, `onStopSession`, phase-based stop button. Back button calls `onBack` (Brief 4 shows leave modal).

Layout: viewport `min-h-[100dvh] max-md:rounded-none max-md:min-h-[100dvh]`; top bar with back / "Scan Cards" / gallery file input; bottom bar flash | status | Review N.

## Acceptance criteria

- [ ] `switchFacingMode` restarts stream with toggled `facingMode`; flash hidden on `user`
- [ ] `ScannerScanPeek` auto-dismisses ~3s; tap invokes `onOpenCheckout`
- [ ] `ScannerToast` uses SVG icons, not Unicode glyphs
- [ ] `identifyFromGalleryFile` calls existing `tryLayer1TextIdentify` — no API route changes
- [ ] `ScannerCountPill` hidden when `count <= 0`; shows gradient Review pill when `count > 0`
- [ ] No hex literals added in edited `.js` files
- [ ] No scope expansion

## Rationale (≤3 sentences)

Camera chrome files are disjoint from `pages/scanner.js`, so this brief can land after the cart model without file conflicts. Gallery needs a thin hook adapter because no file-picker path exists today (boot-the-brief finding). Brief 4 wires props and pause ref when the sheet opens.
