diff --git a/AGENTS.md b/AGENTS.md
index 6fbbfee..b72ed35 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1542,19 +1542,38 @@ session with **no other memory of this project**. So:
it (`QRScannerModal`), calls `addMemberByIdentifier(gid, kpRef)`, and the new
device's `checkInvite` accepts the Welcome — no join request/founder round
trip. Member leaves can be removed by `leafIndex` (`removeMemberFromGroup` →
- `createCommit` with `defaultProposalTypes.remove`); the founder gets an
- **Edit Crew** link for admin-role changes. Messages have copy/reply actions
- (reply is quoted text; reactions/delete are still open). Live benchmark
- (`scripts/cordn-benchmark.ts`, 30 msgs): ~489ms/send, ~20ms/fetch, snapshot
- ~13.8KB (~460 B/message) — fine for hundreds of messages.
+ `createCommit` with `defaultProposalTypes.remove`). **Inline promote/demote
+ (2026-10-28):** the founder gets a shield button on each non-founder member row;
+ it republishes the **existing crew event tags** with the target's `p` tag
+ switched between plain and `admin` (only the founder can do this — an admin's
+ republish would fork the crew). Messages have copy/reply, **reactions
+ (👍/❤️/🔥/😂)**, **delete-for-everyone**, and **read receipts**. These are
+ encrypted MLS **control messages** (empty content + `reaction`/`unreact`/
+ `delete`/`read` tags) sent via `sendControl`; `useCordnNative` filters them out
+ of the visible list and derives reaction/read maps. Delete is a local
+ tombstone, not a relay hard-delete; automatic read receipts are silent-signer
+ only (Amber would prompt). Live benchmark (`scripts/cordn-benchmark.ts`, 30
+ msgs): ~489ms/send, ~20ms/fetch, snapshot ~13.8KB (~460 B/message).
+- **Cordn join-request push (2026-10-28):** `scripts/cordn-join-watcher.ts`
+ runs every minute (user timer) and polls the coordinator for known crew gids.
+ New requests become (a) a **NIP-17 DM** to the founder/admins (push service
+ wakes the phone) and (b) an **encrypted NIP-78** notice (`t=cordnjoinrequest`,
+ authored by the dedicated watcher key `82ce71cf…`). `useCordnJoinNotifications`
+ decrypts it; the bell's Crews/All tabs show a `CordnJoinRequestCard` with
+ Review/Dismiss, and count it by `seenAt`. Key `~/.config/skatespots/cordn-watcher.env`
+ (600); state `~/.local/state/skatespots-cordn-watcher/state.json`. Crews with a
+ **custom coordinator** are skipped (only the SkateSpots embedded coordinator is
+ queried); approved requests leave the coordinator but the bell card remains
+ until dismissed.
- **Map site-links strip (2026-10-05):** the desktop footer is hidden on the map
(full-bleed), so `components/MapFooterStrip.tsx` surfaces the same links
- (Newsletter/License/Privacy/Collaborators + "not sponsored by OpenSats") as an
- overlay inside the map container: a **slim full-width bar on desktop** and a
- **tiny collapsed "Links" chip on mobile** (expands to a compact scrollable row).
- Dismissible via `useMapFooterHidden` (`skatespots:map-footer-hidden`, same-tab
- synced), restorable in **Settings → Appearance**. The map's OpenStreetMap
- attribution moved to `md:bottom-7` so the bar doesn't cover it.
+ (DIY/Newsletter/License/Privacy + "not sponsored by OpenSats") as an overlay
+ inside the map container: a **slim full-width bar on desktop** and a **tiny
+ collapsed "Links" chip on mobile** (expands to a compact scrollable row).
+ **Always visible (2026-10-28):** the old dismissible `useMapFooterHidden`
+ behavior/setting was removed — it was easy to hide the only footer on the map
+ forever and then report it missing. The map's OpenStreetMap attribution moved
+ to `md:bottom-7` so the bar doesn't cover it.
- **Map layout — controls float (2026-09-29):** on mobile the `MapPage` toolbar
is **`absolute` over the map** (`pointer-events-none` wrapper + gradient +
`pointer-events-auto` inner) so the map fills the whole container; `md:static`
@@ -2627,6 +2646,15 @@ Rules:
- **Conversion:** Docker Python 3.11 + torch 2.4.1 + transformers 4.46.3 +
optimum 1.24; dynamic q8; `tokenizer.json` generated from the SentencePiece
models (needs protobuf + `sentencepiece_model_pb2.py`).
+- **Browser matrix runner (2026-10-28):** `scripts/translation-matrix.mjs`
+ (`npm run translation:matrix`) serves `dist` and drives the built
+ `translate.worker` in headless Chromium; `MATRIX_LANGUAGES=es,ca` limits it.
+ It writes `/tmp/opencode/translation-matrix.json` and a per-language partial.
+ First runs on this box: en↔es **~54s**, en↔ca **~21s** (model download
+ included for es); Basque downloads did not finish in a 25-minute run. This is a
+ real performance finding, not just missing test coverage: the 617 MB M2M100
+ single-threaded WASM is too slow for low-end phones. Low-end/APK verification
+ still needs a device (and likely a smaller model or native ONNX).
- **Still to do:** verify all 24 pairs in the browser/APK and measure
speed/memory on low-end phones. **1.7.2 and 1.7.3 are released** (the phone
needs the Zapstore update; the web deploy timer batches for 24h unless forced).
diff --git a/package.json b/package.json
index 74b1df4..d77ac11 100644
--- a/package.json
+++ b/package.json
@@ -27,7 +27,8 @@
"android:version": "node scripts/sync-android-version.mjs",
"android:build": "npm run android:build:apk && npx cap sync android && node scripts/sync-android-version.mjs && node scripts/inject-amber-queries.mjs && node scripts/inject-main-activity.mjs && node scripts/inject-network-security.mjs && node scripts/inject-signing-config.mjs && cd android && ./gradlew assembleRelease",
"android:build:apk": "CAPACITOR_BUILD=1 vite build && sed -i 's|||g' dist/index.html && sed -i 's|