diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml
new file mode 100644
index 000000000..d0262c0c3
--- /dev/null
+++ b/.forgejo/workflows/build.yml
@@ -0,0 +1,120 @@
+# build.yml — the SINGLE-BUILDER CI gate for the doxa-maps bundler.
+#
+# WHY THIS EXISTS (IDEA-FORGEJO-CICD-BUILD-LOCK-MULTIWRITER):
+# Many agents edit + commit source to this repo in PARALLEL. That is safe. What is
+# NOT safe is many agents each running `bun run build` into the shared, committed
+# web-root (public/): concurrent builds clobber each other's output and race the
+# `git add public/` that follows. internal/build-lock.sh makes that STRUCTURALLY
+# impossible on ONE machine (an flock mutex); this workflow does the same thing at
+# CLUSTER scope — it makes CI the ONE-AND-ONLY builder, and serializes it.
+#
+# THE CONTRACT for a swarm agent:
+# edit files -> git commit -> git push (DONE. never run `bun run build`.)
+# The push lands here. This job — and ONLY this job — runs the authoritative build
+# and commits the regenerated public/ back. See the handoff doc:
+# _qa-cycle/03-deployment-cycle/delegation/HANDOFF-forgejo-cicd-buildgate.md
+#
+# NOTE ON PATH: Forgejo reads .forgejo/workflows/ (and .gitea/workflows/), NOT
+# .github/workflows/. The existing .github/workflows/pages.yml is a GitHub-Pages
+# workflow and never runs here; this is its Forgejo graduation.
+
+name: build-gate
+
+# Trigger on every push to the source branches. workflow_dispatch lets the Driver
+# run it by hand from the Forgejo UI.
+on:
+ push:
+ branches:
+ - main
+ - doxa-maps-plugin-studio
+ # Source-only edits trigger a build. Skip pushes that touch NOTHING but the
+ # build output or CI itself — the build-output commit this job makes would
+ # otherwise re-trigger the job in a loop.
+ paths-ignore:
+ - 'public/**'
+ - '.forgejo/**'
+ workflow_dispatch:
+
+# THE HEART OF THE GATE. All builds share ONE concurrency group, so Forgejo runs
+# at most one at a time. A push that arrives while a build is running is QUEUED,
+# not cancelled and not failed — cancel-in-progress is FALSE on purpose: we want
+# every push's source to be built in order, never dropped. This is the flock
+# nonblock/queue behaviour of build-lock.sh, expressed as a scheduler primitive.
+concurrency:
+ group: doxa-maps-build
+ cancel-in-progress: false
+
+jobs:
+ build:
+ # Requires a registered Forgejo Actions runner labelled `ubuntu-latest`
+ # (docker or host). If none is registered the job stays queued — see the
+ # handoff doc for the ONE Driver action that registers it.
+ runs-on: ubuntu-latest
+
+ # The job needs to read the repo AND commit the regenerated public/ back, so
+ # it needs write. The runner's automatic token carries the repo's scope.
+ permissions:
+ contents: write
+
+ steps:
+ - name: Checkout the pushed source
+ uses: actions/checkout@v4
+ with:
+ # Full history so the build-output commit is a normal fast-forward.
+ fetch-depth: 0
+
+ - name: Install bun
+ uses: https://github.com/oven-sh/setup-bun@v2
+ with:
+ bun-version: latest
+
+ - name: Install dependencies (locked)
+ run: bun install --frozen-lockfile
+
+ # THE AUTHORITATIVE BUILD. build:unlocked is the real chain (clean -> token
+ # -> per-bundle vite -> tree -> drop/parse checks -> verify -> gallery). We
+ # call :unlocked, NOT `bun run build`, because CI IS the single builder now —
+ # the local flock mutex would only get in its own way here. The MAPBOX_TOKEN
+ # secret becomes public/api/maps/token.json (gitignored) so the credential
+ # reaches the built site without ever entering git history.
+ - name: Build (authoritative, single-writer)
+ env:
+ MAPBOX_TOKEN: ${{ secrets.MAPBOX_TOKEN }}
+ VITE_MAPBOX_TOKEN: ${{ secrets.MAPBOX_TOKEN }}
+ BUILD_LOCK_OWNER: forgejo-ci
+ run: bun run build:unlocked
+
+ # Guards graduated from pages.yml: never publish a broken or credential-
+ # leaking web-root. A failure here fails the build — nothing is committed.
+ - name: Assert the token endpoint exists
+ run: |
+ test -s public/api/maps/token.json || {
+ echo "::error::public/api/maps/token.json is missing/empty — set the MAPBOX_TOKEN secret, or every basemap renders blank."
+ exit 1
+ }
+
+ - name: Assert no local credential is being published
+ run: |
+ if [ -e public/token.local.js ]; then
+ echo "::error::public/token.local.js is present in the web-root and must not be committed."
+ exit 1
+ fi
+
+ # Commit the regenerated public/ back to the branch. This is the ONE writer
+ # of build output in the whole system. If the build produced no change,
+ # commit nothing (idempotent). The token endpoint is gitignored, so it is
+ # never staged. The [skip ci]-equivalent is handled by the paths-ignore
+ # above: a public/-only commit does not re-trigger this job.
+ - name: Publish the built web-root
+ run: |
+ git config user.name "forgejo-ci"
+ git config user.email "ci@doxa.life"
+ git add public/
+ if git diff --cached --quiet; then
+ echo "No build-output change — nothing to publish."
+ exit 0
+ fi
+ git commit -m "build: regenerate public/ from ${GITHUB_SHA:-source}"
+ # Push back to the branch that triggered us. The runner token authorizes
+ # this; contents:write is granted above.
+ git push origin "HEAD:${GITHUB_REF_NAME}"
diff --git a/README.md b/README.md
index 827ad3d32..6e17edee7 100644
--- a/README.md
+++ b/README.md
@@ -1,305 +1,169 @@
-# doxa-maps — prayer maps that embed everywhere
+# doxa-maps — one build, maps that embed everywhere
-> **Bottom line up front.** A global prayer movement stalls when there is only **one** website where people
-> can see the task and sign up — today that plateau sits at roughly 600–1,000 people groups covered. The fix is
-> not a better single website. It is a map that lives on **thousands** of partner-team websites at once, shows
-> the prayer task clearly, changes as people sign up, and carries a **Share** button so every team that receives
-> it can pass it on. This bundler builds that map — and every other map you need — as one embeddable file each.
+**A Vite bundler that turns each map into a self-contained web component.** One `bun run build` produces a
+`doxa-maps/` folder you drop on any host — iframe it, script-embed it, ship it as a mobile app, or open the
+whole thing as one SPA. **Same code, every screen.**
-## Why embed, not link
-
-One sign-up site is a bottleneck. A map embedded on a partner team's *own* site multiplies three surfaces at
-once: the **prayer surface** (more people see the task), the **information surface** and the **understanding
-surface**. And a team that has the map on its own site treats it as its own — ownership becomes stewardship.
-Sign-up can still happen in one place; the *map* does not have to.
-
-## What this trains you to be (Training for Trainers)
-
-We borrow the language of **Training for Trainers (T4T)** — Ying Kai's method, told in *T4T: A Discipleship
-Re-Revolution* (Steve Smith with Ying Kai, 2011; trainer resources at [t4tonline.org](https://t4tonline.org)):
-the goal is never one trained person, but trainers who train trainers. This README trains you to be three things:
-
-1. a **map maker** — you (with your AI agent) build maps on the shared library in this repo;
-2. a **map multiplier** — every map you build embeds anywhere and carries a Share button;
-3. a **prayer mobilizer** — you run the campaign that multiplies the surface: the map onto the website of
- every team you know that is trying to multiply disciples among the unreached, and each of them trained to
- pass it on with the same button — **[the Playbook](#the-playbook--start-a-prayer-movement-with-this-map)**.
-
-Everything below is for the people who build the maps. If you only want to *run the campaign*, jump to the
-Playbook; the full walkthrough is [docs/tutorial.md](./docs/tutorial.md).
+This README is a **show, don't tell** index. Skim the three tables, then jump to the section you need.
---
-## What the bundler is — a JAMstack map bundler for vibe coders
+## Index
-> **Built by a vibe coder, for vibe coders** — and for map multipliers and prayer mobilizers who
-> want every people, in every place, prayed for by name.
+### The 5 plugin (app) types — from simplest to full app
-**One build turns every map into a self‑contained web component** that embeds anywhere an embed
-script or URL works, and every map carries the **Share button** so any team can pass its map to the
-next team. **The seemingly impossible part:** the same build also composes all your maps, tables, and
-dashboards into a single‑page application that runs as a website, installs as a mobile app, and
-embeds on partner sites piece by piece. *Same code, every screen.*
+| # | Capability | What it is | Example plugin | Section |
+|---|------------|------------|----------------|---------|
+| 1 | **Single element** | One folder → one bundle → one custom element. The lightest tier. | `doxa-vision-card` | [§1](#cap-1--single-element) |
+| 2 | **Preset element (prop-table)** | ONE `root.vue` rendered N ways by preset props — one plugin, many maps. | `doxa-simple-map` (prayer · engagement · adoption) | [§1](#cap-2--preset-element-prop-table) |
+| 3 | **Monolithic / multi-view** | A whole research map, all in one `root.vue`. | `doxa-research-map` | [§1](#cap-3--monolithic-research-map) |
+| 4 | **Composite element** (nested-element SPA) | A composite element that nests other elements and wires them via props/events. | `my-upg-100-list` | [§1](#cap-4--composite-element) |
+| 5 | **App shell** (nested-SPA) | The full doxa-maps app — every plugin in one deep-linkable page. | the doxa-maps SPA | [§1](#cap-5--app-shell--the-doxa-maps-app) |
-
-
-
-
-
One build, same code: the SPA in a desktop browser and on a phone — many maps, one place.
+### The 5 deployment methods
-- **One repo, one library** — every map shares components. Not fifty folders of duplicated spaghetti.
-- **`bun run dev`** → a live plugin gallery of every map, each with a copy‑paste embed snippet.
-- **`bun run build`** → every map ships as its own self‑contained embeddable file (script tag or iframe — WordPress included), and the same build feeds the SPA and the mobile app.
+| # | Method | Status | Section |
+|---|--------|--------|---------|
+| 1 | **iframe embed** on any site | required | [§2](#deploy-1--iframe-embed) |
+| 2 | **Mobile app** (Android / Capacitor) | required | [§2](#deploy-2--mobile-app) |
+| 3 | **Direct-JS / research dashboard** (props + events + SRI) | required | [§2](#deploy-3--research-dashboard-direct-js) |
+| 4 | **WordPress** (same artifacts, Custom HTML block) | deferred POC | [§3](#3-cli-commands--installs-per-deploy-method) |
+| 5 | **Desktop** (Tauri) | deferred POC | [§3](#3-cli-commands--installs-per-deploy-method) |
-**You don't write the code.** Point your AI agent at the example maps, describe what you want in plain English, and it builds a new one on the shared library.
+### CLI commands + what to install
-> ⚠️ **Maps ship without data** — being JAMstack (pure JS), they fetch it (like people groups) from an **API** at runtime ([details](#the-data-caveat)).
-
----
-
-## Have you ever dreamed of making maps?
-
-Here's the story. Maybe it's yours too.
-
-You got into building software right as AI made it possible for anyone to write code. And you had a
-vision: **make maps** — every map you could imagine — to advance the Kingdom of God, so that every people
-in every place could hear the gospel of Jesus.
-
-So you started. And it worked… for one map. AI is *great* at writing a single map — a couple thousand
-lines of code you can embed on a website.
-
-But then you wanted a second map. And a fifth. And a fiftieth. And here's the wall you hit:
-
-- Every new map meant **copying the whole thing** into a new folder — nothing was reusable.
-- Your maps grew past 2,000… 5,000… lines of tangled "spaghetti code."
-- You ended up with **50 folders, 50 map projects**, scattered across your machine — half of them lost.
-- Each one was fragile, glitchy, and difficult to maintain, and harder to form a community around.
-- You tried to build the bigger platform — many maps in one app — and the agent got stuck past map one. Somewhere in there, the mobile‑app dream quietly went out the window.
-
-**If that's you: you built maps for Jesus. I salute you.** Those maps have their place. But you were
-missing one thing — a *framework* that lets all your maps live in one place and share reusable pieces.
-
-That's what this is. Built for communities of map makers who want to make open source maps that can multiply everywhere. And the dream you threw out the window? The SPA here runs on your phone **by design** — same code.
-
----
-
-## Key terms (don't be afraid of these — you can learn each in one search)
-
-New to this? Read this table first. You do **not** need to memorize it — just know the words exist and where to learn them.
-
-| Term | In plain words | Learn more |
-|---|---|---|
-| **Vibe coder** | Someone who builds with AI writing most of the code. That's who this is for. | — |
-| **JAMstack** | A way of building where the front end is just **J**avaScript, **A**PIs, and **M**arkup (HTML/CSS). It runs almost anywhere — including WordPress. | [jamstack.org](https://jamstack.org) |
-| **Bundler** | A tool that takes all your code and packs it into a few ready‑to‑ship files. This repo *is* a bundler for maps. | [vite.dev/guide](https://vite.dev/guide/) |
-| **Build step** | The one command (`bun run build`) that turns your code into those shippable files. Your agent can run it for you. | — |
-| **bun** | A fast, all‑in‑one replacement for `npm` (installs packages, runs scripts). We use it here. | [bun.sh](https://bun.sh) |
-| **node modules** | The reusable code packages your project depends on. If you use VUE, and VITE you need Node Modules. It is like installing libraries and dependencies that allow VUE to work. `bun install` downloads them into your doxa-maps folder on your laptop `node_modules/`. | [bun.sh/docs](https://bun.sh/docs/cli/install) |
-| **Dev server** | A local preview website (run this command 'bun run dev' after 'bun install') that updates live as you edit. You run it, it let's you see all the maps in one place so you can give your agents feedback, and see changes almost real time when the agent makes edits. It runs at `http://localhost:5173`. | — |
-| **Vue / React** | Popular, well‑supported frameworks for building front ends. This bundler is built on **Vue** + **Vite** — commit to learning Vue (if you know React, the concepts transfer). | [vuejs.org](https://vuejs.org) |
-| **Web component** | A custom HTML tag (like ``) that works on any page, no framework required. Each of your maps becomes one. | [MDN: Web Components](https://developer.mozilla.org/en-US/docs/Web/API/Web_components) |
-| **iframe** | A safe window that embeds one site inside another. Simple, isolated. Each map comes with iframe support by default | [MDN: iframe](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe) |
-| **Shadow DOM** | Lets your map's JavaScript run *directly* inside the host page (not walled off in an iframe) so it can integrate deeply — while keeping its styles from clashing. | [MDN: Shadow DOM](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM) |
-| **SPA (single‑page application)** | One page that composes several maps together in a layout — a whole multi-map site from one build. | [MDN: SPA](https://developer.mozilla.org/en-US/docs/Glossary/SPA) |
-| **CDN** | A URL that serves a file (JavaScript/CSS) to be fetched and run in a browser. When you use Mapbox, you're loading its CDN. Your built maps are served the same way. JAMstack allows these map frontends to run through a CDN, yes they are that modular! Or, you can run them directly in your site, but CDN allows them to embed via iframe onto partner websites easily with a single URL | [MDN: CDN](https://developer.mozilla.org/en-US/docs/Glossary/CDN) |
-| **Minify** | The build shrinks your code into compact, hard‑to‑read (but still working) form — smaller and harder to copy. | [MDN: Minification](https://developer.mozilla.org/en-US/docs/Glossary/Minification) |
-| **Capacitor** | Wraps your maps app into a real iOS/Android apps, or make a SPA ('single-page application' see above) by joining multiple maps together into a layout, and bundling that SPA into IOS/Android easily — **same code, no Swift, no Flutter, no React Native.** (The Android project ships in this repo; iOS scaffolds the same way on a Mac.) | [capacitorjs.com](https://capacitorjs.com) |
-| **Lazy loading** | Only loads each map when it's actually needed, so a page with many maps stays fast. | [web.dev: lazy loading](https://web.dev/articles/lazy-loading) |
-
----
-
-## What doxa-maps gives you
-
-- **One repository for all your maps.** Each map is an *application profile* in `plugin-designs//`.
-- **A shared library of reusable, parameterized components** (`library/` — the map, the legend, the
- controls, the search bar) so your maps stop duplicating code.
-- **Room for each map's own custom code** — because real maps always have something unique.
-- **Rapid prototyping.** Add a `.vue` app-profile file, rebuild, and it's live. Build *hundreds* of maps from one library.
-- **Your agent's happy place.** Everything's in one folder, so your AI agent has all the context it needs to
- build the next map from the examples already here. Give your agent the proper context and it will have what it needs to make new maps. The only limit is our fear of new architecture, we can learn JAMstack, and see the use cases, and grow in a new framework of software design that is appropriate for open source maps that need to embed everywhere.
- - **The Great Commission's Maps** Everyone needs to work together to fulfill the great commission, and it will not be possible without maps. Allowing map communities to multiply open source maps that can embed onto all partner sites means doxa-maps is built to solve global mobilization problems, not just a map for centralized website.
-
----
-
-## The parts that make you do a backflip
-
-- **One build → many embeddable maps.** `bun run build` turns every map into a self‑contained web component
- *and* generates a standalone HTML page for each one.
-- **Embed anywhere.** Drop a map on your own site, a partner team's site, or WordPress — via an **iframe**
- (safe and isolated) *or* via **shadow DOM** (the map's JavaScript runs directly in the page, for deep
- integration and advanced features).
-- **Compose maps into a single‑page app.** Put several maps into one layout — a whole multi‑map site — and it
- **lazy‑loads** each map so it stays fast even when there are many.
-- **Turn it into a mobile app.** The same code runs as an **iOS/Android app** via **Capacitor** — no new
- language to learn. Design it mobile‑friendly right in your browser (inspect element → switch to a phone
- aspect ratio), then wrap it.
-- **Your source stays protected.** The build **minifies** your code — it still runs perfectly, but anyone who
- grabs it off a partner site gets compact, hard‑to‑read output, not your clean source.
-- **Update once, everywhere.** Serve the built maps from a **CDN**, and when you rebuild, every partner site
- that embeds a map picks up the latest version when their cache refreshes.
-
-**One codebase → your own map website · embedded on partner sites · WordPress · mobile app · standalone.**
-
----
-
-## The Playbook — start a prayer movement with this map
-
-The learn‑by‑doing use case this bundler was built for. Five steps, trainable to any team:
-
-1. **List your partner teams.** Build a spreadsheet of every team you know that is working to
- multiply disciples among the unreached — their websites, and the contact who can edit each site.
-2. **Keep the list in a secure, collaborative CRM — not a loose file.** That list of teams,
- contacts, and sites is an **attack surface** in the wrong hands; protect the people on it.
- [Disciple.Tools](https://disciple.tools) is an open‑source, multiplayer CRM built exactly for
- tracking teams, contacts, and mobilization campaigns — privately.
-3. **Commission prayer mobilizers.** Assign real people to work the list — call, network, and ask
- each team to put the prayer map on their site and present it to their spiritual family.
-4. **Embed everywhere.** Press the map's **Share button**: *Share Link* is the map's own page,
- *Add to Your Website* is the embed code — both carry the full URL of whatever host serves your
- maps (plus a `utm_source` tag on the generated pages, so every site's traffic is measurable).
- Verified places a pasted embed works: team websites and WordPress (Custom HTML block),
- Squarespace (Code block — iframes need a Business or Commerce plan), Wix (Add → Embed → HTML
- iframe), Google Sites (Insert → Embed → By URL), Notion (`/embed` + the Share Link), and internal
- boards with an iframe add-on (e.g. Trello's *Card Frames* Power-Up). Rule of thumb: anywhere that
- accepts an HTML snippet or a URL embed, over https.
-5. **Train the trainer.** Teach each team to use the Share button to embed the map — then teach
- them to train the *next* team the same way. The movement doesn't hang on one mobilization
- team: every team that carries the map can multiply it.
-
-The result: one signup flow, a thousand doorways into it — and a map that visibly changes as
-people commit, so every team watches the task shrink on *their own* site.
-
-## A word before you decide
-
-This architecture is probably different from what you're used to. **Different doesn't mean worse** — it
-means it can make your life better and let you build the maps of your dreams.
-
-So be honest with yourself. If you don't have big dreams about map‑making, this product might not be for
-you — and that's okay. But if you dream of maps that change the world and carry the gospel to places it has
-never been, you're going to need **more than a vibe‑coded map**. You'll need a *framework* that supports far
-more components than a typical one‑off map. And you'll need a *community*.
-
-You can't form a community around spaghetti‑coded maps. You **can** form one around a shared framework with a
-reusable library — because then people prototype on the same foundation and build something stronger
-together, where maps never have to be rewritten from scratch. One problem that hinders the Great Commission, is each worker keeps their maps private source, and don't have a shared map framework to build maps on, with reusable map components in a modern framework like VUE with VITE embeddability support. **Doxa-maps** aims to meet this open source map gap for a global prayer movement which needs more maps, maps, maps!
-
-## Requirements — the one small wall (and it's worth it)
-
-Someone once told me: *don't reinvent the wheel — use Vue and Vite instead of hand‑rolling vanilla
-JavaScript.* They were right. So here's the honest deal before you start:
-
-1. **Commit to learning Vue.** This bundler is built on **Vue + Vite** (React skills transfer —
- the components you'll touch are Vue).
- You don't have to master it — but pick one and lean in. (Afraid you can't learn a new architecture? You
- can. The concepts are one search away — that's what the glossary above is for.)
-2. **Learn the four basics** (your agent can run all of them):
- - `bun install` — get the dependencies
- - `bun run dev` — the dev server (live preview)
- - `bun run build` — the build step (ship it)
- - what `node_modules` are (the downloaded dependencies)
-3. **You do *not* need to write the code.** Give your agent access to the example maps in `plugin-designs/` plus
- the shared `library/`, describe your map in plain English, and it builds a new map that reuses the library
- instead of duplicating spaghetti. The library is for reusable components while the app profiles is for storing custom components that are not reusable, and are specific to that app-profile or map-app. Each map app has custom components that are not reusable and they are not appropriate in the shared library, but if you look at other plugin-designs you will see they store their custom components, as well as their main.vue file for the map in the plugin-designs folder.
-
-That's the wall. It's small, and everything past it is the maps of your dreams.
-
----
-
-## Quickstart
-
-You need [**bun**](https://bun.sh) installed (`bun.lock` is committed, so installs are reproducible).
+Install **[Bun](https://bun.sh)** once, then:
```bash
bun install # download node_modules (one time)
-
-bun run dev # the plugin gallery: http://localhost:5173 (or the next free port — read the terminal)
- # an auto-generated SPA of every map, grouped by bundle and composition tier,
- # each deep-linkable; `bun run smoke:gallery` proves every one of them boots
-
-bun run build # the build step: every plugin-designs/