diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index d01910dae..1034c3764 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,384 +1,61 @@
-# Contributing to doxa-maps — building, embedding, and shipping a map bundle
+# Contributing to doxa-maps
-Maps sit on the edge of your ecosystem: built once, they migrate with you.
-WordPress → Nuxt → whatever comes next, the maps come along unchanged. Embed them
-anywhere with a single script tag.
+Everything a contributor needs, short. Recipes live in [docs/how-to.md](./docs/how-to.md),
+lookups in [docs/reference.md](./docs/reference.md), depth in
+[docs/architecture/](./docs/architecture/).
-> **TL;DR — the one rule that surprises everyone:**
-> To see your map changes in the served site, run **`bun run build`** — **NOT
-> `bun run dev`**. The build is what writes your bundle into `public/`, the
-> served web-root. There is no HMR across that boundary.
-> `bun run dev` is the fast HMR loop for working on a map **in isolation**.
-
-## The mental model
-
-This is a **standalone Vite project** with no host framework of any kind. It
-compiles each map into a self-contained `.js` bundle — an IIFE that registers a
-custom element — which any page can load as a plain `
-
-
-```
-
-### Pattern B — Multi-profile bundle (several profiles, selected by name)
-
-**Use when:** the maps are **genuinely different map types** (different `.vue`
-logic/layout), but you want them to **share one build** because they import the
-same slice of the template.
-
-One bundle, **multiple** `.vue` profiles at the folder root. The host chooses which
-profile to mount via the `profile` field — `ProfileLoader` resolves it against the
-bundle's `import.meta.glob('./*.vue')` registry (key `./.vue`). Still
-**one script tag**; you select the map per element with `profile`.
-
-**Example — `my-upg-100-list`:** two profiles in one bundle —
-`upg-100-list.vue` and `research-map-clone.vue` — both served by the single
-`my-upg-100-list-map` element.
-
-```html
-
-
-
-```
-
-> **Note — profiles are selected by `profile`, not by separate script tags.** A
-> bundle ships exactly one `.js`. Whether it has one profile (Pattern A) or
-> many (Pattern B), you load that one script and pick the profile per element via
-> `profile-config`. (A bundle *may* additionally register more than one element
-> **tag** — e.g. `doxa-simple-map` registers both `` and
-> `` for back-compat — but that's a tag-aliasing concern, separate
-> from how many maps the bundle offers.)
-
-### Which to pick
-
-| | Pattern A — parameterized | Pattern B — multi-profile |
+| Command | What | Writes to `public/`? |
|---|---|---|
-| `.vue` profiles in the bundle | one | several |
-| Maps differ by | config **parameters** | distinct `.vue` **logic** |
-| Select a map with | different `profile-config` (same `profile`) | the `profile` field |
-| Script tags | one | one |
-| Example | `doxa-simple-map` (3 instances) | `my-upg-100-list` (2 profiles) |
+| `bun run dev` | HMR playground at `localhost:5173` — iterate on a map in isolation | **No** |
+| `bun run build` | every bundle → `public/doxa-maps//.js` + generated pages | **Yes** |
----
+To see a change on the served site: `bun run build` (or `BUNDLE= bun run build:bundle`),
+then refresh. `dev` never updates the served tree.
-## Where this comes from — the migration story
+**Two-terminal workflow** for integration work: terminal 1 `bun run build:watch` (rebuilds
+into `public/` on save) · terminal 2 serves the host site / `public/`.
-These maps were built **modular-first**, as standalone embeddable widgets, while
-the host site still ran on **WordPress**. Each map was a `
-
-```
-
-This is the default for any page served from the same web-root. The
-build writes straight to where the host reads, so there is no copy step, symlink,
-or upload — see "Where the bundles go" below.
-
-### External CDN mode (a third party serves it)
-
-The exact same built `.js` can be published to a **public CDN** and loaded
-from an absolute URL by a page on **any other site**. Nothing about the bundle
-changes — it is self-contained, registers its own custom element, and pulls the
-Mapbox peer from Mapbox's own CDN.
-
-```html
-
-
-
-```
-
-The choice between modes is purely **the `src` URL on the page** — relative
-(`/doxa-maps/...`, same-origin) vs absolute (`https://cdn.../...`, external).
-The build itself only ever produces one thing: a portable IIFE.
-
-Either way the map needs a **Mapbox token**, and a page outside this web-root has
-no automatic way to get one. Pass it in the element's `profile-config` as `tk`, or
-set `window.MAP_TOKEN_URL` to an endpoint that returns it — the same contract the
-app shell and the generated pages use. Without one the bundle loads and the
-basemap stays blank.
-
-### Partner-site embedding
-
-A **partner site** is any external site that wants to show one of these maps. It
-uses **external CDN mode**: it loads the Mapbox peer scripts, then the bundle from
-a CDN URL, then drops in the custom element with its own `profile-config`. No
-framework, no build step, and no access to this repo is required on their end.
-
-```html
-
-
-
-
-
-
-
-```
-
-Because the bundle is the same artifact in every mode, a map you tested in
-isolation here, and that runs in the app shell, will run on a partner site too —
-the only difference is the URL the partner points their script tag at.
-
----
-
-## The two build steps
-
-There are two commands, and they do **different jobs**. Knowing which is which is
-the whole game.
-
-### `bun run dev` — HMR staging (work on a map in isolation)
-
-```bash
-bun run dev # Vite dev server for THIS project, with HMR
-```
-
-`dev` serves every bundle at its own staging page **inside this project**, with
-hot module replacement — save a `.vue`, see it instantly. This is the fast loop
-for building and styling a map **on its own**, away from the host.
-
-**`dev` does NOT touch the served web-root.** It writes nothing to
-`public/doxa-maps/`. If you only ever run `dev`, the served site keeps showing
-the **old** bundle.
-
-### `bun run build` — bundle to `public/doxa-maps/`
-
-```bash
-bun run build # build every bundle → public/doxa-maps//.js
-```
-
-`build` compiles each bundle and writes it into the web-root's static
-directory. **This is the only command that updates what the parent (or a CDN)
-serves.** After a build, refresh the parent page to see the change.
-
-| | `bun run dev` | `bun run build` |
-|---|---|---|
-| Purpose | iterate on a map in isolation | publish the bundle for hosts/CDNs |
-| HMR | yes — instant on save | n/a (one-shot compile) |
-| Writes to `public/doxa-maps/` | **no** | **yes** |
-| Updates the served site | **no** | **yes** (after a page refresh) |
-| Updates a partner/CDN copy | no | yes (it produces the file to publish) |
-
-> Bun is the primary toolchain (see `bun.lock`); `npm` works identically — the
-> scripts are the same. Use whichever the rest of your environment uses.
-
----
-
-## The two-terminal dev workflow
-
-For day-to-day work you want **two terminal tabs** open at once:
-
-**Tab 1 — this project (the map source).** Pick one:
-
-```bash
-# in the doxa-maps repo
-bun run dev # HMR staging: iterate on the map in isolation
-# — or, when you want the parent to update on every save —
-bun run build:watch # rebuild into public/doxa-maps/ on every change
-```
-
-**Tab 2 — the app shell.** Run the host's own dev server (from the host
-root), and keep its page open in the browser:
-
-```bash
-# in the served site root
-bun run dev # the host dev server; open the page that embeds the map
-```
-
-The split is deliberate:
-
-- **Tab 1 with `bun run dev`** is for *building the map* — fast HMR, no host
- involved. When it looks right, run `bun run build` once to push it across.
-- **Tab 1 with `bun run build:watch`** is for *integration work* — every save
- rebuilds into the web-root, and the **Tab 2** host page picks up the
- new bundle on refresh. This is the closest you get to a live loop across the
- boundary.
-
-You do not need both at once. Use `dev` while shaping a map, switch to
-`build`/`build:watch` when you want to see it inside the host.
-
----
-
-## Where the bundles go (and why it just works)
-
-`vite.config.js` sets the output directory **three levels up**, into the parent
-app's public assets:
-
-```js
-build: {
- outDir: resolve(__dirname, 'public/doxa-maps'), // ← the served web-root
- emptyOutDir: false, // ← never wipes sibling bundles
-}
-```
-
-- The build writes straight into **`public/doxa-maps/`**, which is inside the
- served web-root — so no copy step or symlink is needed; it writes exactly where
- a host reads. (To serve a bundle from an **external CDN**, publish that same
- file to the CDN.)
-- `emptyOutDir: false` means a build of **one** bundle leaves the **other**
- bundles untouched. You can rebuild a single map without breaking the rest.
-
-Each folder under `app-profiles//` produces exactly one file:
-`public/doxa-maps//.js`. `bun run build` loops over every
-`app-profiles/*/` folder and builds them all.
-
-### Build just one bundle (faster iteration)
-
-`bun run build` rebuilds every bundle. To rebuild only the one you're editing:
-
-```bash
-BUNDLE= bun run build:bundle
-# e.g. BUNDLE=my-map bun run build:bundle → public/doxa-maps/my-map/my-map.js
-```
-
-(`` is the folder name under `app-profiles/`.)
-
----
-
-## Troubleshooting
-
-| Symptom | Cause | Fix |
-|---|---|---|
-| Changed a `.vue`, parent shows old map | You ran `dev`, or didn't rebuild | Run `bun run build`, then refresh the parent page |
-| Build error: `Set BUNDLE= for build` | Ran `vite build` directly with no bundle selected | Use `bun run build` (loops all) or `BUNDLE= bun run build:bundle` |
-| New bundle doesn't appear in parent | Missing `index.html` or `index.js` in the `app-profiles//` folder | Vite auto-discovers a bundle only when both files exist |
-| Old bundle still loads after a hard build | Browser cached the static `.js` | Hard-refresh / disable cache in devtools |
-| Partner/CDN copy is stale | The CDN still serves the old file | Re-publish the freshly built `.js` to the CDN (and bust its cache) |
-| Map is blank on a partner page | Mapbox peer scripts not loaded, or no token | Load `mapbox-gl.js` + geocoder **before** the bundle; pass a valid `tk` in `profile-config` |
-
----
+- Never edit `library/` to customize one map (bundle-private code goes in your `src/`).
+- Never bake tokens or data into a bundle — everything arrives at runtime via `profile-config`.
+- Before a PR: `bun run build && bun run verify` must pass (adds drop-portability + parse gates).
+- Colors are values, not logic: [contributing/COLORS.md](./contributing/COLORS.md).
## One-line recap
-**Edit here → `bun run dev` to shape it → `bun run build` to ship it.** The build
-*is* the bridge: it writes the same portable bundle that the served site serves
-(internal CDN) and that partner sites load from a CDN URL (external CDN). `dev`
-only runs this project in isolation.
+Copy `app-profiles/template-bundle/`, change exactly four things (folder name, tag in
+`index.js`, tag/profile strings in `index.html`, `.vue` filenames), build everything inside
+your folder from `@map` blocks, `bun run build`, embed the snippet. That's the whole job.
diff --git a/README.md b/README.md
index 3ec5484e1..8fd943550 100644
--- a/README.md
+++ b/README.md
@@ -1,71 +1,42 @@
# doxa-maps
-**A bundler that turns your maps into small embeddable pieces you can drop onto any website.**
+**A bundler that turns your maps into embeddable pieces you can drop onto any website.**
-> ### ✨ New here, and you don't write code?
-> You can build real, embeddable maps by **describing them to an AI coding agent** — no
-> programming required. Start with the friendly, zero-jargon guide:
+```
+ app-profiles/