Mobile capture from `/capture` (Phase 1.6 polish, D220 + D221) does two things the open-tab-on-a-phone version of last quarter didn't: it de-skews captures taken at an angle, and it queues uploads when connectivity drops.
## Perspective correction
When the still-photo path detects document corners (opencv.js, lazy-loaded ~9MB WASM only on first capture session), it applies a 4-point perspective warp before the file uploads. The result lands on the server already de-skewed — no more trapezoidal contracts in your archive. Detection-failure or unsupported browser falls back to passthrough; the upload still completes. Same downstream pipeline (extraction + classification + DLP) as a flat-bed scan.
The lazy-load matters because most `/capture` sessions don't need OpenCV — voice notes, drag-drop, and PDF captures bypass the WASM entirely. Only the first still-photo capture in a session pays the bundle cost, and it's cached for the rest of the visit.
## Offline buffer
Phones on job sites and in courthouse basements lose connectivity all the time. Failed uploads now write to an IndexedDB queue (`kodori-capture` database, `queue` store). The service worker registers a Background Sync `capture-drain` tag — when connectivity returns the browser fires the sync event, the worker reads the queue, and POSTs every row to `/api/v1/documents` with the original metadata: mime type, display name, sensitivity, collection, and a `capturedAt` timestamp inside an `offlineCapture` metadata block. 201 deletes the row; non-201 leaves it for the next sync attempt (Background Sync's exponential backoff handles cadence).
iOS Safari doesn't support Background Sync yet (as of 2026-04). The foreground-drain helper covers this path: when the user returns to `/capture` and connectivity is back, queued rows drain inline before the page renders. Same code path, same metadata block — the audit log can't tell the difference.