# 🤖 Marketing screenshots — native Mac + iPhone captures for the hero collage

How we capture the native **Mac** and **iPhone** screenshots that sit in the
marketing site's three-device hero collage (alongside a web frame). This is the
committed, hand-operable form of a capture recipe that previously lived only in
a throwaway branch and agent memory — the "outlive-AI" rule in action: anyone,
human or agent, can stand the demo up and reproduce the shots from this doc plus
`tools/marketing-shots/`, with no special knowledge encoded elsewhere.

The whole flow is fictional and local. Nothing here ever points an app at a
real or production instance, and the Mac app is built in an **isolated identity**
so it can only ever show the seeded demo space — never anyone's real signed-in
spaces (see *Isolation* below — it is the load-bearing safety property).

The tool stands up the demo and points you here; the interactive captures
themselves stay **manual** on purpose (a person frames and takes each shot).

## Quick start (`./tools/dev shots`)

```sh
./tools/dev shots up          # Terminal 1 — build the isolated demo Mac app (if
                              # needed) + run a throwaway local server seeded with
                              # the fictional "The Commons" space. Foreground:
                              # prints the local URL + a localhost sign-in link;
                              # Ctrl-C stops it.
./tools/dev shots tunnel      # Terminal 2 — give the local server a real public
                              # https try-*.11relay.com address (via `lchat try
                              # --local`) + single-use Mac & iPhone sign-in links.
./tools/dev shots mac         # Launch the isolated "Eleven Demo" Mac app.
./tools/dev shots web-check shot.png   # Headless 2x screenshot of the seeded
                              # space, to eyeball the look before capturing native.
```

Implementation lives in `tools/marketing-shots/` (`serve.sh`, `tunnel.sh`,
`seed-team.mjs`, `demo-lib.mjs`, `device-link.mjs`, `web-check.mjs`) — the
`shots` verbs are thin wrappers, mirroring how `./tools/dev up` wraps
`tools/lchat-dev-up.mjs`. Runtime artifacts (the demo DB, state, PNGs) are
gitignored.

## The demo space: "The Commons"

A fictional civic-tech team space that matches the web hero frame
(`website/src/assets/hero/team-web.png`). It's seeded **through the real E2EE
protocol** (`demo-lib.mjs` provisions each member via an admin key-invite → claim
→ pubkey, then encrypts every thread/message client-side exactly as the browser
does) — the server only ever holds ciphertext, so there is no plaintext
shortcut. Six threads: **The commons 🌱** (the flagship — the exact web-frame
conversation: Aoife / Marcus / Devi, a reply, and reactions), Standup, Shipping
🚀, Field notes 📓, Roadmap, Watercooler ☕. You sign in as **Cass** (your own
messages are the accent bubbles on the right, matching the web frame's reply).

Eyeball it any time without a native build:

```sh
./tools/dev shots web-check /tmp/commons.png && open /tmp/commons.png
```

## Isolation — why this is safe (load-bearing)

A normal Mac build reads the **real** account. Both `LChatMac.entitlements` and
`LChatMac-Dev.entitlements` carry
`keychain-access-groups = $(AppIdentifierPrefix)com.elevenmessenger.messenger.shared`,
and a dev alt (`./tools/dev alt`) shares that same group **on purpose** — an alt
is the real account, not a sandbox, and has grabbed Paul's private space before.
That is exactly what we must NOT do for a marketing screenshot.

So the demo Mac build is signed with **`ios/LChatMac/LChatMac-Demo.entitlements`**
— minimal by design (only `app-sandbox`, `network.client`, and
`files.user-selected.read-write`), with the shared keychain group (and the app
group, push, and Sparkle exceptions) **removed**. No shared keychain group →
the build has its **own empty keychain** and can only show the demo space you
add. It also gets a distinct bundle id, `com.elevenmessenger.messenger.mac.capdemo`
(its own sandbox container + Launch Services identity, so it runs beside the real
app), and the display name "Eleven Demo". The real purple `AppIcon` is kept on
purpose so the screenshot shows the real brand mark.

**Crucially, none of this touches `project.yml`.** `./tools/dev shots` applies
the demo identity purely as xcodebuild **command-line build-setting overrides**
(`CODE_SIGN_ENTITLEMENTS=<abs demo path>`,
`PRODUCT_BUNDLE_IDENTIFIER=…mac.capdemo`, `LCHAT_MAC_DISPLAY_NAME=Eleven Demo`),
which outrank the target/config values in the generated project. A plain
`./tools/dev build mac` (or `xcodegen generate`) therefore still resolves the
normal entitlements (Release → `LChatMac.entitlements`, Debug →
`LChatMac-Dev.entitlements`) and the real `com.elevenmessenger.messenger.mac`
bundle id — the released app is never at risk.

Verify the isolation on the built app any time — **no output means isolated**:

```sh
codesign -d --entitlements :- \
  "ios/build-demo/Build/Products/Debug/Eleven Demo.app" | grep -i keychain
# → (no output) = no keychain-access-groups = isolated, own empty keychain
```

## Capturing the Mac shot

1. `./tools/dev shots up` (Terminal 1), then `./tools/dev shots tunnel`
   (Terminal 2) for a real https origin the native apps can reach.
2. `./tools/dev shots mac` to launch "Eleven Demo". In the app: **Spaces ▸ Add
   from link…** and paste the **Mac** link `shots tunnel` printed (signs you in
   as Cass). If a "Keep your access safe / Sign in" card appears, dismiss it with
   "Remind me later".
3. **Leave the Mac on its default Classic light theme** (light background, blue
   accent bubbles) — that is what `hero-mac.png` uses and what the collage wants
   for the natives; do **not** switch it to Lit Room (see *Theme pairing*). Open
   **The commons 🌱** to mirror the web frame, or **Standup** for variety (it has
   an own-bubble + a reaction).
4. **Capture full-screen, then crop with PIL** — the robust path. Interactive
   `screencapture -w` (click a window) and System Events window-bounds both rely
   on Accessibility / Quartz (pyobjc), which isn't granted on the build Mac and
   is unreliable when driven headlessly. Full-screen + crop needs none of that:

   ```sh
   screencapture -o ~/Desktop/team-mac-raw.png     # -o = no window shadow; full screen, 2x retina
   ```

   Crop the app-window region to the collage aspect (`hero-mac.png` is
   **1300×919**, aspect ≈ **1.415**) and scale to 1300×919:

   ```python
   from PIL import Image
   im = Image.open("team-mac-raw.png")          # full-screen 2x capture
   # Pick a crop box (left, top, right, bottom) around the window, keeping
   # (right-left)/(bottom-top) ≈ 1.415 so nothing is squashed on the resize.
   box = (LEFT, TOP, RIGHT, BOTTOM)
   im.crop(box).resize((1300, 919), Image.LANCZOS).save("team-mac.png")
   ```

## Capturing the iPhone shot — REAL DEVICE ONLY

> ⚠️ **Do NOT use the iOS Simulator.** The iOS 26.3.1 simulator renders emoji as
> tofu (□) — confirmed 2026-07-28, both in-app and in Safari. Use a real iPhone.

1. `./tools/dev shots tunnel` prints an **iPhone** link. Open it **on the
   device** (paste into Safari → Open in the app / join) — it signs the phone in
   as Cass. The phone is the real app, so it'll show this demo space alongside
   any real spaces; scroll to / open "The Commons" for the shot.
2. Land on the space's **thread list** (the frame in `hero-ios.png`: title + the
   six thread rows with previews).
3. Screenshot on the device (**Volume-Up + Side button**) and AirDrop it to the
   Mac. An iPhone 16 Pro shot is **1206×2622** (aspect ≈ 0.460).

### Reusing the existing bezel frame

The website's `hero-ios.png` (**760×1554**, RGBA, transparent surround) already
has the Apple bezel baked in. Rather than re-render a device frame, drop the new
screenshot into that same bezel. Its screen rect is **(41, 39)** with size
**678×1476** (aspect ≈ 0.459 — the raw 1206×2622 shot's 0.460 is a near-perfect
match, so scaling barely distorts), corners rounded at **r ≈ 92px**. PIL only
(numpy is **not** installed on the build Mac):

```python
from PIL import Image, ImageDraw

BEZEL = "hero-ios.png"        # 760×1554, Apple bezel baked in, transparent surround
RAW   = "team-ios-raw.png"    # on-device screenshot (e.g. 1206×2622, iPhone 16 Pro)
OUT   = "team-ios.png"

X, Y, W, H, R = 41, 39, 678, 1476, 92          # screen rect + corner radius in the bezel

frame = Image.open(BEZEL).convert("RGBA")
shot  = Image.open(RAW).convert("RGBA").resize((W, H), Image.LANCZOS)

# Rounded-rect mask so the shot's corners tuck under the bezel's screen cutout.
mask = Image.new("L", (W, H), 0)
ImageDraw.Draw(mask).rounded_rectangle([0, 0, W, H], radius=R, fill=255)

frame.paste(shot, (X, Y), mask)
frame.save(OUT)
```

You may **read** `website/src/assets/hero/hero-ios.png` as the reference bezel,
but the composite output belongs in the website repo — do not modify the website
repo from a messenger-repo change.

## Theme pairing

The collage deliberately pairs a **warm** web frame (Lit Room cream) with
**lighter** natives (Classic light / blue). So only the **web** frame is Lit
Room; **both natives stay on the default Classic light** (blue accent) — the
Mac app's out-of-the-box theme, matching `hero-mac.png` and the iPhone. Don't
switch the Mac to Lit Room, or all three frames read as the same cream wash.

## Where the outputs go

The finished frames live in the **website** repo (not this one):

- `website/src/assets/hero/team-web.png`, `team-mac.png`, `team-ios.png` — wired
  into the homepage `.collage` in `website/src/index.njk`.
- The original `hero-web.png` / `hero-mac.png` / `hero-ios.png` family (a
  household space) is used on `/families/`.

Committing and deploying the website is a separate, website-repo concern (its
own deploy path) — out of scope for this kit, which only produces the raw + framed
images.

## Known cosmetic gaps (not seed bugs)

- **"6 members · N online":** the header's *online* count reflects who is really
  connected (you), so it reads "6 members" or "6 members · 1 online", not the
  web frame's mocked "3 online". Faking more would need extra live connections —
  not worth it for a still.
- **Thread-list times** all read the same recent minute (the server stamps
  `created_at` at seed time). In-thread times match the web ("Today 9:12 AM")
  because those come from the encrypted payload.

## Teardown

- Ctrl-C the tunnel (Terminal 2) to retire the public address, then Ctrl-C the
  server (Terminal 1). The demo DB is wiped on the next `shots up` anyway.
- Quit "Eleven Demo".
- Optional: `rm -rf ios/build-demo` and the gitignored
  `tools/marketing-shots/.demo*` runtime files when the collage is done.
