Grid layout transitions — v10 sketch (anon-friendly: acequia.js bootstrap + ambient-first submit) (Firewatch Camera Grid)

**Note** from Bead: Firewatch Camera Grid · [canonical source](https://redfish.acequia.io/guerin/.agents/65783732-7907-4a36-983a-3b015e75e80b/2026-06-24/notes/04-grid-layout-v10-sketch.md) · session 2026-06-24 · discussion: Talk: Firewatch Camera Grid

Sketched by Debbie (senior UX/UI). **v10 patches v9's auth/identity + submit flow only** — it reuses the [v9 engine](03-grid-layout-v9-sketch.md) **wholesale** (same single-scene `lerp(mapPos, gridPos, t)` morph, position-preserving placement, MAP ◀▶ GRID scrubber, results dashboard, onboarding overlay, 2D/3D drag-pan — all intact). Two fixes from Stephen. **Rendered sketch:** [https://redfish.acequia.io/guerin/.agents/65783732-7907-4a36-983a-3b015e75e80b/2026-06-24/artifacts/sketches/grid-layout-transitions-v10.html](https://redfish.acequia.io/guerin/.agents/65783732-7907-4a36-983a-3b015e75e80b/2026-06-24/artifacts/sketches/grid-layout-transitions-v10.html) *(top bar reads `v10` + `build 2026-06-24` — freshness marker, house convention.)*

## The two fixes ### FIX A — include `/acequia.js` so anon/incognito visitors get an ambient cookie token The bug: an anonymous / incognito visitor's **cookie token was never set**, because nothing on the page loaded the acequia client that establishes it. v10 adds, as the **first element in `<head>`**: ```html <script type="module" src="/acequia.js"></script> ``` That root-relative client (served anonymously off the redfish origin, HTTP 200, no auth) **bootstraps the acequia framework**: it registers `/sw.js` and **establishes an ambient cookie token for anonymous/incognito visitors** (auto-generated keypair). So an anon/incognito voter now has a usable identity and can write to the dock **with no login**. It loads asynchronously, so the page treats `window.acequia` as **best-effort** — it polls briefly (≤2s) and never blocks the UI on it. A one-time green note in the onboarding overlay now says, in plain words, that **anonymous & incognito voting works** — an ambient identity is created automatically, no login needed. ### FIX B — ambient-first submit; token modal **only** on a write failure The bug: v9 prompted for a token **up front** before sending. v10 removes that gate entirely and reworks `sendAnswer()`: 1. **No upfront prompt.** The old `prompt(...)` / localStorage-token gate is gone. 2. **Ambient-first PUT.** On submit we PUT both files (`.md` + `.meta.json`) to the `request/` dock with **`credentials:'include'` and NO `Authorization` header** — relying on the ambient cookie token that acequia.js now guarantees. 3. **Modal only on auth failure.** *Only* if a PUT comes back **401 or 403** do we show a small **in-page token modal** (not a blocking `window.prompt`): paste an acequia bearer, persist it (`localStorage acequiaToken`), and **retry** the PUTs with `Authorization: Bearer <token>` + `credentials:'include'`. 4. **Non-auth failures** (network / 5xx) → a friendly error + "try again"; the token modal is **never** shown for those. Most voters never see the modal — it appears strictly after an ambient-token write is rejected.

## Layered, defensive identity (no pasted token required; raw token never persisted) Since most voters are now ambient/anon, identity is computed **without** requiring a pasted token, in three layers (`refreshIdentity()`): 1. **acequia-native** — poll `window.acequia` (≤2s); if `acequia.tokens.createUserToken` exists, mint a short-lived token, decode its JWT header/payload, derive a **principal** (`kid` / `iss` / `sub`), and set `tokenId = tok:` + short sha-256 of that principal. (`source: 'acequia'`.) 2. **readable token** — else if a token is readable (`localStorage acequiaToken`, or a non-HttpOnly `acequiaToken` cookie via `document.cookie`) → decode JWT → `sub` → `tokenId`. (`source: 'pasted'` / `'cookie'`.) 3. **local pseudonymous id** — else a stable `tok:local-xxxx` persisted in localStorage, clearly labeled as a local id. (`source: 'local'`.) The optional display name + transparency line are kept ("Voting as &lt;name&gt; · id `tok:…` — we record this id to attribute your vote, never your token"). The answer meta still records **only** `from` / `who` / `principal` / `tokenId` / `tokenClaims` (+ a new non-secret `tokenSource`) — **never the raw bearer** in the `.md` / `.meta.json`. The raw token rides only the `Authorization` header on the retry path. *Harness-verified:* a JWT whose signature segment is a known secret produces an md + meta in which neither the raw token nor the secret appears. The dashboard keeps showing token ids (never tokens).

## Kept from v9 (not regressed) The single-scene `lerp(mapPos,gridPos,t)` engine (zero create/destroy); position-preserving placement (NAIVE↔POSITION, crossings → 0); dim-ghost basemap; straight-lerp; MAP ◀▶ GRID scrubber; viewport-implicit selection + live filtering; 2D/3D drag-pan; per-card un-pin / re-flow; OrientedMap glyphs + tie-point ray; 3D oblique; TIME cursor; identity color; the 📊 results dashboard; the onboarding overlay + Buxton link; "Redfish Team" label. The 180° axis stays removed. `node --check` passes on the extracted inline script.

## The open fork (carried forward) — grid shape Unchanged from v9. Position-preserving placement is the **fix** (shown). The one open choice is the **grid's shape**, answerable by the team (vote → `request/` dock as a `sketch-answer`, now `sketch:'grid-layout-v10'`): - **A — aspect-follows-layout (FIRST-CUT).** rows×cols match the cameras' spatial bounding-box aspect. - **B — fixed (≤3 cols, reading order).** Predictable, dense.

## Status First-cut, team-shareable. Anonymous/incognito voting now works (acequia.js bootstraps the ambient cookie token); submit is ambient-first with an in-page token modal **only** on a 401/403; identity is layered + defensive and never persists the raw bearer; v9 engine + features intact. Awaiting the team's grid-shape fork choice via the `request/` dock.