diff --git a/.agents/context/content-i18n.md b/.agents/context/content-i18n.md
new file mode 100644
index 0000000..746be5d
--- /dev/null
+++ b/.agents/context/content-i18n.md
@@ -0,0 +1,40 @@
+# Context: full-guide language switching
+
+## Decision
+
+The Astro full guide keeps the current client-side language switch on its
+existing `/full-guide/` URL. It server-renders both locale variants and the
+language-toggle island shows the selected variant after `client:idle` hydration.
+
+This deliberately preserves the current no-URL-change contract, including links
+shared without a locale segment, and avoids a route/redirect and publishing
+change. The cost is duplicated localized HTML and both locales in the response.
+That is acceptable for this small guide and avoids sending duplicated string
+data through every interactive island.
+
+## Markup and island contract for task 15d
+
+- Render each static localized fragment twice. Put `data-language-content="en"`
+ or `data-language-content="pt"` on its outer element. English is visible in
+ server HTML; the toggle uses the native `hidden` attribute for the inactive
+ locale.
+- Add `` to the guide top bar. Astro's `client:idle` directive
+ is only valid for framework components; this `.astro` island defers its
+ browser setup with `requestIdleCallback` (and a timeout fallback) instead. Do
+ not hydrate the page or use `client:load`; the control is deliberately
+ idle-priority.
+- The island owns the `ai-for-dummies-language` localStorage key. Every read and
+ write remains inside `try`/`catch`, because previews may disable storage.
+- On each selection the island sets `` to `en` or `pt-BR`, updates
+ its `[data-lang]` buttons' `.active` class and `aria-pressed` state, updates
+ `[data-language-content]`, then dispatches `ai-for-dummies:languagechange` on
+ `window`. The event detail is `{ language: 'en' | 'pt' }`.
+- The guide selector island (15a) must read `document.documentElement.lang` when
+ it hydrates and listen for that event. On receipt it must re-render the
+ currently active phase and all active selector panels from their collection
+ data. This preserves today’s `applyLanguage` behaviour without coupling the
+ toggle to page selectors.
+
+This is a page-local contract: the existing `/rules/` toggle continues using its
+own `rules-language` key and must not be changed as part of full-guide
+migration.
diff --git a/plans/astro-refactor/MODEL-ROUTING.md b/plans/astro-refactor/MODEL-ROUTING.md
index 28f3f37..d0483ff 100644
--- a/plans/astro-refactor/MODEL-ROUTING.md
+++ b/plans/astro-refactor/MODEL-ROUTING.md
@@ -5,9 +5,9 @@ interchangeable here, and the split is not about which is "smartest" — it is
about which failure mode each task punishes.
**Honest caveat up front:** I have not benchmarked these three on this
-repository. The routing below is reasoned from task shape and each model's
-known strengths. Validate it cheaply on **task 07** (small, self-contained,
-easy to judge) before fanning out across ten worktrees.
+repository. The routing below is reasoned from task shape and each model's known
+strengths. Validate it cheaply on **task 07** (small, self-contained, easy to
+judge) before fanning out across ten worktrees.
## Short answer
@@ -18,22 +18,27 @@ points where M3 is the wrong tool.
## Routing table
-| Task | Model | Why this one |
-| --- | --- | --- |
-| 01 scaffold + gates | **Codex** | Config-heavy, many interacting tools (Astro + husky + lint-staged + CI), and success is binary — it builds and hooks fire, or not. Codex's long autonomous run-until-green loop suits it, and getting the foundation wrong is expensive later. |
-| 02 design tokens | **Gemini** | Needs the whole CSS corpus in one context (8 stylesheets, ~90 KB) plus **visual judgement on screenshots**. Gemini's long context and multimodal comparison are the differentiator; the others would work file-by-file and miss cross-file drift. |
-| 03 verification net | **Codex** | Writing test tooling with a tight feedback loop. Precision about what an assertion pins matters more than speed. |
-| 04 content schema | **MiniMax-M3** | Small, well-specified, one file. |
-| 05–06 content migration | **MiniMax-M3** | High-volume mechanical string moves with a `diff` as the oracle. Cheap, parallel, verifiable. Exactly M3's sweet spot. |
-| 07 primitives | **MiniMax-M3** | Small components from templates. Use this task to calibrate the whole routing decision. |
-| 08–11 component blocks | **MiniMax-M3 ×4 parallel** | Four bounded tasks, one template each, checklist-gated. Cost per task matters because there are many. |
-| 12–14, 17 pages | **MiniMax-M3** | Bounded, snapshot-diff verified. |
-| 15 full guide | **Codex** | The hard one: 50 KB `app.js`, 12 render functions, tab state, bilingual swap. Long sustained reasoning over interacting pieces; the task most likely to need many iterations against a failing check. |
-| 16 review desk | **Codex** | Same shape and worse — search, filtering, file fetching, six query params, markdown rendering, client-side diff. Highest defect risk in the plan. |
-| 18 motion | **Gemini** | Judging whether motion looks right is perceptual. Feed it before/after captures. |
-| 19 contract re-point | **Codex** | 42 assertions to translate without losing coverage. Meticulous, mechanical, verifiable. |
-| 20 cutover | **you, with M3 assisting** | Touches production publishing. A human should be watching. |
-| review of every task | **Gemini** | Fresh eyes, whole-diff-in-context, and it did not write the code. Never review with the model that wrote it. |
+| Task | Model | Why this one |
+| ------------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| 01 scaffold + gates | **Codex** | Config-heavy, many interacting tools (Astro + husky + lint-staged + CI), and success is binary — it builds and hooks fire, or not. Codex's long autonomous run-until-green loop suits it, and getting the foundation wrong is expensive later. |
+| 02 design tokens | **Gemini** | Needs the whole CSS corpus in one context (8 stylesheets, ~90 KB) plus **visual judgement on screenshots**. Gemini's long context and multimodal comparison are the differentiator; the others would work file-by-file and miss cross-file drift. |
+| 03 verification net | **Codex** | Writing test tooling with a tight feedback loop. Precision about what an assertion pins matters more than speed. |
+| 04 content schema | **MiniMax-M3** | Small, well-specified, one file. |
+| 05–06 content migration | **MiniMax-M3** | High-volume mechanical string moves with a `diff` as the oracle. Cheap, parallel, verifiable. Exactly M3's sweet spot. |
+| 07 primitives | **MiniMax-M3** | Small components from templates. Use this task to calibrate the whole routing decision. |
+| 08–11 component blocks | **MiniMax-M3 ×4 parallel** | Four bounded tasks, one template each, checklist-gated. Cost per task matters because there are many. |
+| 12–14, 17 pages | **MiniMax-M3** | Bounded, snapshot-diff verified. |
+| 15 full guide | **split** | Ran twice on Codex, zero usable commits both times. This table's own swap rule applied: too big, so it became 15a–15e. |
+| 15a guide selector | **Codex** | Collapsing nine near-identical render functions into one island is the reasoning-heavy part that remains. |
+| 15b copy prompt | MiniMax-M3 | Small, mechanical, and has an exact oracle: the clipboard payload must be byte-identical. |
+| 15c language toggle | **Codex** | Needs a design decision written down, not a port — the selector-map approach cannot survive. |
+| 15d assemble full guide | **Codex** | Assembly, but wide: 22 KB of bilingual markup against three islands and task 10's blocks. |
+| 15e retire responsive.css | `agy` | Screenshot-diff driven — needs vision. |
+| 16 review desk | **Codex** | Same shape and worse — search, filtering, file fetching, six query params, markdown rendering, client-side diff. Highest defect risk in the plan. |
+| 18 motion | **Gemini** | Judging whether motion looks right is perceptual. Feed it before/after captures. |
+| 19 contract re-point | **Codex** | 42 assertions to translate without losing coverage. Meticulous, mechanical, verifiable. |
+| 20 cutover | **you, with M3 assisting** | Touches production publishing. A human should be watching. |
+| review of every task | **Gemini** | Fresh eyes, whole-diff-in-context, and it did not write the code. Never review with the model that wrote it. |
## The reasoning in one line each
@@ -41,11 +46,11 @@ points where M3 is the wrong tool.
Use it for volume: 13 of the 20 tasks. Its weakness is long multi-file
reasoning where the spec is vague; every task above that it owns has a
template and a mechanical oracle.
-- **Codex** — best at "keep iterating until the check passes" over a
- complicated existing codebase. Use it where the loop is long and the answer is
- not obvious: scaffold, the two hard pages, verification.
+- **Codex** — best at "keep iterating until the check passes" over a complicated
+ existing codebase. Use it where the loop is long and the answer is not
+ obvious: scaffold, the two hard pages, verification.
- **Gemini** — biggest context and genuinely useful multimodal comparison. Use
- it where the input is *everything at once* or where the judgement is
+ it where the input is _everything at once_ or where the judgement is
**visual**: token consolidation, motion, screenshot diffing, and code review.
## Cross-checking rule
@@ -56,13 +61,13 @@ reviews; Codex writes → Gemini reviews; Gemini writes → Codex reviews. The
## Swap the routing if you see this
-| Symptom | Move the task to |
-| --- | --- |
-| M3 spends more than ~3 iterations failing the same gate | Codex |
-| M3 edits files outside its task scope | Codex, and tighten the brief |
-| Codex "fixes" a red suite by deleting assertions | anything — but re-read `context/verification.md` to it first; `gate.sh` blocks the merge either way |
-| Gemini gives confident visual sign-off with no screenshots attached | require the artifacts; do not accept prose |
-| A task needs more than two models' worth of hand-holding | the task is too big — split it |
+| Symptom | Move the task to |
+| ------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
+| M3 spends more than ~3 iterations failing the same gate | Codex |
+| M3 edits files outside its task scope | Codex, and tighten the brief |
+| Codex "fixes" a red suite by deleting assertions | anything — but re-read `context/verification.md` to it first; `gate.sh` blocks the merge either way |
+| Gemini gives confident visual sign-off with no screenshots attached | require the artifacts; do not accept prose |
+| A task needs more than two models' worth of hand-holding | the task is too big — split it |
## Cost shape
diff --git a/plans/astro-refactor/README.md b/plans/astro-refactor/README.md
index 319ad05..e0825e7 100644
--- a/plans/astro-refactor/README.md
+++ b/plans/astro-refactor/README.md
@@ -37,32 +37,38 @@ on four branches; nothing is merged or pushed.
Phase 0 foundation 01 → (02 ∥ 03 ∥ 04)
Phase 1 content 05 ∥ 06 after 04
Phase 2 components 07 → (08 ∥ 09 ∥ 10 ∥ 11) after 02
-Phase 3 pages 12 ∥ 13 ∥ 14 ∥ 17, then 15 ∥ 16
+Phase 3 pages 12 ∥ 13 ∥ 14 ∥ 17, then 15a ∥ 15b ∥ 15c ∥ 16, then 15d, 15e
Phase 4 polish 18 ∥ 19, then 20
```
-| # | Task | Agent | Depends on | Parallel with |
-| --- | ------------------------------------------------ | --------------------- | ---------- | ------------- |
-| 01 | [scaffold + gates](task-01-scaffold.md) | astro-architect | — | — |
-| 02 | [design tokens](task-02-tokens.md) | design-system-keeper | 01 | 03, 04 |
-| 03 | [verification net](task-03-verification-net.md) | verification-engineer | 01 | 02, 04 |
-| 04 | [content schema](task-04-content-schema.md) | content-i18n-migrator | 01 | 02, 03 |
-| 05 | [guide content](task-05-content-guide.md) | content-i18n-migrator | 04 | 06 |
-| 06 | [review-desk content](task-06-content-review.md) | content-i18n-migrator | 04 | 05 |
-| 07 | [primitives](task-07-primitives.md) | component-builder | 02 | — |
-| 08 | [route cards](task-08-route-cards.md) | component-builder | 07 | 09, 10, 11 |
-| 09 | [chapter blocks](task-09-chapter-blocks.md) | component-builder | 07 | 08, 10, 11 |
-| 10 | [guide blocks](task-10-guide-blocks.md) | component-builder | 07 | 08, 09, 11 |
-| 11 | [review-desk blocks](task-11-review-blocks.md) | component-builder | 07 | 08, 09, 10 |
-| 12 | [landing page](task-12-page-landing.md) | page-migrator | 03, 08 | 13, 14, 17 |
-| 13 | [chapter pages ×4](task-13-page-chapters.md) | page-migrator | 03, 09 | 12, 14, 17 |
-| 14 | [rules page](task-14-page-rules.md) | page-migrator | 03, 09 | 12, 13, 17 |
-| 15 | [full guide](task-15-page-full-guide.md) | page-migrator | 05, 10, 13 | 16 |
-| 16 | [review desk](task-16-page-review-desk.md) | page-migrator | 06, 11, 13 | 15 |
-| 17 | [hands-on passthrough](task-17-hands-on.md) | astro-architect | 01 | 12, 13, 14 |
-| 18 | [motion pass](task-18-motion.md) | motion-designer | 15, 16 | 19 |
-| 19 | [contract re-point](task-19-verify-repoint.md) | verification-engineer | 15, 16 | 18 |
-| 20 | [cutover + cleanup](task-20-cutover.md) | astro-architect | all | — |
+| # | Task | Agent | Depends on | Parallel with |
+| --- | ----------------------------------------------------------------- | --------------------- | ------------------ | ------------- |
+| 01 | [scaffold + gates](task-01-scaffold.md) | astro-architect | — | — |
+| 02 | [design tokens](task-02-tokens.md) | design-system-keeper | 01 | 03, 04 |
+| 03 | [verification net](task-03-verification-net.md) | verification-engineer | 01 | 02, 04 |
+| 04 | [content schema](task-04-content-schema.md) | content-i18n-migrator | 01 | 02, 03 |
+| 05 | [guide content](task-05-content-guide.md) | content-i18n-migrator | 04 | 06 |
+| 06 | [review-desk content](task-06-content-review.md) | content-i18n-migrator | 04 | 05 |
+| 07 | [primitives](task-07-primitives.md) | component-builder | 02 | — |
+| 08 | [route cards](task-08-route-cards.md) | component-builder | 07 | 09, 10, 11 |
+| 09 | [chapter blocks](task-09-chapter-blocks.md) | component-builder | 07 | 08, 10, 11 |
+| 10 | [guide blocks](task-10-guide-blocks.md) | component-builder | 07 | 08, 09, 11 |
+| 11 | [review-desk blocks](task-11-review-blocks.md) | component-builder | 07 | 08, 09, 10 |
+| 12 | [landing page](task-12-page-landing.md) | page-migrator | 03, 08 | 13, 14, 17 |
+| 13 | [chapter pages ×4](task-13-page-chapters.md) | page-migrator | 03, 09 | 12, 14, 17 |
+| 14 | [rules page](task-14-page-rules.md) | page-migrator | 03, 09 | 12, 13, 17 |
+| 15 | [full guide](task-15-page-full-guide.md) — **split into 15a–15e** | — | — | — |
+| 05b | [interactiveCopy data](task-05b-guide-interactive-data.md) | content-i18n-migrator | 05 | — |
+| 15a | [guide selector](task-15a-guide-selector.md) | component-builder | 05, 10 | 15b, 15c |
+| 15b | [copy prompt](task-15b-copy-prompt.md) | component-builder | 05 | 15a, 15c |
+| 15c | [language toggle](task-15c-language-toggle.md) | content-i18n-migrator | 05 | 15a, 15b |
+| 15d | [assemble full guide](task-15d-page-full-guide.md) | page-migrator | 05b, 10, 13, 15a–c | 16 |
+| 15e | [retire responsive.css](task-15e-responsive-css.md) | design-system-keeper | 15d, 16 | — |
+| 16 | [review desk](task-16-page-review-desk.md) | page-migrator | 06, 11, 13 | 15d |
+| 17 | [hands-on passthrough](task-17-hands-on.md) | astro-architect | 01 | 12, 13, 14 |
+| 18 | [motion pass](task-18-motion.md) | motion-designer | 15d, 16 | 19 |
+| 19 | [contract re-point](task-19-verify-repoint.md) | verification-engineer | 15d, 16 | 18 |
+| 20 | [cutover + cleanup](task-20-cutover.md) | astro-architect | all | — |
Widest parallelism: **four agents** (tasks 08–11, then 12/13/14/17). More than
that and they start contending on review capacity, not on files.
diff --git a/plans/astro-refactor/task-05b-guide-interactive-data.md b/plans/astro-refactor/task-05b-guide-interactive-data.md
new file mode 100644
index 0000000..e0761f9
--- /dev/null
+++ b/plans/astro-refactor/task-05b-guide-interactive-data.md
@@ -0,0 +1,78 @@
+# Task 05b — The `interactiveCopy` data
+
+**Agent**: `content-i18n-migrator` · **Model**: MiniMax-M3 **Depends on**: 05 ·
+**Blocks**: 15d **Worktree**:
+`.agents/scripts/worktree.sh start 05b guide-interactive-data`
+
+## Why this task exists
+
+It is not in the original plan, and it is the second hole of exactly this kind
+(see [task 04b](task-04b-chapters-data.md), which filled the empty `chapters`
+collection).
+
+Task 05 moved `phases`, `providers`, `efforts`, `skillSources`, `handsOnPrompts`
+and `skillInstallPrompts` out of `app.js`. It did not move
+`const interactiveCopy` at `app.js:160`, which holds six more datasets:
+
+| Key | Feeds | Rendered by |
+| --------------- | ---------------------- | --------------------- |
+| `workers` | `#worker-detail` | `renderWorker` |
+| `trees` | `#tree-detail` | `renderTree` |
+| `routes` | `#route-detail` | `renderRoute` |
+| `skillFiles` | `#skill-detail` | `renderSkillFile` |
+| `skillWorkflow` | `#builder-detail` | `renderSkillWorkflow` |
+| `commonSkills` | `#common-skill-detail` | `renderCommonSkill` |
+
+Task 15d stopped before writing a line because of this — six of the nine groups
+`GuideSelector` drives have no data to read. It was right to stop.
+
+## Scope
+
+`src/content/config.ts` (new collections only) and the new `src/content/*/`
+JSON. **You own `config.ts`** — you are the only agent who may edit it. Do not
+change an existing collection's schema.
+
+## The shapes are already written down
+
+`src/components/islands/GuideSelector.astro` declares a `GuideSelectorData`
+interface at the top of the file. It is the contract 15d will feed. Match it —
+if you think a field there is wrong, say so in your report rather than diverging
+silently.
+
+Note the non-obvious ones, verified against `app.js`:
+
+- `trees[id]` has `owner` and `note` localized, but `path` and `command` are
+ **single strings, not localized** — they are shell paths.
+- `routes[id].score` is a **number**, rendered into `style="--score:N%"`.
+- `workers[id]` is `{ en: string[], pt: string[] }` — a three-element array
+ (`span`, `strong`, `small`), not an object.
+- `skillFiles[id]` mixes `icon`/`title` (unlocalized) with `en`/`pt` strings.
+- `providers[id].tiers` is `[string, string, Localized][]`.
+
+Read the real `app.js` for each. Do not infer a shape from a sibling.
+
+## Rules that bite here
+
+- **Both `en` and `pt` are mandatory on every localized field.** A missing `pt`
+ must fail the build. Never make a field optional to clear a validation error —
+ find the real Portuguese string in `app.js`.
+- Copy strings **verbatim**. This is a move, not a rewrite. No fixed typos, no
+ improved phrasing, nothing translated that is already translated. Several
+ strings contain inline HTML; keep it exactly as-is.
+- Prove it. Diff each migrated string against the `app.js` original
+ programmatically and report the result — task 15b did this for the prompt
+ bodies and it is the standard here.
+- Do not delete `interactiveCopy` from `app.js`. The legacy page still runs on
+ it until task 20 cuts over, and `verify.mjs` still asserts against it.
+- Do not migrate a page or touch a component. 15d owns that.
+- Do not edit `verify.mjs`, `tokens.css`, or `astro.config.mjs`.
+
+## Done when
+
+- [ ] Six collections defined and exported from `config.ts`
+- [ ] Entries for every id in `interactiveCopy`, both locales
+- [ ] Shapes match `GuideSelectorData`, or the divergence is argued in the
+ report
+- [ ] Every string proven byte-identical to its `app.js` original
+- [ ] `pnpm run gate` green — the full gate, not `verify` + `audit-ui` alone
+- [ ] 42 assertions intact
diff --git a/plans/astro-refactor/task-15-page-full-guide.md b/plans/astro-refactor/task-15-page-full-guide.md
index 0208bc1..c39ed95 100644
--- a/plans/astro-refactor/task-15-page-full-guide.md
+++ b/plans/astro-refactor/task-15-page-full-guide.md
@@ -1,64 +1,22 @@
-# Task 15 — Full guide
+# Task 15 — Full guide — **SUPERSEDED, split into 15a–15e**
-**Agent**: `page-migrator` · **Model**: **Codex** — hardest task in the plan
-**Depends on**: 05, 10, 13 · **Parallel with**: 16 **Worktree**:
-`.agents/scripts/worktree.sh start 15 page-full-guide`
+Do not work this brief. It is kept because other documents link to it.
-## Goal
+Task 15 was attempted twice on Codex and produced no usable commit either time.
+The second report's own words: _"this is an incomplete scaffold, not the real
+migration requested."_ `MODEL-ROUTING.md` says a task needing more than two
+models' worth of hand-holding is too big — split it. So it is split, along the
+seams that made it hard: the nine-fold selector duplication, the clipboard
+fallback, and the selector-map language toggle that cannot survive the port.
-`/full-guide/` → Astro. 22 KB of HTML, a 50 KB script, 30 KB of CSS, twelve
-render functions, bilingual throughout.
+| Brief | What | Depends on |
+| ---------------------------------- | ------------------------------------------- | --------------------- |
+| [15a](task-15a-guide-selector.md) | one generic selector island for nine groups | 05, 10 |
+| [15b](task-15b-copy-prompt.md) | copy-prompt buttons, reading progress | 05 |
+| [15c](task-15c-language-toggle.md) | language toggle — needs a decision first | 05 |
+| [15d](task-15d-page-full-guide.md) | assemble `/full-guide/` | 10, 13, 15a, 15b, 15c |
+| [15e](task-15e-responsive-css.md) | prove `responsive.css` dead, delete it | 15d, 16 |
-## What `app.js` actually is
-
-Not application code — a **bilingual content database** (task 05 already moved
-it) plus ~12 `render*` functions that swap `innerHTML` on tab clicks. Once
-content is a collection, the remaining JS is small: tab state and a language
-toggle.
-
-## Islands
-
-Only these hydrate. Everything else is server-rendered.
-
-| Island | Directive | Why |
-| -------------------------------------------------------- | ---------------- | -------------------------------------------------- |
-| Phase tabs | `client:visible` | click-driven panel swap |
-| Tree / worker / route / model / effort / skill selectors | `client:visible` | same pattern; consider one generic selector island |
-| Language toggle | `client:idle` | page-wide, not urgent |
-| Copy-prompt buttons | `client:visible` | clipboard |
-
-If you end up with 12 separate islands you have missed the pattern — they are
-one selector component with different data.
-
-## Asserted by verify.mjs — all must survive
-
-`const phases`, `const handsOnPrompts`, `const modelGuide`,
-`const skillSources`, `const skillInstallPrompts`, `render('plan')`,
-`renderTree`, `renderWorker`, `renderRoute`, `renderModelProvider`,
-`renderEffort`, `renderSkillFile`, `renderSkillWorkflow`, `renderCommonSkill`,
-`renderHandsOn`, `copyPrompt`.
-
-These are **implementation-detail assertions** — they look deletable and are
-not. Each pins a feature. Coordinate with task 19 to replace each with an
-output-level assertion of the same behaviour. **Never delete one yourself.**
-
-Also: `data-copy-target="prompt-install-skills|prompt-basic|prompt-skills"`,
-`hands-on/starter/`, `additional-reading.md`, `role="tablist"`, `
`.
-
-## Watch for
-
-- `copyPrompt` uses `navigator.clipboard` with a `document.execCommand`
- fallback. Keep both — the fallback exists for non-secure contexts.
-- The hands-on prompt strings are copy-pasted by attendees into an agent. Exact
- whitespace and line breaks matter.
-- `responsive.css` (30 KB) mostly serves this page. Port what is needed, prove
- the rest dead, delete it. Screenshots are the proof.
-
-## Done when
-
-- [ ] Snapshot diff empty
-- [ ] Every interaction works: all tabs, both languages, all copy buttons
-- [ ] Keyboard: arrow keys move between tabs; focus visible throughout
-- [ ] JS payload **smaller** than today's 50 KB (content is now static)
-- [ ] Screenshots match at four widths; checklist complete; `pnpm run gate`
- green
+15a, 15b and 15c run in parallel. 15d is assembly and must not invent islands.
+Downstream tasks that said "depends on 15" now depend on **15d** (18, 19) or
+**15e** (20).
diff --git a/plans/astro-refactor/task-15a-guide-selector.md b/plans/astro-refactor/task-15a-guide-selector.md
new file mode 100644
index 0000000..44ae48f
--- /dev/null
+++ b/plans/astro-refactor/task-15a-guide-selector.md
@@ -0,0 +1,86 @@
+# Task 15a — The one guide selector island
+
+**Agent**: `component-builder` · **Model**: **Codex** **Depends on**: 05, 10 ·
+**Parallel with**: 15b · **Blocks**: 15d **Worktree**:
+`.agents/scripts/worktree.sh start 15a guide-selector`
+
+## Why this task exists
+
+Task 15 was attempted twice on Codex and produced no usable commit either time.
+Its own second report said "this is an incomplete scaffold, not the real
+migration requested". `MODEL-ROUTING.md` says a task that needs more than two
+models' worth of hand-holding is too big — so it is split. This is the first
+piece: **the interactive machinery, with no page migration in it.**
+
+## Scope
+
+One new island under `src/components/islands/`. You import nothing into a page
+and you migrate no page — tasks 07 through 11 shipped components ahead of their
+pages the same way, and the gate is happy with an unimported component.
+
+## What you are replacing
+
+`app.js` has nine functions that are the same function nine times:
+
+| Function | Trigger attribute | Detail panel target |
+| --------------------- | --------------------- | ---------------------- |
+| `render` | `data-phase` | `#phase-panel` |
+| `renderWorker` | `data-worker` | `#worker-detail` |
+| `renderTree` | `data-tree` | `#tree-detail` |
+| `renderRoute` | `data-route` | `#route-detail` |
+| `renderModelProvider` | `data-model-provider` | `#provider-detail` |
+| `renderEffort` | `data-effort` | `#effort-detail` |
+| `renderSkillFile` | `data-skill-file` | `#skill-detail` |
+| `renderSkillWorkflow` | `data-skill-step` | `#builder-detail` |
+| `renderCommonSkill` | `data-common-skill` | `#common-skill-detail` |
+
+Each: click a button in a group, mark it active, swap the panel's `innerHTML`
+from a content object. **Build one island, not nine.** If your diff has nine
+components you have missed the point of the task.
+
+The panels' inner markup differs per group (`#route-detail` draws a `--score`
+meter, `#tree-detail` draws owner/checkout/note/command). Take the shape from
+the slot or from a per-group layout, not from nine islands.
+
+## The one coupling that is not uniform
+
+`data-model-provider` clicks also re-run `renderEffort` with the currently
+active effort. Preserve that. Everything else is independent.
+
+## Asserted by verify.mjs — all nine names must survive
+
+`render('plan')`, `renderTree`, `renderWorker`, `renderRoute`,
+`renderModelProvider`, `renderEffort`, `renderSkillFile`, `renderSkillWorkflow`,
+`renderCommonSkill`. Also `role="tablist"`.
+
+These are implementation-detail assertions. They look deletable and are not —
+each pins a feature. **Never delete one.** If a name genuinely cannot survive
+the new shape, stop and report it; task 19 re-points assertions, you do not.
+
+## Also deliver
+
+- Keyboard: Arrow keys, Home and End move between buttons in a group; focus
+ visible throughout. `role="tablist"` groups follow the ARIA tabs pattern.
+- Server-render the initially active panel. The panel must not be empty before
+ hydration.
+- `client:visible`.
+
+## Do not
+
+- Migrate `/full-guide/` or create `src/pages/full-guide.astro`. That is 15d.
+- Touch the language toggle or the copy buttons. Those are 15c and 15b.
+- Touch `src/content/config.ts`, `verify.mjs`, `tokens.css`, `responsive.css`.
+- Reformat `app.js`. It is in `.prettierignore`; keep it that way.
+- Substitute a near-miss design token for a legacy value. Mark it:
+ `/* token-gap: ; owner design-system-keeper */`. See
+ `.agents/rules/gates.md`.
+
+## Done when
+
+- [ ] One island, driven by the collections task 05 filled
+- [ ] All nine behaviours reachable through it, including the provider/effort
+ coupling
+- [ ] Keyboard and focus complete
+- [ ] `pnpm run gate` green — the **full** gate, not just `verify` + `audit-ui`
+- [ ] 42 assertions intact
+- [ ] Report names the island's props so 15d can wire it without guessing
diff --git a/plans/astro-refactor/task-15b-copy-prompt.md b/plans/astro-refactor/task-15b-copy-prompt.md
new file mode 100644
index 0000000..0110697
--- /dev/null
+++ b/plans/astro-refactor/task-15b-copy-prompt.md
@@ -0,0 +1,55 @@
+# Task 15b — Copy-prompt buttons and reading progress
+
+**Agent**: `component-builder` · **Model**: MiniMax-M3 **Depends on**: 05 ·
+**Parallel with**: 15a, 15c · **Blocks**: 15d **Worktree**:
+`.agents/scripts/worktree.sh start 15b copy-prompt`
+
+## Scope
+
+Two small pieces of `app.js`, as islands. No page migration.
+
+### 1. `copyPrompt`
+
+Reads `#${button.dataset.copyTarget}`'s `textContent` and copies it.
+
+- Keeps `navigator.clipboard.writeText` **and** the `document.execCommand`
+ textarea fallback. The fallback exists because the site is served over plain
+ HTTP in workshop settings, where `navigator.clipboard` is undefined. Deleting
+ it silently breaks the lab for attendees. Keep both paths.
+- Writes a bilingual result string into `#copy-status`.
+- On success, swaps the button's `` to COPIED / COPIADO and back after
+ 1800 ms.
+
+Targets asserted by `verify.mjs`:
+`data-copy-target="prompt-install-skills|prompt-basic|prompt-skills"`.
+
+The prompt bodies are copy-pasted by attendees straight into an agent. **Exact
+whitespace and line breaks matter** — verify what lands on the clipboard is
+byte-identical to today's, not merely visually similar.
+
+### 2. Reading progress
+
+The `scroll` listener that sets `.reading-progress span`'s width. It is
+`{ passive: true }` today; keep it passive.
+
+## Language
+
+Both pieces read `currentLanguage`. Task 15c owns how language is held. Do not
+invent a second mechanism — take the language as a prop or read the document's
+`lang`, and say in your report which you chose so 15c and 15d can align.
+
+## Do not
+
+- Create `src/pages/full-guide.astro`. That is 15d.
+- Touch `verify.mjs`, `tokens.css`, `src/content/config.ts`.
+- Reformat `app.js`.
+- Substitute a near-miss token; mark gaps with
+ `/* token-gap: ; owner design-system-keeper */`.
+
+## Done when
+
+- [ ] Both clipboard paths present and the fallback actually exercised
+- [ ] `#copy-status` bilingual, and announced (it is a live region)
+- [ ] Clipboard payload byte-identical to today's for all three targets
+- [ ] `pnpm run gate` green — the full gate
+- [ ] 42 assertions intact
diff --git a/plans/astro-refactor/task-15c-language-toggle.md b/plans/astro-refactor/task-15c-language-toggle.md
new file mode 100644
index 0000000..41f7373
--- /dev/null
+++ b/plans/astro-refactor/task-15c-language-toggle.md
@@ -0,0 +1,62 @@
+# Task 15c — The language toggle
+
+**Agent**: `content-i18n-migrator` · **Model**: **Codex** **Depends on**: 05 ·
+**Parallel with**: 15a, 15b · **Blocks**: 15d **Worktree**:
+`.agents/scripts/worktree.sh start 15c language-toggle`
+
+## Why this is its own task
+
+This is the part of the full guide that does not survive a mechanical port, and
+it is the most likely reason task 15 failed twice.
+
+Today `applyLanguage` walks a `translations.pt` map of **CSS selector →
+Portuguese HTML** and overwrites `innerHTML` at each selector. It keeps an
+`originals` Map to restore English. That design cannot survive the migration:
+the selectors are page-structure coupling, and once the content is a collection
+the Portuguese string already lives beside the English one.
+
+## Deliver a decision, then an implementation
+
+Write the approach down in `.agents/context/content-i18n.md` (or the rule file
+it points at) **before** you build, because tasks 15d, 16, and 20 all depend on
+it and there is currently no stated answer.
+
+The realistic options:
+
+1. **Server-render both locales, toggle visibility.** Simple, no hydration cost
+ for text, doubles the HTML.
+2. **Server-render the saved locale, islands re-render on toggle.** Smaller
+ HTML; every island then needs both strings client-side anyway.
+3. **Separate routes per locale.** Cleanest, but changes URLs, which touches
+ publishing and every internal link — out of scope unless you argue for it and
+ the report flags it as a plan change.
+
+Pick one, say why, and note what it costs.
+
+## Behaviour that must not regress
+
+- `localStorage` key `ai-for-dummies-language`, wrapped in try/catch — previews
+ disable storage and an unguarded read throws.
+- `document.documentElement.lang` becomes `pt-BR` or `en`.
+- `[data-lang]` buttons get `.active` and `aria-pressed`.
+- Toggling language re-renders the active phase panel and every selector panel.
+ Coordinate with 15a: the island must expose a way to do this.
+- `client:idle` — page-wide, not urgent.
+
+## Do not
+
+- Create `src/pages/full-guide.astro`. That is 15d.
+- Edit `src/content/config.ts` schemas belonging to other collections beyond
+ what the toggle genuinely needs; if a schema is wrong, report it.
+- Touch `verify.mjs`.
+- Translate, rewrite, or "improve" any string. Both locales already exist in the
+ collections. This is plumbing, not copywriting.
+
+## Done when
+
+- [ ] Approach written down where 15d, 16 and 20 will find it
+- [ ] Toggle island built, `client:idle`, storage guarded
+- [ ] Both locales verified on a real rendered page, not just in theory
+- [ ] `pnpm run gate` green — the full gate
+- [ ] 42 assertions intact
+- [ ] Report states the contract 15d must satisfy
diff --git a/plans/astro-refactor/task-15d-page-full-guide.md b/plans/astro-refactor/task-15d-page-full-guide.md
new file mode 100644
index 0000000..1215dba
--- /dev/null
+++ b/plans/astro-refactor/task-15d-page-full-guide.md
@@ -0,0 +1,102 @@
+# Task 15d — Assemble /full-guide/
+
+**Agent**: `page-migrator` · **Model**: **Codex** **Depends on**: 05b, 10, 13,
+15a, 15b, 15c · **Parallel with**: 16 · **Blocks**: 15e, 18, 19 **Worktree**:
+`.agents/scripts/worktree.sh start 15d page-full-guide`
+
+## Goal
+
+`src/pages/full-guide.astro`. 22 KB of HTML, bilingual throughout, everything
+interactive already built by 15a/15b/15c and every block already built by
+task 10. **This task is assembly.** If you find yourself writing a new island,
+stop — it belongs to one of the earlier briefs and you should report the gap
+instead.
+
+Read the reports from 15a, 15b and 15c first. They state their props and the
+language contract.
+
+## Islands and nothing else
+
+| Island | Directive | From |
+| ------------------------------- | ---------------- | ---- |
+| Guide selector (one, ×9 groups) | `client:visible` | 15a |
+| Copy-prompt buttons + progress | `client:visible` | 15b |
+| Language toggle | `client:idle` | 15c |
+
+Everything else is server-rendered.
+
+## Asserted by verify.mjs — all must survive
+
+`const phases`, `const handsOnPrompts`, `const modelGuide`,
+`const skillSources`, `const skillInstallPrompts`, `render('plan')`,
+`renderTree`, `renderWorker`, `renderRoute`, `renderModelProvider`,
+`renderEffort`, `renderSkillFile`, `renderSkillWorkflow`, `renderCommonSkill`,
+`renderHandsOn`, `copyPrompt`, plus
+`data-copy-target="prompt-install-skills|prompt-basic|prompt-skills"`,
+`hands-on/starter/`, `additional-reading.md`, `role="tablist"`, `
`.
+
+Implementation-detail assertions, deliberately. **Never delete one.** Task 19
+re-points them to output-level checks; you do not.
+
+## Watch for
+
+- `hands-on/starter/` is a **lab fixture**. Link to it, ship it as a static
+ asset, do not componentize it. Same for `hands-on/rules/`.
+- `renderHandsOn` takes no argument — it is not part of 15a's selector pattern.
+ Check whether 15a covered it; if not, it is yours, and say so in the report.
+- Do not delete `responsive.css` here. That is 15e, and it needs screenshots.
+
+## Done when
+
+- [ ] Snapshot diff against `.agents/snapshots/` empty
+- [ ] Every interaction works: all nine selector groups, both languages, all
+ three copy buttons
+- [ ] Keyboard: arrows move between tabs; focus visible throughout
+- [ ] JS payload **smaller** than today's 50 KB — content is static now
+- [ ] Screenshots match at 560 / 800 / 1100 / 1600 px
+- [ ] `pnpm run gate` green — the full gate, not `verify` + `audit-ui` alone
+- [ ] 42 assertions intact
+
+## What 15a, 15b and 15c actually shipped
+
+Read `.agents/context/content-i18n.md` first — it is the language contract and
+it is binding on this task.
+
+- **`src/components/islands/GuideSelector.astro`** — one island, all nine
+ groups. Render it as
+ ``. `data` needs
+ `phases`, `workers`, `trees`, `routes`, `providers`, `efforts`, `skillFiles`,
+ `skillWorkflow`, `commonSkills` and a bilingual `labels` object; the
+ `GuideSelectorData` interface at the top of the file is the exact shape. You
+ server-render each group's shell and its initial detail panel, keeping today's
+ `data-*` hooks and `.active` state. Mark each group `role="tablist"` and its
+ controls `role="tab"` or the keyboard handler will not bind.
+- **`src/components/islands/LanguageToggle.astro`** — render each localized
+ fragment twice, with `data-language-content="en"` or `"pt"` on the outer
+ element. The toggle flips `hidden` on those, sets ``, and fires
+ `ai-for-dummies:languagechange` on `window`. It is a plain `.astro` island
+ that defers its own setup with `requestIdleCallback`; **do not** put
+ `client:idle` on it — that directive is framework-components only.
+- **`src/components/islands/CopyPrompt.astro`** — one per button. Pass
+ `target="prompt-install-skills" | "prompt-basic" | "prompt-skills"`. Render
+ `
` exactly once on the
+ page; the island writes into it. Fill the `
` bodies
+ from the `handsOnPrompts` and `skillInstallPrompts` collections — 15b verified
+ those are byte-identical to the legacy `app.js` constants, and the clipboard
+ copies whatever you render, so do not reformat them.
+- **`src/components/islands/ReadingProgress.astro`** — replaces the legacy
+ `
` at the top of the page.
+
+Nothing else is missing. If you are about to write an island, you are doing
+another task's work — report the gap instead.
+
+## Two things to expect
+
+- **The snapshot will not match by construction.** Dual-locale rendering emits
+ both languages into the HTML where today's page emits English plus a
+ Portuguese map inside `app.js`. Compare _rendered, language-filtered_ output
+ against today's page, and if `.agents/snapshots/` needs regenerating, say so
+ explicitly in your report with what changed and why — do not quietly rewrite a
+ snapshot to make a diff go away.
+- **`renderHandsOn` takes no argument** and is not part of GuideSelector's
+ nine-group pattern. It is yours. Its name is asserted by `verify.mjs`.
diff --git a/plans/astro-refactor/task-15e-responsive-css.md b/plans/astro-refactor/task-15e-responsive-css.md
new file mode 100644
index 0000000..fc69a5e
--- /dev/null
+++ b/plans/astro-refactor/task-15e-responsive-css.md
@@ -0,0 +1,42 @@
+# Task 15e — Retire responsive.css
+
+**Agent**: `design-system-keeper` · **Model**: `agy` (Gemini 3.1 Pro — vision)
+**Depends on**: 15d, 16 · **Blocks**: 20 **Worktree**:
+`.agents/scripts/worktree.sh start 15e responsive-css`
+
+## Goal
+
+`responsive.css` is 30 KB and mostly served `/full-guide/`. Once 15d and 16 have
+landed, port what the Astro pages still need into component styles or
+`tokens.css`, prove the remainder dead, and delete it.
+
+**Proof is screenshots, not reading.** A rule that looks unused because no
+selector matches at 1600 px may be the only thing holding the 560 px layout
+together.
+
+## Method
+
+1. Build. Screenshot every migrated route at 560 / 800 / 1100 / 1600 px.
+2. Remove `responsive.css` from the build entirely.
+3. Screenshot again. Every diff is a rule you must port.
+4. Port it into the owning component's `
diff --git a/src/components/islands/GuideSelector.astro b/src/components/islands/GuideSelector.astro
new file mode 100644
index 0000000..1c2dc67
--- /dev/null
+++ b/src/components/islands/GuideSelector.astro
@@ -0,0 +1,294 @@
+---
+// GuideSelector — one client:visible controller for the nine independent
+// selectors on /full-guide/. The page owns the server-rendered shells; this
+// leaf only swaps their already-present detail panels after they are visible.
+
+interface Localized {
+ en: string;
+ pt: string;
+}
+interface GuideSelectorData {
+ phases: Record;
+ workers: Record;
+ trees: Record;
+ routes: Record;
+ providers: Record<
+ string,
+ {
+ label: string;
+ source: string;
+ title: Localized;
+ copy: Localized;
+ tiers: [string, string, Localized][];
+ config: string;
+ }
+ >;
+ efforts: Record;
+ skillFiles: Record;
+ skillWorkflow: Record<
+ string,
+ {
+ number: string;
+ title: Localized;
+ question: Localized;
+ action: Localized;
+ output: Localized;
+ proof: Localized;
+ }
+ >;
+ commonSkills: Record<
+ string,
+ {
+ number: string;
+ kind: Localized;
+ title: string;
+ rule: Localized;
+ use: Localized;
+ example: Localized;
+ caution: Localized;
+ source: string;
+ }
+ >;
+ labels: {
+ context: Localized;
+ owner: Localized;
+ reasoningLoad: Localized;
+ officialSource: Localized;
+ skillFileHint: Localized;
+ workflow: { question: Localized; action: Localized; artifact: Localized; proof: Localized };
+ commonSkill: {
+ whenToUse: Localized;
+ example: Localized;
+ watchOut: Localized;
+ source: Localized;
+ };
+ };
+}
+interface Props {
+ /** The page root that contains the nine static selector shells. */
+ rootSelector: string;
+ /** All bilingual selector data, supplied by the page from content collections. */
+ data: GuideSelectorData;
+}
+const { rootSelector, data } = Astro.props;
+---
+
+
+
+
+
+
+
diff --git a/src/components/islands/LanguageToggle.astro b/src/components/islands/LanguageToggle.astro
new file mode 100644
index 0000000..f44f7fa
--- /dev/null
+++ b/src/components/islands/LanguageToggle.astro
@@ -0,0 +1,68 @@
+---
+// LanguageToggle — idle-hydrated, full-guide-only locale control.
+//
+// The guide server-renders both locales. This leaf owns the persisted locale
+// and announces changes so selector islands can rebuild their active panels
+// from collection data without knowing about this control's markup.
+---
+
+
+
+ /
+
+
+
+
diff --git a/src/components/islands/ReadingProgress.astro b/src/components/islands/ReadingProgress.astro
new file mode 100644
index 0000000..ad6d2f1
--- /dev/null
+++ b/src/components/islands/ReadingProgress.astro
@@ -0,0 +1,57 @@
+---
+// ReadingProgress — scroll-position bar. One instance per page.
+//
+// Renders the same markup the legacy `full-guide/index.html` line 13
+// carried: `
`.
+// The scroll listener is registered with `{ passive: true }`, matching
+// app.js line 406 and the rule in `.agents/rules/animation.md` that a
+// passive listener is required for any handler bound to a frequently
+// fired event like scroll. Animating `width` here is fine — the bar
+// is a 3px element at the very top of the viewport, so layout cost
+// is negligible compared to the alternative of subscribing to
+// IntersectionObserver and computing progress from a single sentinel
+// per section.
+//
+// Language: this island does not read language. It has no string
+// labels and the same progress semantics apply to EN and PT.
+
+interface Props {
+ // Optional override selector. Defaults to `.reading-progress span`
+ // to match the legacy app.js query, in case a page renders the bar
+ // with a different class name.
+ target?: string;
+}
+
+const { target = '.reading-progress span' } = Astro.props;
+---
+
+