diff --git a/.dockerignore b/.dockerignore
index dca3d75..b98ef50 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -14,3 +14,11 @@ HANDOFF.md
# promo footage is a local build product (tools/capture.py), never served
promo/clips
tools
+
+# the Capacitor shell — the server serves the repo root directly, and none of this is ever fetched
+node_modules
+www
+ios
+package.json
+package-lock.json
+capacitor.config.json
diff --git a/.gitignore b/.gitignore
index a09e7ee..d53b26b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,8 @@ assets/_gen/
assets/_refcheck/
assets/__pycache__/
promo/clips/
+
+# the Capacitor shell. ios/ IS committed (it is the project, and Xcode settings live in it) —
+# ios/.gitignore already keeps the copied web payload, Pods and Xcode's per-user junk out of it.
+node_modules/
+www/
diff --git a/CLAUDE.md b/CLAUDE.md
index 9e83be0..286ce08 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -9,4 +9,8 @@
4. **Every push deploys** (Coolify → splatter-tennis.wuwi.one). Bump `main.js?v=N` in index.html on
every push or phones keep the old build. New top-level files must be added to the Dockerfile COPY.
5. **Zero dependencies, no build step, no framework.** This is a settled decision — solve it with
- what is already in the repo.
+ what is already in the repo. The Capacitor shell (`package.json`, `ios/`) is the ONE exception
+ and it is outside `src/`: **no package JS ships.** Game code calls native plugins through the
+ injected bridge — `Capacitor.nativePromise(name, method, opts)`, see `src/haptics.js` — never an
+ `import` of a package (a bare specifier 404s on the web) and never `Capacitor.Plugins.*` (empty
+ without @capacitor/core, so it fails SILENTLY). `npm run ios` builds the app.
diff --git a/PROJECT.md b/PROJECT.md
index d1f49cb..558a5d6 100644
--- a/PROJECT.md
+++ b/PROJECT.md
@@ -20,7 +20,9 @@ anything, so if you add a `return` for an awkward deal, check the body still run
the state-machine contracts that have historically produced the show-stoppers. The Dockerfile runs
it as a build stage, so a red suite cannot deploy — but finding out from Coolify is the slow way.
-Last updated 2026-08-20 (the friend across the net gets his OWN sheets — the player's kit, drawn
+Last updated 2026-08-20 (THE SHELL: a Capacitor iOS project, haptics on the existing sfx call
+sites, portrait enforced in Info.plist, and swipe-left-to-go-back in the menus — the web build
+unchanged and still primary. See "THE SHELL". Before that, same day: the friend across the net gets his OWN sheets — the player's kit, drawn
from the front, so his pick dyes the same garment yours does; the setup screen shows that front too.
See "THE FRIEND ACROSS THE NET WEARS YOUR KIT". Before that 2026-08-18: the store model in LATER revised: free download + €2.99 unlock-everything
IAP with the free-tier gates, superseding paid-upfront; before that 2026-08-15, two sessions: the
@@ -2536,6 +2538,74 @@ The gates, and the numbers they were calibrated on:
**SUPERSEDED 2026-08-12 — see the next section.** Those 31 frames were not art: `--verify` found
27 of them dead. The brute is now live (`ZFAMS` has `'br'`, the KEEPER wears it) off ONE sheet.
+## THE SHELL — CAPACITOR, HAPTICS, PORTRAIT, SWIPE-BACK (2026-08-20)
+
+The first three bullets of LATER item 2 ("What the shell buys GAMEPLAY-wise") are built. **The web
+build is unchanged and stays the primary surface** — every line below degrades to a no-op in a
+browser, and `splatter-tennis.wuwi.one` keeps deploying off the same repo on every push.
+
+**The zero-dependency rule bends exactly once and exactly here.** `package.json`, `node_modules/` and
+`ios/` exist for the SHELL. **No JS from any package ships.** `src/haptics.js` calls the bridge the
+shell injects — `Capacitor.nativePromise('Haptics', 'impact', { style })` — never
+`import ... from '@capacitor/haptics'`. There is no bundler in this project, so a bare specifier
+inside a `
+