Grid layout transitions — v12 sketch (feedback ballot-box token; layered own→ballot→modal auth) (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/06-grid-layout-v12-sketch.md) · session 2026-06-24 · discussion: Talk: Firewatch Camera Grid

Sketched by Debbie (senior UX/UI). **v12 is one surgical change on top of [v11](05-grid-layout-v11-sketch.md)** — it reuses v11's whole engine + UX wholesale. The change is the **write credential** for the ballot box, so anonymous/incognito visitors can vote with **no login and no paste-token modal**. **Rendered sketch:** [https://redfish.acequia.io/guerin/.agents/65783732-7907-4a36-983a-3b015e75e80b/2026-06-24/artifacts/sketches/grid-layout-transitions-v12.html](https://redfish.acequia.io/guerin/.agents/65783732-7907-4a36-983a-3b015e75e80b/2026-06-24/artifacts/sketches/grid-layout-transitions-v12.html) *(top bar reads `v12` + `build 2026-06-24` — freshness marker, house convention.)*

## Why v12 — the feedback ballot-box model v11 wrote each vote **under the visitor's own acequia identity**. Proven empirically (Stephen, incognito DevTools): a fresh **incognito acequia identity is authenticated but has no write scope** to Stephen's `guerin/` namespace → the WebDAV write **401s**, which forced the paste-token modal. That is exactly the friction a "drop a vote" affordance must not have. Stephen's instruction: *"make an api token only for writing to the feedback directory and use that for incognito."* So v12: 1. **Renames the ballot-box dir `votes/` → `feedback/`** everywhere — both the write path and the dashboard read path are now [`/guerin/.agents/65783732-…/feedback/`](https://redfish.acequia.io/guerin/.agents/65783732-7907-4a36-983a-3b015e75e80b/feedback/). Vote filename stays one-per-voter: `grid-layout-v12__<voterKey>.json` (`voterKey` = sanitized `userId`, last-write-wins). The `sketch` field is now `grid-layout-v12`; the **dashboard still tallies any `grid-layout*` file**, so earlier v11 votes still show. 2. **Embeds a ballot-box token** as a const: `const BALLOT_TOKEN = "<JWT>";`. It is a **server-attested chain token, scoped write-only to `feedback/`** (verified: write→feedback = 201, write anywhere else = 403), **180-day, revocable**.

## The token (scope + revocation) - **tokenId:** `hxhs9al8o865ljoj3ijet4d9` - **scope:** `writePaths: ["/guerin/.agents/65783732-…/feedback/*"]` — write-only, this directory only. It cannot read, and cannot write anywhere else in the namespace (a deliberately tiny blast radius). - **server-attested:** a PS256 chain token signed by `server-redfish.acequia.io`, delegated from a parent token (`depth 1`, `max_depth 4`). - **lifetime:** `iat 2026-06-26` → `exp ~2026-12` (180 days). - **revoke:** `DELETE /auth/tokens/hxhs9al8o865ljoj3ijet4d9` kills it immediately (apoptotic — the embedded copy stops authorizing writes the moment it's revoked). This is the [cloud-auth "goes-outa" membrane](../../../../..) pattern at sketch scale: a **scoped, revocable write gate** rather than handing visitors broad credentials. It is the one **intentionally-embedded write credential**; per feedback_browser-is-a-parciante-node there is no taboo on placing a key in the browser when its scope × blast-radius × revocability is right — and here it is minimal (write-only, one dir, revocable).

## Layered auth (own → ballot → modal) `submitVote` builds the vote with the **visitor's own acequia identity** (`getDeviceName`/`getUserId`/`getDeviceId`; local pseudonymous fallback if acequia is absent) — **this identity is recorded for attribution regardless of which credential authorizes the write**. Then `writeVote` tries, in order: 1. **The visitor's OWN authenticated write first** — `acequia.webdav.createAcequiaClient().putFileContents(...)`. An owner/granted user (e.g. Stephen) writes **under their own identity** (201). 2. **On auth failure (401/403) → fall back to the `BALLOT_TOKEN`** — a plain `fetch(PUT, { Authorization: 'Bearer ' + BALLOT_TOKEN })`. This is the **incognito/anon path**; it succeeds **silently — no modal**. 3. **The paste-token modal is a last resort only** — it appears only if **both** the own-identity write **and** the ballot-token write fail (should basically never happen). It is kept for completeness but should rarely appear. The **vote JSON never contains a raw user token** — only the identity (name / userId / deviceId) + a `tokenId` fingerprint (`tok:<shortHash(userId)>`) + `source`. The raw bearer (own or ballot) rides **only** the `Authorization` header. The dashboard shows the `tokenId`, never the token.

## Transparency copy (updated) Identity line + onboarding overlay now read: *"Your vote is signed into a shared **feedback ballot box** (a write-only key scoped to just this directory) so anonymous/incognito works with no login; we still record your acequia **token id** for attribution — never your token."* The Buxton sketch link is kept.

## What's kept intact (v8–v11, not regressed) Single-scene `lerp(mapPos, gridPos, t)` morph (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; onboarding overlay + Buxton link; "Redfish Team" label; the **5s-polling results dashboard** (dedup by userId, tally bars + comment feed with each voter's name + tokenId, live status line, anonymous read of `feedback/`). The 180° axis stays removed.

## The fork (unchanged) — grid shape - **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. **Auth: layered own → ballot → modal.** Incognito now votes with no login and no modal via the write-only, revocable `feedback/` ballot token (`hxhs9al8o865ljoj3ijet4d9`); granted users still write under their own identity; the modal is a never-expected last resort. Identity attribution kept; no raw user token in any vote file. `node --check` passes on the extracted inline script. Awaiting the team's grid-shape fork choice in the ballot box.