# Empower Guyana — Canvass platform ## Specs & overview, version 3 (post LGE 2023 integration + polish rounds) > Author: Claude · Date: 2026-05-26 > Replaces: SPECS_OVERVIEW_v2.md (archived — see VERSION_HISTORY.md) > Audience: the pilot owner, contributors picking the code up, and the > weekly stand-up This update layers in everything shipped since v2: - A complete **LGE 2023** election dataset (80 of 80 LAAs · 980 candidates · 610 elected councillors), wired into the Map Explorer, Region Explorer, 2026 LAA Campaign Hub, and the voter detail dialog - A new top-level **2026 LAA Campaign Hub** view - Map Explorer **LGE 2023 color modes**, **region quick-jump pills**, and a **parliamentary-seats** color mode - A reusable **confirm-with-reason dialog** that replaced 56 native `prompt()` / `confirm()` calls (audit-clean export reasons, branded destructive confirms) - One major **mobile reflow pass** --- ## 1. What this is A two-product platform for **APNU field operations** during the Guyana 2026 Local Government Election: | Product | Path | Voter-level data | Default theme | |---|---|---|---| | **Empower Guyana — HQ** (Electoral Admin) | `output/admin-platform/` | No (aggregates only) | Empower Guyana · *Neutral Mode* toggle for external sharing | | **Empower Guyana — Field** (Canvassing) | `packages/canvass-web/` + `packages/canvass-api/` | Yes (field ops) | Empower Guyana · *Field Mode (dark)* toggle | Both share the **Empower Guyana** branding. HQ is aggregate-only by design. Field is the working surface this document covers. --- ## 2. Tech stack - **Node.js 22+** with experimental `node:sqlite` (no `better-sqlite3` dep) - **SQLite single file** at `output/canvassing/canvassing.sqlite` (~773k voters, 1,693 polling divisions, 80 local authorities, 10 regions) - **Vanilla HTML/CSS/JS** in `packages/canvass-web/public/` — no React/Vue, no build step. Bundle is one `app.js` + one `styles.css` + one `index.html` - **Custom design tokens** in `packages/canvass-shared/design-tokens/` - **Leaflet 1.9.4** (CDN) for the embedded Map Explorer — replaced the iframe-only viewer in v2; standalone viewer is still bundled for sharing - **No external chart library** — charts are inline SVG / DOM bars - **Stand-alone server** at `scripts/run-canvass-v1.mjs` → `packages/canvass-api/src/server.mjs`. Pilot mode binds to `127.0.0.1:8790` - Optional: Postgres path via `CANVASS_DB_URL` for production ### Files of note ``` packages/canvass-api/src/ server.mjs ─ HTTP routes + bootstrap auth.mjs ─ login() + selectDemoUser() (pilot-only) session.mjs ─ token issue + bundled cached overview services.mjs ─ overview cache, walk-list, search, lookup breakdowns.mjs ─ /api/breakdown + /api/data-rollup + caches print-templates.mjs ─ admin-configurable canvass print templates slice.mjs ─ role-aware SQL slicing electoral.mjs ─ regions + LAAs structure elections.mjs ─ NEW · multi-election history service (LGE 2023 + LGE 2023 constituencies + LAA-OCR councillors). In-process JSON cache; benign empty shapes when the pipeline hasn't run. campaign.mjs ─ election phase + polls-close handling mfa.mjs ─ TOTP enrolment / verification rate-limit.mjs ─ per-IP login + auth throttling packages/canvass-web/public/ index.html ─ shell, all view sections, voter dialog, SHARED CONFIRM dialog (Batch B) app.js ─ single SPA module (~12,000 lines) — see §6 styles.css ─ design tokens + view styles + polish + mobile reflow offline-store.js ─ IndexedDB queue + turf pack store sw.js ─ service worker (registered but off during pilot) brand/ ─ APNU logo PNG design-tokens/ ─ light/dark/neutral theme tokens output/elections/ lge2023.json ─ NEW · per-LAA results (80/80 coverage post master-merge: PPP/C 67, APNU 13) lge2023_constituencies.json ─ NEW · 507 constituency names extracted from GECOM sketch maps lge2023_tracking.xlsx ─ NEW · 80-LAA tracking workbook (5 sheets: Tracking, Results, National, Regional, Notes) lge2023_manual_review.xlsx ─ NEW · manual-entry tracker for LAAs OCR couldn't reach (now empty — 0 rows) lge2023_laa_ocr_audit.json ─ NEW · append-only merge audit log lge2023/forms/ ─ 80 GECOM Form 31 PDFs + 35 OCR sidecars lge2023/maps/ ─ 80 LAA boundary maps (terse names) lge2023/named_maps/dNN/ ─ 80 LAA maps with GECOM-canonical names (used to rebind 11 LAA bindings) output/LAA-OCR/ guyana_laa_master_national_workbook.xlsx ─ user-compiled OCR master laa_ocr_results.xlsx ─ NEW · 6-sheet deliverable (Candidates / Constituencies / LAA summary / Elected councillors / National overview / Notes) laa_ocr_constituencies.json ─ NEW · per-candidate JSON (980 rows, 608 constituencies, 610 elected councillors) laa_ocr_merge_payload.json ─ ingestion intermediate output/maps/ layers_manifest.json ─ 82 boundary layers geojson/ ─ converted GeoJSON for each layer results_region_summary_2025 ─ GE/RE 2025 by region (CSV + JSON) viewer/ ─ standalone Map Explorer (still bundled) map_assessment.md ─ join-risk catalogue scripts/ run-canvass-v1.mjs ─ npm start entry build-canvassing-db.mjs ─ rebuild SQLite from source PDFs seed-canvass-v1.mjs ─ seed demo users + sample canvass fetch-lge2023-forms.mjs ─ NEW · downloads 80 GECOM Form 31 PDFs fetch-lge2023-maps.mjs ─ NEW · downloads 160 boundary maps ocr-lge2023-scanned.py ─ NEW · Tesseract sidecar generator parse-lge2023-forms.py ─ NEW · Form 31 → lge2023.json parse-laa-ocr-master.py ─ NEW · master workbook → deliverables + merge payload merge-lge2023-laa-ocr.py ─ NEW · folds LAA-OCR into lge2023.json build-lge2023-tracking-xlsx.py ─ NEW build-lge2023-overview-deck.mjs ─ NEW build-lge2023-manual-review-tracker.py ─ NEW rebind-lge2023-by-named-maps.mjs ─ NEW · corrects 11 LAA bindings via GECOM named-map filenames extract-lge2023-constituencies.py ─ NEW · 507 names from sketch maps build-briefing-deck.mjs ─ leadership PPTX (this update) docs/ AGENTS.md ─ agent onboarding SPECS_OVERVIEW_v3.md ─ THIS doc SPECS_OVERVIEW_v2.md ─ superseded (archived) ``` --- ## 3. How the user moves around Sidebar (desktop ≥ 900 px) / bottom nav (mobile, now horizontal-scroll on phones — Batch F): ``` Overview ├── Home ─ welcome card, "Jump to" tiles, your grants └── Summary ─ 5 metric tiles + 4 charts + 2025 winners ★ 2026 LAA Campaign ─ NEW · top-level item · countdowns to LGE + pilot · LGE 2023 coverage tile (live: 80/80) · 80 LAAs / 10 regions / 5 munis tile strip · Top 10 LAAs by voters · LAAs by region bars · Focus areas grid (R4, swing reads, GOTV) · National map preview · LIVE 2026 vs 2023 baseline panel: · 5 competitiveness buckets · Top 10 opportunity targets (contested 2023 + low current contact) · Campaign milestones timeline (11 dates) · Recent platform activity feed Field operations ├── Areas ─ flat division list, district filter ├── Region Explorer ─ pick a region → 6 cards now: top divisions · support · occupation · households · 2025 result · 2023 LGE panel · LAA list. Compare-mode supports side-by-side. ├── Data Viewer ─ sortable table at regions/LAAs/divisions level, CSV export, row-click drills in ├── Map Explorer ─ native Leaflet · 3 layer modes: · 10 regions (GE 2025 + parliamentary seats) · 80 LAAs (LGE 2023 results) · NDC-level zoom (DORMANT — incomplete GIS) Color modes: · 2025 winner / 2025 turnout / 2025 PPP share · Parliamentary seats (per region) · 2023 LGE winner / 2023 competitiveness / 2023 PPP/C share / 2023 APNU share · Voter count LAA layer side panel shows: stats · 2023 LGE result card (party bars, seats, margin) · divisions list · elected councillors. Region quick-jump pills (All / R01–R10). ├── Canvass Sheet ─ 3-step: region → division → voter call sheet with 🖨 Print sheet (3 built-in + custom) ├── Phone Bank ─ one-voter-at-a-time call queue └── Turfs ─ create / list / assign turfs (finalize + reopen now use branded reason dialog) People ├── Teams & scripts ─ roster + call/door scripts └── Field health ─ stale canvasser detection + sync rejections Data & reports ├── Audited exports ─ CSV exports — all 31 export reasons now use branded dialog with live char counter + minLength enforcement (Batch B) └── Geography ─ electoral structure, LAAs, GIS link (stats-grid now actually styled — Batch F) My account └── 2FA / digests / queue ─ MFA disable dialog now branded (Batch B) Election day (phase-gated) └── E-day ─ coordinator dashboard, GOTV queue, hotline, poll watch, incidents Admin (role-gated) ├── Admin hub ─ landing with cards ├── Users & access ─ users, delegates, devices, roster import. Suspend/Offboard/Reset password all use branded confirm dialogs (Batch B) ├── Campaign config ─ phase, agreement, scripts, watermark, integrations, GIS crosswalk, ride providers, printable canvass sheet templates └── Audit & logs ─ export log, audit trail, notifications, digests ``` ### Voter detail dialog - **Dirty-state guard** (Batch C): any input flips a flag; close / Esc / cancel pops "Discard unsaved changes?". Successful save clears it silently. - **Autofocus** the Support select on open - **2023 LGE context badge** under voter meta: `2023 LGE · YOUR LAA · WINNER · share% · valid votes` — populated from the cached `/api/elections/lge2023` lookup; quiet-fails if no LAA match - Save-and-Next race fix — no more 100 ms setTimeout; awaits the PATCH and bails on read-only turfs ### Quick-find (top of every page) - Single search bar across **voters · divisions · regions** - Scope chips: All / Voters / Areas - Click result → drills to the right view (voter dialog, division walk list, or region explorer) - Debounced 220 ms; ↑↓ Enter Esc keyboard nav --- ## 4. Key API endpoints All routes under `/api/`. Auth via `Authorization: Bearer ` from `POST /api/auth/select` (pilot) or `POST /api/auth/login` (password + MFA). | Method | Path | Purpose | |---|---|---| | GET | `/api/health` | Server ready check, pilot demo accounts list | | GET | `/api/meta` | Schema version, voter count, division count, party policy | | POST | `/api/auth/select` | **Pilot one-click sign-in** | | POST | `/api/auth/login` | Password + MFA-aware login | | POST | `/api/auth/mfa/verify` | Submit TOTP code | | POST | `/api/auth/logout` | Drop session | | GET | `/api/me` | Current user profile + grants + flags | | GET | `/api/overview?lite=1` | **Hot dashboard summary** — cached, 2-3 ms | | GET | `/api/lookup?q=…&scope=…` | Quick-find unified search | | GET | `/api/breakdown?scope=…` | Comprehensive per-scope breakdown | | GET | `/api/data-rollup?level=…` | Flat table for Data Viewer | | GET | `/api/election-results` | **2025 GECOM region results JSON** (existing) | | GET | `/api/elections` | **NEW · registry — GE 2025 + LGE 2023** | | GET | `/api/elections/lge2023` | **NEW · full LGE 2023 dataset** (80 LAAs) | | GET | `/api/elections/lge2023/laa/:laa_id` | **NEW · single-LAA detail** | | GET | `/api/elections/lge2023/region/:region` | **NEW · region rollup** | | GET | `/api/elections/lge2023/constituencies?laa_id=` | **NEW · constituency names** | | GET | `/api/elections/lge2023/laa/:laa_id/councillors` | **NEW · elected councillors** with votes + voting % + margin | | GET | `/api/elections/lge2023/laa/:laa_id/progress` | **NEW · 2023 result merged with current canvass progress** | | GET | `/api/electoral/regions` | 10 regions with seat counts | | GET | `/api/electoral/local-authorities` | 80 LAAs | | GET | `/api/laas/:laa_id/divisions` | **NEW · divisions inside an LAA** with voter/contact counts | | GET | `/api/divisions?district=NN` | All divisions in a district | | GET | `/api/walk-list?…` | Paged voter list | | GET | `/api/voters/:uid` | Single voter detail + history + household | | PATCH | `/api/voters/:uid` | Update voter | | GET / PUT | `/api/print-templates` | Admin-configurable print templates | | GET | `/api/export/log` | Audited export history (super admin) | | POST | `/api/export` | Run a CSV export (reason code + MFA gate) | ### Cache strategy - **Overview cache**: SQLite-backed `analytics_rollups` table, 15 min TTL, warmed at server startup. Hot path is 2-3 ms. - **Breakdown + data-rollup caches**: in-process Map, 15 min TTL. - **Elections cache**: in-process JSON cache for `lge2023.json` and the LAA-OCR constituencies file. Loaded once on first request. - Cold start of all caches: ~55 s on the pilot dataset. --- ## 5. Data assets ### Voter records (unchanged from v2) - 773,165 voters - Fields include `laa_id` (mapped via 6-pass crosswalk, currently 99.4%) - Sourced from GECOM Official Lists of Electors ### Local government structure - 10 administrative regions - 80 local authority areas — 70 NDCs + 10 municipalities - 1,693 polling divisions - 237 LG constituencies (master GIS layer — incomplete; layer dormant) ### Election results — **2025 General Election** (unchanged) - `output/maps/results_region_summary_2025.csv` + `.json` - GE2025 + RE2025 totals by region for parties: PPP/C, APNU, AFC, WIN, ALP, FGM - Includes winner, turnout %, boxes, electors, total valid + rejected ### Election results — **2023 Local Government Election** (NEW) - `output/elections/lge2023.json` — per-LAA results - **100% coverage**: all 80 LAAs have a declared winner - PPP/C: **67 LAAs** · 107,612 votes · 442 seats - APNU: **13 LAAs** · 68,959 votes · 144 seats - Voters' Groups (15 distinct): 12+ seats across 80 LAAs (no outright LAA wins) - Smaller parties: MUD, ICP, PDM, AGNSP, TPM (no outright LAA wins) - 185,323 total valid votes · 3,092 rejected - Per-LAA fields: `party_votes`, `party_seats`, `winner`, `winner_share_percent`, `total_valid_votes`, `rejected_ballots`, `parse_status`, `text_source`, `laa_ocr_meta` (when merged from LAA-OCR) - `_pre_manual` block preserves pre-merge state for reversibility ### Election results — **2023 LGE constituency-level** (NEW) From `output/LAA-OCR/laa_ocr_constituencies.json`: - **980 candidate records** with name, party, votes, voting %, margin vs winner - **608 constituency summaries** with winner, runner-up, margin (absolute + %) - **610 elected councillors** identified - Compiled by the user across 9 OCR rounds → master workbook → master parser ### GIS layers (largely unchanged) - 82 boundary GeoJSONs in `output/maps/geojson/` - 80 LAA boundary maps + 80 named district maps in `output/elections/lge2023/{maps, named_maps}/` (NEW · downloaded May 25) - Honest disclosure: only 20 of 1,693 polling divisions match GIS boundary IDs ### Canvass-status (live, per-voter — unchanged) --- ## 6. Frontend architecture in one page `app.js` is one ES module (now ~12k lines, was 7.5k in v2). State now includes: ```js const state = { // global user/session state user, role, grants, permissions, lastMe, walk: { district, divisionNumber, turfId, offset, total }, walkRows, offlinePack, localPacksByTurf, divisions, divisionsCacheKey, overview, customPrintTemplates, printColumnCatalog, activeVoterUid, activeVoterRevision, voterDialogDirty, // NEW (Batch C) }; const canvassState = { regions, divisionsByDistrict, selectedDistrict, … }; const regionExplorerState = { selectedDistrict, compareDistrict, … }; const dataViewState = { level, district, filter, rows, sortKey, sortDir }; const phonebankState = { district, filter, queue, index, isLoaded }; const mapState = { pendingDistrict, iframeReady }; const leafletMap = { map, regionsLayer, laasLayer, constituenciesLayer, laasGeoJson, laaMeta, lge2023ByLaa, // NEW · LGE 2023 cache regionBoundsByLayer, // NEW · pill targets selectedDistrict, selectedLaaId, layerMode, colorMode, … }; const laaHubState = { loaded, loading, lge2023Coverage, … }; const quickfindResults = []; quickfindActiveIndex = -1; quickfindScope = "all"; ``` Each view has a `loadXxx()` and a `bindXxx()`. `setView(name)` is the only navigation primitive. `api(path, opts)` is the single network function — uses `fetch`, 30 s default timeout, attaches the session token automatically, surfaces HTTP errors with specific messages (404 → "Endpoint missing — restart the server", 403/401 similarly tagged). ### Cross-cutting helpers (NEW in v3 territory) - `confirmDialog(opts)` — shared modal replacing 56 native prompts/confirms - `confirmYesNo(title, body, opts)` — `if (!(await confirmYesNo(…))) return;` - `confirmDestructive(title, body, opts)` — destructive action with optional reason textarea - `promptExportReason(scope)` — wraps `confirmDialog` with 20-char minLength, live counter, audit-log hint - `toast(message, type, opts)` — existing; many `alert()` calls migrated to it - `_loadLge2023ByLaa()` — lazy-cached fetch of LGE 2023 for the Map Explorer ### Map Explorer (extensive new code) - 3 layer modes (regions / laas / constituencies-dormant) - 9 color modes (3 GE 2025, 4 LGE 2023, parliamentary seats, voter count) - Region quick-jump pill bar (only on layers that benefit) - LAA layer side panel composed of: stats tiles → 2023 LGE result card → divisions list → elected councillors list (3 async fetches in parallel) --- ## 7. Known limitations / pending work ### Voter → LAA crosswalk **RESOLVED IN PROGRESS** — was 0% in v2, now **99.4%** of 773k voters carry a `laa_id`. Six-pass derivation pipeline: division names · addresses · GeoJSON vocabulary · NDC fallback · hand-curated R4 + R6 villages · numbered-village heuristic. Admin → Campaign config → LAA crosswalk runner exists for the remaining 0.6%. ### LGE 2023 LAA-id binding **RESOLVED** — `rebind-lge2023-by-named-maps.mjs` corrected 11 LAA bindings in R02, R03, and R05 by using GECOM's named-map filenames as ground truth. Pre-rebind state preserved at `output/elections/lge2023/inventory.json.pre-rebind.bak`. ### Polling-division boundaries **UNCHANGED** — only 20 of 1,693 voter-list division numbers match exactly to GIS boundary IDs. Region- and LAA-level color works (Map Explorer); division- level coloring needs more crosswalk work. The 237-feature "LG constituencies" GIS layer was investigated and removed from the Map Explorer in this round (160 of 237 polygons had no NDC tag, only 10 of 80 NDCs had any named coverage). Code is preserved in `app.js` under `_activateConstituenciesLayer` for re-enable when complete data lands. ### Map integration depth **RESOLVED** — Native Leaflet shipped in this update with all the canvass- context overlays the v2 doc anticipated for "option B" (color by 2023 winner, competitiveness, party share; click for councillor list). ### Mobile pinch-zoom on map Leaflet handles touch gestures natively. Resolved by the native-Leaflet swap. ### Service worker Still off during pilot (was causing stale-cache freezes). ### Trend over time **UNCHANGED** — no week-over-week tracking of contact rate, support shift, sync health yet. Schema can support it; renderers need to plot over time. --- ## 8. Deployment (unchanged from v2) ### Pilot (current default) ```bash npm run build:db # rebuild SQLite from PDFs (one-time) npm run seed # seed demo users + sample canvass data npm start # boots server on http://127.0.0.1:8790 ``` Cache warmup is automatic on `npm start` (~55 s on cold cache). ### LGE 2023 pipeline (new) If reproducing from scratch: ```bash node scripts/fetch-lge2023-forms.mjs --download node scripts/fetch-lge2023-maps.mjs --download python scripts/ocr-lge2023-scanned.py python scripts/parse-lge2023-forms.py # Optionally drop a master OCR workbook into output/LAA-OCR/ and: python scripts/parse-laa-ocr-master.py python scripts/merge-lge2023-laa-ocr.py # Then rebuild the trackers + decks: python scripts/build-lge2023-tracking-xlsx.py node scripts/build-lge2023-overview-deck.mjs python scripts/build-lge2023-manual-review-tracker.py ``` --- ## 9. Changelog since v2 (2026-05-23 → 2026-05-26) ``` LGE 2023 INTEGRATION ───────────────────── + Stage 1: fetch-lge2023-forms.mjs · 80 GECOM Form 31 PDFs downloaded + Stage 2: parse-lge2023-forms.py · text-PDF parsing (45 of 80 clean) + Stage 2b: ocr-lge2023-scanned.py · Tesseract sidecars (35 created) + Stage 3: lge2023.json · per-LAA results dataset + Stage 4: /api/elections/lge2023 routes + Stage 5: lge2023_tracking.xlsx (5 sheets · all 80 LAAs status-coded) + Stage 5b: lge2023_manual_review.xlsx · pre-filled tracker (23 → 0 rows) + Stage 6: fetch-lge2023-maps.mjs · 160 boundary maps + Stage 7: rebind-lge2023-by-named-maps.mjs · 11 LAA bindings corrected + Stage 8: extract-lge2023-constituencies.py · 507 constituency names + Stage 9: parse-laa-ocr-master.py · master workbook → 980 candidates, 608 constituencies, 610 elected councillors + Stage 10: merge-lge2023-laa-ocr.py · folds master into lge2023.json + RESULT: 80/80 LAAs with declared winner. PPP/C 67, APNU 13. MAP EXPLORER ──────────── + Layer: 80 LAAs · LGE 2023 results + Layer: 237 constituencies · NDC-level zoom (added then removed — source data was incomplete; code preserved dormant) + 4 LGE 2023 color modes (winner / competitiveness / PPP & APNU share) + Parliamentary seats color mode + tooltip + region side-panel pill + Region quick-jump pills (All · R01–R10) on LAA layer + LAA side panel: 2023 LGE result card + elected councillors list + Tooltip enriched with seats won + Auto-switch to lge2023_winner color mode on first LAA layer entry + Coverage pill (pulsing) on LAA layer intro 2026 LAA CAMPAIGN HUB (NEW VIEW) ───────────────────────────────── + Top-level "★ 2026 LAA Campaign" sidebar item + Countdowns (LGE date, pilot date, LGE 2023 coverage tile) + 5-metric tile strip + Top 10 LAAs by voters + LAAs by region bars + Focus areas grid + National map preview + LIVE 2026 vs 2023 baseline panel: 5 competitiveness buckets + Top 10 opportunity targets (contested 2023 + low current contact) + Campaign milestones timeline + Recent platform activity feed REGION EXPLORER ─────────────── + New "2023 Local Government Election" chart card per region: winner counts by party · party-share bars · per-LAA results table + Compare-mode for side-by-side region analysis VOTER DETAIL ──────────── + 2023 LGE context badge ("2023 LGE · YOUR LAA · winner · share%") + Dirty-state guard (Batch C) + Autofocus Support select on open + Save-and-Next race fix + voter-meta line no longer shows raw server_revision POLISH ROUNDS ───────────── + Batch A · stale text & numbers (8 fixes) + Batch C · voter dialog hardening (5 fixes) + Batch F · mobile reflow (5 fixes + Geography stats-grid styles) + Batch B · shared confirm dialog · replaced 56 native prompt()/confirm(): · 31 export reasons (promptExportReason with live counter) · 6 free-form reason prompts (turf finalize/reopen, hotline resolve, suspend, offboard, restore voter) · 18 destructive confirms · MFA disable (code input) · Daily contact goal (numeric input) + About → "What's new" refreshed + Bottom-nav horizontal scroll on phones DELIVERABLES (for non-engineer audiences) ───────────────────────────────────────── + Empower_Guyana_Briefing.pptx (17 slides · leadership briefing) + LGE2023_Results_Overview.pptx (12 slides · campaign reference) + guyana_laa_master_national_workbook.xlsx (user-compiled OCR master) + laa_ocr_results.xlsx (6 sheets · 980 candidates + 608 constituencies + 610 councillors) + lge2023_tracking.xlsx (5 sheets · 80-LAA status tracker) ``` Plus the cache version bumps `?v=73 → ?v=80` for browser-pickup. --- ## 10. Showcase demo path (updated) 1. **One-click sign-in** as NATIONAL 2. **Home / Summary** — show the LGE 2023 results land in the right places (Summary's 2025 winners chart still there; 80/80 LGE 2023 visible on the Hub) 3. **★ 2026 LAA Campaign** — open the new top-level view: · countdowns to LGE + pilot · "Live 2026 vs 2023 baseline" — point at the opportunity targets list (e.g. R03 contested LAAs at low current contact %) 4. **Map Explorer** — Layer "80 LAAs" auto-flips Color to "2023 winner". Pill bar appears below. Click R04 → zooms to Demerara-Mahaica's 18 LAAs. Click Georgetown polygon → side panel shows: 2023 LGE result card (APNU 60.9% winner · party bars) → divisions list → 15 elected councillors. 5. **Map Explorer** — Color "Parliamentary seats" — R04 jumps out (7 seats); R08/R09 lightest (1 each). Side panel shows the 65-seat breakdown. 6. **Region Explorer** — pick R10 (Linden) → scroll past the 2025 panel to the new **2023 LGE** card: APNU wins both LAAs, 76.5% in Linden, 55.5% in Kwakwani. 7. **Open a voter in Linden** — point at the "2023 LGE · YOUR LAA · APNU 76.5%" badge as canvasser conversation context. 8. **Canvass sheet → 🖨 Print** — show landscape PDF still works. 9. **Audited Exports → click an Export** — show the new branded reason dialog: live char counter (0 / min 20), green at threshold, audit hint inline. Cancel without losing message context. 10. **Admin → Users → Suspend** — show the single (no longer doubled) branded dialog with optional reason textarea + success toast. Total runtime: ~12 minutes if clicked steadily.