diff --git a/deployment/src-tauri/README.md b/deployment/src-tauri/README.md
index 18ed859b..3694170a 100644
--- a/deployment/src-tauri/README.md
+++ b/deployment/src-tauri/README.md
@@ -49,9 +49,9 @@ the Tauri origin** — exactly the surprise `capacitor.config.json` documents fo
**The fix is a response header on the token host, not a change in `tauri.conf.json`.**
Measured 2026-09-21: `https://doxa.life/api/maps/token` → **200**;
-`https://nuxtbase.app/api/maps/token` → **404**. A desktop build pointed at nuxtbase would launch
+the test host's `/api/maps/token` → **404**. A desktop build pointed at that host would launch
with blank basemaps even though the window opens and the element mounts correctly. Separately, the
-Mapbox token is **URL-restricted**: with a `nuxtbase.app` Referer the style document returns 200 but
+Mapbox token is **URL-restricted**: with the test host as Referer the style document returns 200 but
every **tile 403s**, while the same token with a `doxa.life` Referer returns 200. Verify the token
path before blaming the shell.
diff --git a/deployment/wordpress/doxa-maps-embed/HOW-TO-TEST.md b/deployment/wordpress/doxa-maps-embed/HOW-TO-TEST.md
index f6428af7..5c35681b 100644
--- a/deployment/wordpress/doxa-maps-embed/HOW-TO-TEST.md
+++ b/deployment/wordpress/doxa-maps-embed/HOW-TO-TEST.md
@@ -28,7 +28,7 @@ Then in wp-admin (http://localhost:8888/wp-admin, admin/password):
1. Plugins → activate **Doxa Maps Embed**.
2. Settings → **Doxa Maps** → set the catalog base to a real served tree, e.g.
- `https://doxa.life/js/doxa-maps` (or your own CDN / GitHub Pages / doxa-base Forgejo host).
+ `https://doxa.life/js/doxa-maps` (or your own CDN / GitHub Pages / a Forgejo host you run).
3. Create a page and add the shortcode:
```
diff --git a/deployment/wordpress/tests/wp-embed-contract-test.php b/deployment/wordpress/tests/wp-embed-contract-test.php
index 7c018d26..ea279adf 100644
--- a/deployment/wordpress/tests/wp-embed-contract-test.php
+++ b/deployment/wordpress/tests/wp-embed-contract-test.php
@@ -52,7 +52,7 @@ $out = do_shortcode_call('doxa_map', [
'bundle' => $target['bundle'],
'element' => $prof['element'],
'profile' => $prof['id'] ?? $prof['name'] ?? $target['bundle'],
- 'base' => 'https://nuxtbase.app/plugin-assets/'.$tree,
+ 'base' => 'https://cdn.example.org/plugin-assets/'.$tree,
'integrity' => $integrity,
]);
global $SCRIPTS, $INLINE;
@@ -76,7 +76,7 @@ $cfg = json_decode($json, true);
$assert('profile-config is valid JSON', is_array($cfg), $json ? substr($json,0,60) : 'no match');
// 3. the script src equals the manifest's catalog path for this bundle
$src = reset($SCRIPTS)['src'] ?? '';
-$want = "https://nuxtbase.app/plugin-assets/{$tree}/{$target['bundle']}/{$target['bundle']}.js";
+$want = "https://cdn.example.org/plugin-assets/{$tree}/{$target['bundle']}/{$target['bundle']}.js";
$assert('script src matches catalog path', $src === $want, $src);
// 4. SRI: the filter must add integrity to the emitted ", $h2, 'x');
diff --git a/documentation/README-embed-gospel-resources.md b/documentation/README-embed-gospel-resources.md
index f7f7d32e..ff06c2e5 100644
--- a/documentation/README-embed-gospel-resources.md
+++ b/documentation/README-embed-gospel-resources.md
@@ -9,7 +9,7 @@ again, so every parameter below is a promise kept by a gate
style="width:100%;height:600px;border:0" title="Gospel access">
```
-## The four addresses
+## The five addresses
| Address | What the reader gets |
|---|---|
@@ -17,6 +17,7 @@ again, so every parameter below is a promise kept by a gate
| `?country=` | That country's sheet open, framed on its groups. The country's own name also works (`?country=India`). |
| `?code=` | The people group's sheet, with its resources. `&name=` and `&country=` disambiguate when a language has many groups. |
| `?view=zero` | The groups with **nothing found** — the question this map exists for. `?view=gap` is the same view. |
+| `?tab=province&province=` | On the data tables: that state's row — groups, people, what has nothing, the local way in, and who nobody is praying for. Accents are folded, so `Arunachal Pradesh` finds `Arunāchal Pradesh`. |
`?provider=maplibre` is not this map's parameter but the whole fleet's: it selects the keyless
engine, and no map ever substitutes a different one (`bun run check:engine-contract`).
diff --git a/documentation/reference-library-index.md b/documentation/reference-library-index.md
index f79a1bde..798be755 100644
--- a/documentation/reference-library-index.md
+++ b/documentation/reference-library-index.md
@@ -147,6 +147,8 @@ and a change here can break other maps.
`import … from '@map/composables/useMapSelection.js'`
- **useMapTheme** — useMapTheme.js — shared light/dark theme for every map profile.
`import … from '@map/composables/useMapTheme.js'`
+- **useOptionalT** — ★ NO .-SHAPED EXAMPLES IN THIS FILE'S COMMENTS. check:i18n-keys-exist reads any such
+ `import … from '@map/composables/useOptionalT.js'`
- **usePosterPrints** — usePosterPrints.js — THE print actions every poster wall shares.
`import … from '@map/composables/usePosterPrints.js'`
- **usePplrInstance** — usePplrInstance.js — per-map "instance store" mediator.
diff --git a/internal/check-approx-pin-note.mjs b/internal/check-approx-pin-note.mjs
index 3de198bc..88f0eede 100644
--- a/internal/check-approx-pin-note.mjs
+++ b/internal/check-approx-pin-note.mjs
@@ -29,6 +29,14 @@ check('three groups on one point find each other', colocatedWith(idx, rows[0]).s
check('a GeoJSON feature is matched to rows on the same point', colocatedWith(idx, rows[2]).sort().join() === 'Godda,Koraga', colocatedWith(idx, rows[2]))
check('a lone point shares with nobody', colocatedWith(idx, rows[3]).length === 0, colocatedWith(idx, rows[3]))
check('a row without a position is ignored', colocatedWith(idx, rows[4]).length === 0, colocatedWith(idx, rows[4]))
+// A click hands over the RENDERED feature: the engine rebuilt its geometry from tile coordinates, so it is
+// near the roster point but not equal to it: ~0.001 degree at low zoom (rega, row 46, a shared point that never matched).
+const rendered = { type: 'Feature', properties: { name: 'Koraga' }, geometry: { type: 'Point', coordinates: [75.2486, 12.8431] } }
+check('a rendered (tile-rounded) feature still finds its co-located groups', colocatedWith(idx, rendered).sort().join() === 'Godda,Maila', colocatedWith(idx, rendered))
+// The same people in two countries: the nearest roster row wins, not the first.
+const twin = buildColocationIndex([...rows, { name: 'Koraga', latitude: 40, longitude: 40 }, { name: 'Other', latitude: 40, longitude: 40 }])
+const renderedFar = { type: 'Feature', properties: { name: 'Koraga' }, geometry: { type: 'Point', coordinates: [40.0007, 39.9994] } }
+check('a repeated name resolves to the nearest roster point', colocatedWith(twin, renderedFar).join() === 'Other', colocatedWith(twin, renderedFar))
const { createServer } = await import('vite')
const { default: vue } = await import('@vitejs/plugin-vue')
diff --git a/internal/check-gospel-embed-contract.mjs b/internal/check-gospel-embed-contract.mjs
index 3d6f072f..e16a02a8 100644
--- a/internal/check-gospel-embed-contract.mjs
+++ b/internal/check-gospel-embed-contract.mjs
@@ -108,7 +108,43 @@ try {
if (!c.mounted) fails.push('?country=ZZZ: the leaf did not mount — a bad parameter must never break the embed')
else console.log(' ok ?country=ZZZ'.padEnd(34) + 'unknown country ignored, leaf still renders')
- /* 4 — the view that already existed, kept honest */
+ /* 4 — a province deep-links into the TABLE, diacritics folded. The map writes "Arunāchal
+ Pradesh" (geoBoundaries' shapeName); a partner or a reader types the plain ASCII, and a link
+ that only works for whoever copied it off the map is not an address. */
+ const prov = JSON.parse(readFileSync(resolve(PUBLIC, TREE_NAME, 'assets/tables/gospel-by-province.json'), 'utf8'))
+ const accented = (prov.rows || []).map((r) => r.province).find((n) => /[^\x00-\x7F]/.test(n))
+ if (!accented) fails.push('no accented province name in the table — the diacritic arm would pass vacuously')
+ else {
+ const plain = accented.normalize('NFD').replace(/[\u0300-\u036f]/g, '')
+ const tbl = `${BASE}/${TREE_NAME}/gospel-resources/data-tables/index.html`
+ const readTable = async (query) => {
+ await page.goto(`${tbl}${query}`, { waitUntil: 'domcontentloaded', timeout: 60000 })
+ await page.waitForTimeout(12000)
+ return page.evaluate((tag) => {
+ /* Every leaf in this bundle is served through the SAME custom element; the profile is what
+ differs. Guessing a per-leaf tag name here returned null and read as "0 rows". */
+ const root = document.querySelector(tag)?.shadowRoot || document
+ return {
+ rows: root.querySelectorAll('.tabulator-row').length,
+ /* NOT sliced. The first version cut this to 300 characters and then asked whether the
+ province name was in it: the row was on screen, 400 characters down, and the gate
+ reported the product broken. A slice belongs in the MESSAGE, never in the predicate. */
+ text: (root.textContent || '').replace(/\s+/g, ' ').trim(),
+ }
+ }, TAG)
+ }
+ const hit = await readTable(`?tab=province&province=${encodeURIComponent(plain)}`)
+ if (hit.rows !== 1) fails.push(`?province=${plain}: ${hit.rows} row(s) rendered, expected exactly 1 — the diacritic fold is not working`)
+ else if (!hit.text.includes(accented)) fails.push(`?province=${plain}: the row rendered but "${accented}" is not on screen`)
+ else console.log(` ok ?tab=province`.padEnd(34) + `"${plain}" found "${accented}", 1 row`)
+
+ const miss = await readTable('?tab=province&province=Nowhereshire')
+ if (miss.rows !== 0) fails.push(`?province=Nowhereshire: ${miss.rows} row(s) — an unknown province must match nothing, not everything`)
+ else if (!/\b0\b/.test(miss.text)) fails.push('?province=Nowhereshire: nothing on screen tells the reader the filter matched nothing')
+ else console.log(' ok ?tab=province (unknown)'.padEnd(34) + 'matches nothing and says so')
+ }
+
+ /* 5 — the view that already existed, kept honest */
const d = await read(page, '?view=zero')
if (!d.mounted) fails.push('?view=zero: the leaf did not mount')
else if (!d.text.length) fails.push('?view=zero: the leaf rendered nothing')
@@ -125,7 +161,7 @@ try {
for (const f of fails) console.error(` ${f}`)
process.exit(1)
}
- console.log(`EMBED-CONTRACT: PASS — ${matched} of ${total} pins for a three-code link, a country opens from its ISO3 alone, an unknown one is ignored, and ?view=zero still renders.`)
+ console.log(`EMBED-CONTRACT: PASS — ${matched} of ${total} pins for a three-code link; a country opens from its ISO3 alone and an unknown one is ignored; a province deep-links into the table with its diacritics folded and an unknown one matches nothing; ?view=zero still renders.`)
} finally {
try { await browser?.close() } catch (_) {}
try { srv?.close?.() } catch (_) {}
diff --git a/internal/check-legend-logical-insets.mjs b/internal/check-legend-logical-insets.mjs
new file mode 100644
index 00000000..8f7bc516
--- /dev/null
+++ b/internal/check-legend-logical-insets.mjs
@@ -0,0 +1,93 @@
+#!/usr/bin/env node
+/**
+ * check-legend-logical-insets.mjs — the legend components position themselves with LOGICAL insets.
+ *
+ * WHY: the shared legend (.stl-panel, its reopen button, the detail buttons, the tree-guide hairlines) used
+ * `left: 8px`. MapToolbar uses `inset-inline-end`, which mirrors under dir="rtl"; a physical `left` does not.
+ * So in Arabic the controls moved left onto a legend that stayed left, on three production maps at once
+ * (Simple Research, Prayer Progress, UPG-100; rega's survey at 52eeb0db). The fix is inset-inline-start /
+ * inset-inline-end; this gate keeps a physical left/right from coming back.
+ *
+ * WHAT IT READS: every CSS declaration in library/components/legends/*.vue (comments stripped). A
+ * declaration `left:` / `right:` / `padding-left|right:` / `margin-left|right:` fails unless it is:
+ * - a centring or full-width rule, which is symmetric under RTL (`left:50%`, `left:0` beside `right:0`);
+ * - a reset to auto (`left:auto`), which undoes rather than places;
+ * - listed in ALLOWED below with the reason it may stay physical.
+ * Inline :style bindings computed in JS (the progress bar's popover) are not CSS declarations and are not read.
+ *
+ * --self-test poisons a copy of one rule and asserts the gate notices: a gate that cannot fail proves nothing.
+ */
+import { readFileSync, readdirSync } from 'node:fs'
+import { join, dirname } from 'node:path'
+import { fileURLToPath } from 'node:url'
+
+const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
+const DIR = join(ROOT, 'library/components/legends')
+
+// file -> substrings of declarations that may stay physical, each with its reason.
+const ALLOWED = {
+ 'LegendMobile.vue': [
+ ['.mapboxgl-ctrl-attrib.mapboxgl-compact{margin-right', "the engine's own attribution control, which the engine places"],
+ ],
+ 'LegendProgressBar.vue': [
+ ['.lpb-pop{', 'the popover is placed by JS (popLeft) over the bar segment it describes'],
+ ],
+}
+
+const PHYSICAL = /(^|[;{\s])((?:padding|margin)-(?:left|right)|left|right)\s*:\s*([^;}]+)/g
+
+function findings(file, src) {
+ const out = []
+ const styles = [...src.matchAll(/