Step 1: Stages
1. **scout** — `subagent scout` with `output: context.md`, `context: fresh`, `async: true`. Map the relevant code, list the files to touch, capture conventions.
2. **ollama-planner** — reads `context.md`, writes `plan.md`. `context: fork`, `async: true`.
3. **ollama-coder** — reads `plan.md`, implements, runs validation. `context: fork`, `async: true`, `defaultContext: fork`.
4. **parallel review** — three `ollama-reviewer` instances with distinct angles (correctness, tests/validation, simplicity/security). `context: fresh`, `concurrency: 3`, `async: true`.
5. **synthesize** — parent reads the three reviews, applies `worth fixing now` items via one more `ollama-coder` run (or manually), then summarizes for the user.

Step 2: When to use
- A new feature spanning 2+ files.
- A non-trivial bug fix touching multiple components.
- A refactor with a clear target shape.

Step 3: When NOT to use
- Single-line fixes → just edit directly.
- Pure research questions → use `research-brief` instead.
- Changes that need user clarification first → ask the user, then start the chain.

Step 4: Inputs
The chain task should include: the feature request, the target repo path, any constraints (must keep API stable, must not touch X, must add tests), and any attached screenshots.

Step 5: Validation contract (set before launching)
Before stage 3, write down in the task prompt:
- expected behavior after the change
- tests or commands the coder must run
- evidence the coder must return (changed files, command output, screenshots)

Step 6: Stop rules
- If `scout` finds the task is out of scope or the codebase is in an unexpected state, stop and ask the user.
- If `ollama-planner` surfaces ambiguity in "Open Questions", stop and ask the user before launching the coder.
- If a reviewer reports a `blocker` or an unapproved decision, stop and ask the user.
- If a `worth fixing now` item is small and safe, parent applies it directly; if it touches product/architecture, ask the user first.


## Subagent result

Unknown agent: Stages (step 1)

Unknown agent: Stages (step 1)


Step 1: scout
Map the relevant code for {task}. List files to touch, capture conventions, identify risks, and write findings to {output}.

Step 2: ollama-planner
Read {reads} and create a concrete implementation plan for {task}. Write ordered tasks with file paths, acceptance criteria, and risks to {output}.

Step 3: ollama-coder
Execute the plan from {reads}. Implement the smallest correct changes, run validation, and report changed files with evidence to {output}.

Step 4: reviewer
Review the implementation against the plan. Focus on: correctness, regressions, edge cases, and whether the change does what it claims. Write findings to {output}.

Step 5: reviewer
Review test coverage and validation. Focus on: tests added/updated, do they exercise the new code, are they sufficient to catch regressions. Write findings to {output}.

Step 6: reviewer
Review for simplicity, maintainability, and security. Focus on: smallest correct change, readability, naming consistency, input validation, secrets, injection risks. Write findings to {output}.

Step 7: worker
Synthesize the three reviews from {reads}. Apply fixes worth doing now (small, safe changes). Report: what was fixed, what was deferred, and any blockers that need user decision.

scout ollama-planner ollama-coder reviewer reviewer reviewer worker


<file name="/tmp/pi-subagent-BSjXCh/task.md">
Task: [Read from: /home/liph/.pi-subagents/chain-runs/f9677476/context.md]

You are a delegated subagent running from a fork of the parent session. Treat the inherited conversation as reference-only context, not a live thread to continue. Do not continue or answer prior messages as if they are waiting for a reply. Your sole job is to execute the task below and return a focused result for that task using your tools.

Task:
Read {reads} and create a concrete implementation plan for test. Write ordered tasks with file paths, acceptance criteria, and risks to {output}.

---
Update progress at: /home/liph/.pi-subagents/chain-runs/f9677476/progress.md
Previous step output:
# Code Context — Scout for webpage test mapping

**Project:** `/mnt/flash1/code/webpage` — "Grounded Brotherhood" static marketing site
**Task:** Map the relevant code surface so a follow-up agent can plan and write tests.
**Project conventions source:** `AGENTS.md` (in-repo).

---

## Files Retrieved

1. `/mnt/flash1/code/webpage/AGENTS.md` (lines 1-40) — architecture, CSS split, reveal pattern, gotchas.
2. `/mnt/flash1/code/webpage/index.html` (lines 1-292) — single-page, 5 sections, 1 marquee, footer.
3. `/mnt/flash1/code/webpage/js/main.js` (lines 1-165) — IIFE; smooth scroll, mobile menu, active link, IntersectionObserver, parallax.
4. `/mnt/flash1/code/webpage/css/main.css` (855 lines) — tokens in `:root`, base layout, `.reveal` / `.reveal.visible` (lines 846-852).
5. `/mnt/flash1/code/webpage/css/responsive.css` (242 lines) — breakpoints at 991px, 767px, 479px, 1400px (lines 9, 84, 184, 233).
6. `/mnt/flash1/code/webpage/orchestration.md` — prior static-audit artifact; shows in-tree audit convention.

---

## Architecture

```
webpage/
├── index.html              # entry: one <body> with 5 <section>s + 1 marquee + footer
├── js/main.js              # single IIFE, all behavior
├── css/
│   ├── main.css            # design tokens (:root) + desktop-first base layout + .reveal
│   └── responsive.css      # 4 @media breakpoint overrides only
├── assets/images/          # empty — all images are external Unsplash URLs
└── orchestration.md        # in-tree audit artifact (read-only, do not overwrite)
```

- **No build step, no package.json, no node_modules.** Per `AGENTS.md`: "edit files directly; open `index.html` in browser to verify."
- **Section IDs are the public contract** — `#home`, `#about`, `#practices`, `#retreats`, `#contact` are both anchor targets and the key the active-link observer uses.
- **Two behavior surfaces with hidden coupling:**
  - `.reveal` is added by JS, but the *transition* lives in CSS. Both must agree on the selector list.
  - The smooth-scroll `offset = 80` is hard-coded in JS; CSS `--offset` (if any) is duplicated.
- **Three external dependencies** (no local fallbacks): Google Fonts CSS + WOFF2 files, and 9 Unsplash images.

---

## Key Code

### JS — `js/main.js`

| Section | Lines | Notes for tests |
|---|---|---|
| IIFE wrapper | 1-9 | Strict mode; nothing exported. Tests must evaluate the file in a real DOM context (jsdom) to trigger the IIFE. |
| DOM refs | 11-17 | `navbar`, `menuBtn`, `navLinks` (combined header + footer), `revealElements`. Fail loud if any selector is missing. |
| `updateNav` | 19-31 | Adds/removes `.scrolled` on `navbar` based on `scrollY > 60`. Wired with `{passive: true}`. |
| `toggleMenu` | 35-40 | Toggles `body.open`, sets `aria-expanded`, locks `body.style.overflow`. |
| Smooth scroll | 47-67 | Catches `click` on any `#navLinks a, .footer-nav a` whose `href` starts with `#`. Subtracts 80px. |
| Active link | 71-92 | Iterates `section[id]`, finds the one whose `offsetTop..offsetTop+offsetHeight` contains `scrollY+120`, adds `.active` to matching links. |
| Reveal observer | 96-121 | Uses `IntersectionObserver` with `threshold: 0.12`; adds `.visible` once, then `unobserve`. Has a non-IO fallback that adds both `.reveal` and `.visible`. |
| Parallax | 125-136 | Bails on `< 768px`. Writes `transform: scale(1.06) translateY(...)` on every scroll tick while inside hero. No throttle. |
| Init | 140-144 | `DOMContentLoaded` re-runs `updateNav` and `updateActiveLink`. |

### CSS — selectors that JS depends on

From `main.js` line 17:
```js
'.about-card, .practice-item, .retreat-card, .section-head, .contact-info, .contact-image'
```
All six exist in `main.css` (`.about-card` 528, `.practice-item` 580, `.retreat-card` 637, `.section-head` 475, `.contact-info` 728, `.contact-image` 772).

CSS reveal state — `main.css` 846-852:
```css
.reveal { opacity: 0; transform: translateY(30px); transition: ...; }
.reveal.visible { opacity: 1; transform: translateY(0); }
```

### HTML — anchor + id wiring (assertion target)

- Header nav: `#home`, `#about`, `#practices`, `#retreats`, `#contact` — all resolve to real `<section id="...">`s.
- Footer nav duplicates the same five anchors.
- In-page CTAs: `#retreats`, `#about` (hero), `#contact` (each `.retreat-card` "Inquire Now"), `#home` (footer brand).
- `mailto:hello@groundedbrotherhood.com` in contact section.

### Breakpoints — `responsive.css`

| Media query | Line | Use |
|---|---|---|
| `(max-width: 991px)` | 9 | Tablet collapse |
| `(max-width: 767px)` | 84 | Mobile layout + parallax disable threshold (matches JS) |
| `(max-width: 479px)` | 184 | Small phones |
| `(min-width: 1400px)` | 233 | Large desktop |

The 768px threshold appears in **both** `js/main.js` (`window.innerWidth < 768`) and is implicit in the `(max-width: 767px)` block in CSS — coupling risk.

---

## Start Here

**`/mnt/flash1/code/webpage/js/main.js`** (lines 1-165). Open this first because:
1. It is the smallest file with the densest behavior.
2. It defines every selector any test will need to assert against.
3. The reveal observer and parallax listener are the only places that touch `transform` / `IntersectionObserver` — highest test-design risk.

Then read `index.html` to confirm the DOM contract (IDs, anchor wiring, `aria-expanded` initial state), and finally `main.css` lines 846-852 to confirm the reveal transition.

---

## Files to Touch (test implementation)

| File | Change | Severity |
|---|---|---|
| `package.json` (new) | Add `vitest` + `jsdom` + a `test` script. | required |
| `index.html` | None. Tests load the file, do not edit it. | none |
| `js/main.js` | None for unit tests. Consider extracting `offset = 80` to a named export for visibility — but the IIFE is not exported, so a refactor is the only way. **Risk:** refactor changes the production file just to make it testable; weigh against non-testable scope. | low/optional |
| `tests/dom.contract.test.js` (new) | Assert every selector `main.js` queries exists in `index.html`; assert every `href="#x"` in nav/footer resolves to a real `id="x"`; assert all images have `alt`. | required |
| `tests/scroll-behavior.test.js` (new) | Stub `IntersectionObserver`, fire `scroll` events, assert `.scrolled` toggles at 60px and `.active` moves across sections. | required |
| `tests/mobile-menu.test.js` (new) | Click `menuBtn`, assert `body.open`, `aria-expanded="true"`, `body.style.overflow === "hidden"`. Click a nav link, assert all three reset. | required |
| `tests/parallax.test.js` (new) | Stub `window.innerWidth = 1024` and 600, assert transform string applied / not applied respectively. | required |
| `orchestration.md` | Append a `## Test plan` section if the parent wants the test rationale captured in-tree (matches existing convention). | optional |
| `vitest.config.js` (new) | Set `environment: 'jsdom'`, `include: ['tests/**/*.test.js']`. | required |
| `.gitignore` | Add `node_modules/`. | required |

**No production source files (index.html, js/main.js, css/*) need modification to add tests** — the only refactor candidate is exporting the nav offset, and that is optional.

---

## Conventions (must follow when writing tests)

- **No build step.** The test harness is a new dep but should not introduce a bundler. Vitest is the lightest match.
- **CSS split is locked** (AGENTS.md). If a test reads CSS, read both files separately and assert the breakpoint union, not a merged file.
- **Reveal selector list is the single source of truth** for which elements animate. Tests should assert the list contents, not just behavior, so removing a section doesn't silently break coverage.
- **Section IDs are the anchor contract** — never rename `#home` etc. without a coordinated HTML+JS+CSS edit.
- **Mobile menu state is `body.open`** — tests should not invent a different signal.
- **Parallax is intentionally desktop-only** — tests at `innerWidth = 600` must assert no `transform` write.

---

## Risks

| # | Risk | Severity | Mitigation |
|---|---|---|---|
| R1 | No test harness exists; `package.json` must be bootstrapped. | medium | Use `vitest` + `jsdom`; both are the smallest viable stack for vanilla JS. |
| R2 | `main.js` is an IIFE — no exports. Behavior can only be observed via DOM, not by calling functions directly. | medium | Accept this. Tests are black-box against the rendered DOM, which is the higher-value contract anyway. |
| R3 | `IntersectionObserver` does not exist in jsdom. The reveal path will hit the no-IO fallback. | high | Stub `globalThis.IntersectionObserver` in a setup file. Without this, the reveal test passes for the wrong reason. |
| R4 | External CDN dependencies (Google Fonts, Unsplash) are not loadable offline and are irrelevant to behavior tests. | low | Do not await images. Stub `Image.prototype.decode` if needed. |
| R5 | Parallax writes `style.transform` on every scroll tick. No throttle. If a future test asserts only "parallax is non-jank", it will fail. | low | Do not assert performance. Only assert presence/absence of the transform string. |
| R6 | The `80px` offset is duplicated in JS (smooth scroll, active-link trigger at `+120` is a similar constant). Two related magic numbers; tests will catch breakage but the coupling is fragile. | low | If the offset is ever changed in `main.js`, both call sites must be updated; consider a small test that asserts the offset is positive and finite. |
| R7 | The 768px mobile breakpoint is duplicated between JS (`< 768`) and CSS (`max-width: 767px`). Off-by-one. | low | Add a test that asserts the JS threshold equals `CSS_BREAKPOINT + 1` to surface drift. |
| R8 | Three sections expose `aria-expanded` semantics only on the menu button. No `aria-controls` (called out in `orchestration.md` as a minor a11y improvement). | low | A11y test should assert the *present* a11y attrs, not the missing ones, to avoid noisy failures. |
| R9 | Active-link observer uses `offsetTop`/`offsetHeight`, which jsdom returns as `0` for everything not explicitly styled. Behavior assertions need a layout stub. | medium | In tests, manually set `Object.defineProperty(el, 'offsetTop', {value: N})` for the sections. |
| R10 | No CI config exists; running tests is manual until a `test` script + runner are wired. | low | Out of scope for this scout; flag for the parent. |

---

## Open Questions for the Parent

1. **Harness choice** — vitest + jsdom is recommended, but the project has no prior Node footprint. Is adding `package.json` acceptable, or should tests live in a sibling repo?
2. **Scope** — unit tests only, or also Playwright visual-regression for the 4 breakpoints? (The latter is much larger and needs browser binaries.)
3. **Refactor tolerance** — should `main.js` be lightly refactored to export the offset constant and the reveal selector list, or stay pure IIFE?
4. **CI** — is there a target runner, or is "tests pass locally" the bar?

---

## Quick Reference — selectors the test suite must cover

From `js/main.js`:
- `#navbar`, `#menuBtn`, `#navLinks`, `.footer-nav`
- `section[id]` (5 of them, must all be unique)
- `.about-card`, `.practice-item`, `.retreat-card`, `.section-head`, `.contact-info`, `.contact-image`
- `.hero`, `.hero-img img`

From `index.html` (must resolve):
- 5 nav anchors (`#home`, `#about`, `#practices`, `#retreats`, `#contact`)
- 4 in-page CTAs (`#retreats`, `#about`, `#contact` ×3, `#home`)
- 1 `mailto:` link
- All `<img>` have `alt` (9 external images + 0 local)

---
**Output:**
Write your findings to exactly this path: /home/liph/.pi-subagents/chain-runs/f9677476/plan.md
This path is authoritative for this run.
Ignore any other output filename or output path mentioned elsewhere, including output destinations in the base agent prompt, system prompt, or task instructions.

## Acceptance Contract
Acceptance level: checked
Completion is not accepted from prose alone. End with a structured acceptance report.

Criteria:
- criterion-1: Implement the requested change without widening scope

Required evidence: changed-files, tests-added, commands-run, residual-risks, no-staged-files

Finish with a fenced JSON block tagged `acceptance-report` in this shape:
Use empty arrays when no items apply; array fields contain strings unless object entries are shown.
`criteriaSatisfied[].status` must be exactly one of: satisfied, not-satisfied, not-applicable.
`commandsRun[].result` must be exactly one of: passed, failed, not-run.
`manualNotes` and `notes` are optional strings; an empty string means no note and does not satisfy `manual-notes` evidence.
```acceptance-report
{
  "criteriaSatisfied": [
    {
      "id": "criterion-1",
      "status": "satisfied",
      "evidence": "specific proof"
    }
  ],
  "changedFiles": [
    "src/file.ts"
  ],
  "testsAddedOrUpdated": [
    "test/file.test.ts"
  ],
  "commandsRun": [
    {
      "command": "command",
      "result": "passed",
      "summary": "short result"
    }
  ],
  "validationOutput": [
    "validation output or concise summary"
  ],
  "residualRisks": [
    "none"
  ],
  "noStagedFiles": true,
  "diffSummary": "short description of the diff",
  "reviewFindings": [
    "blocker: file.ts:12 - issue found, or no blockers"
  ],
  "manualNotes": "anything else the parent should know"
}
```
</file>


Let me start by reading the context file to understand what I'm working with.


# Code Context — Scout for webpage test mapping

**Project:** `/mnt/flash1/code/webpage` — "Grounded Brotherhood" static marketing site
**Task:** Map the relevant code surface so a follow-up agent can plan and write tests.
**Project conventions source:** `AGENTS.md` (in-repo).

---

## Files Retrieved

1. `/mnt/flash1/code/webpage/AGENTS.md` (lines 1-40) — architecture, CSS split, reveal pattern, gotchas.
2. `/mnt/flash1/code/webpage/index.html` (lines 1-292) — single-page, 5 sections, 1 marquee, footer.
3. `/mnt/flash1/code/webpage/js/main.js` (lines 1-165) — IIFE; smooth scroll, mobile menu, active link, IntersectionObserver, parallax.
4. `/mnt/flash1/code/webpage/css/main.css` (855 lines) — tokens in `:root`, base layout, `.reveal` / `.reveal.visible` (lines 846-852).
5. `/mnt/flash1/code/webpage/css/responsive.css` (242 lines) — breakpoints at 991px, 767px, 479px, 1400px (lines 9, 84, 184, 233).
6. `/mnt/flash1/code/webpage/orchestration.md` — prior static-audit artifact; shows in-tree audit convention.

---

## Architecture

```
webpage/
├── index.html              # entry: one <body> with 5 <section>s + 1 marquee + footer
├── js/main.js              # single IIFE, all behavior
├── css/
│   ├── main.css            # design tokens (:root) + desktop-first base layout + .reveal
│   └── responsive.css      # 4 @media breakpoint overrides only
├── assets/images/          # empty — all images are external Unsplash URLs
└── orchestration.md        # in-tree audit artifact (read-only, do not overwrite)
```

- **No build step, no package.json, no node_modules.** Per `AGENTS.md`: "edit files directly; open `index.html` in browser to verify."
- **Section IDs are the public contract** — `#home`, `#about`, `#practices`, `#retreats`, `#contact` are both anchor targets and the key the active-link observer uses.
- **Two behavior surfaces with hidden coupling:**
  - `.reveal` is added by JS, but the *transition* lives in CSS. Both must agree on the selector list.
  - The smooth-scroll `offset = 80` is hard-coded in JS; CSS `--offset` (if any) is duplicated.
- **Three external dependencies** (no local fallbacks): Google Fonts CSS + WOFF2 files, and 9 Unsplash images.

---

## Key Code

### JS — `js/main.js`

| Section | Lines | Notes for tests |
|---|---|---|
| IIFE wrapper | 1-9 | Strict mode; nothing exported. Tests must evaluate the file in a real DOM context (jsdom) to trigger the IIFE. |
| DOM refs | 11-17 | `navbar`, `menuBtn`, `navLinks` (combined header + footer), `revealElements`. Fail loud if any selector is missing. |
| `updateNav` | 19-31 | Adds/removes `.scrolled` on `navbar` based on `scrollY > 60`. Wired with `{passive: true}`. |
| `toggleMenu` | 35-40 | Toggles `body.open`, sets `aria-expanded`, locks `body.style.overflow`. |
| Smooth scroll | 47-67 | Catches `click` on any `#navLinks a, .footer-nav a` whose `href` starts with `#`. Subtracts 80px. |
| Active link | 71-92 | Iterates `section[id]`, finds the one whose `offsetTop..offsetTop+offsetHeight` contains `scrollY+120`, adds `.active` to matching links. |
| Reveal observer | 96-121 | Uses `IntersectionObserver` with `threshold: 0.12`; adds `.visible` once, then `unobserve`. Has a non-IO fallback that adds both `.reveal` and `.visible`. |
| Parallax | 125-136 | Bails on `< 768px`. Writes `transform: scale(1.06) translateY(...)` on every scroll tick while inside hero. No throttle. |
| Init | 140-144 | `DOMContentLoaded` re-runs `updateNav` and `updateActiveLink`. |

### CSS — selectors that JS depends on

From `main.js` line 17:
```js
'.about-card, .practice-item, .retreat-card, .section-head, .contact-info, .contact-image'
```
All six exist in `main.css` (`.about-card` 528, `.practice-item` 580, `.retreat-card` 637, `.section-head` 475, `.contact-info` 728, `.contact-image` 772).

CSS reveal state — `main.css` 846-852:
```css
.reveal { opacity: 0; transform: translateY(30px); transition: ...; }
.reveal.visible { opacity: 1; transform: translateY(0); }
```

### HTML — anchor + id wiring (assertion target)

- Header nav: `#home`, `#about`, `#practices`, `#retreats`, `#contact` — all resolve to real `<section id="...">`s.
- Footer nav duplicates the same five anchors.
- In-page CTAs: `#retreats`, `#about` (hero), `#contact` (each `.retreat-card` "Inquire Now"), `#home` (footer brand).
- `mailto:hello@groundedbrotherhood.com` in contact section.

### Breakpoints — `responsive.css`

| Media query | Line | Use |
|---|---|---|
| `(max-width: 991px)` | 9 | Tablet collapse |
| `(max-width: 767px)` | 84 | Mobile layout + parallax disable threshold (matches JS) |
| `(max-width: 479px)` | 184 | Small phones |
| `(min-width: 1400px)` | 233 | Large desktop |

The 768px threshold appears in **both** `js/main.js` (`window.innerWidth < 768`) and is implicit in the `(max-width: 767px)` block in CSS — coupling risk.

---

## Start Here

**`/mnt/flash1/code/webpage/js/main.js`** (lines 1-165). Open this first because:
1. It is the smallest file with the densest behavior.
2. It defines every selector any test will need to assert against.
3. The reveal observer and parallax listener are the only places that touch `transform` / `IntersectionObserver` — highest test-design risk.

Then read `index.html` to confirm the DOM contract (IDs, anchor wiring, `aria-expanded` initial state), and finally `main.css` lines 846-852 to confirm the reveal transition.

---

## Files to Touch (test implementation)

| File | Change | Severity |
|---|---|---|
| `package.json` (new) | Add `vitest` + `jsdom` + a `test` script. | required |
| `index.html` | None. Tests load the file, do not edit it. | none |
| `js/main.js` | None for unit tests. Consider extracting `offset = 80` to a named export for visibility — but the IIFE is not exported, so a refactor is the only way. **Risk:** refactor changes the production file just to make it testable; weigh against non-testable scope. | low/optional |
| `tests/dom.contract.test.js` (new) | Assert every selector `main.js` queries exists in `index.html`; assert every `href="#x"` in nav/footer resolves to a real `id="x"`; assert all images have `alt`. | required |
| `tests/scroll-behavior.test.js` (new) | Stub `IntersectionObserver`, fire `scroll` events, assert `.scrolled` toggles at 60px and `.active` moves across sections. | required |
| `tests/mobile-menu.test.js` (new) | Click `menuBtn`, assert `body.open`, `aria-expanded="true"`, `body.style.overflow === "hidden"`. Click a nav link, assert all three reset. | required |
| `tests/parallax.test.js` (new) | Stub `window.innerWidth = 1024` and 600, assert transform string applied / not applied respectively. | required |
| `orchestration.md` | Append a `## Test plan` section if the parent wants the test rationale captured in-tree (matches existing convention). | optional |
| `vitest.config.js` (new) | Set `environment: 'jsdom'`, `include: ['tests/**/*.test.js']`. | required |
| `.gitignore` | Add `node_modules/`. | required |

**No production source files (index.html, js/main.js, css/*) need modification to add tests** — the only refactor candidate is exporting the nav offset, and that is optional.

---

## Conventions (must follow when writing tests)

- **No build step.** The test harness is a new dep but should not introduce a bundler. Vitest is the lightest match.
- **CSS split is locked** (AGENTS.md). If a test reads CSS, read both files separately and assert the breakpoint union, not a merged file.
- **Reveal selector list is the single source of truth** for which elements animate. Tests should assert the list contents, not just behavior, so removing a section doesn't silently break coverage.
- **Section IDs are the anchor contract** — never rename `#home` etc. without a coordinated HTML+JS+CSS edit.
- **Mobile menu state is `body.open`** — tests should not invent a different signal.
- **Parallax is intentionally desktop-only** — tests at `innerWidth = 600` must assert no `transform` write.

---

## Risks

| # | Risk | Severity | Mitigation |
|---|---|---|---|
| R1 | No test harness exists; `package.json` must be bootstrapped. | medium | Use `vitest` + `jsdom`; both are the smallest viable stack for vanilla JS. |
| R2 | `main.js` is an IIFE — no exports. Behavior can only be observed via DOM, not by calling functions directly. | medium | Accept this. Tests are black-box against the rendered DOM, which is the higher-value contract anyway. |
| R3 | `IntersectionObserver` does not exist in jsdom. The reveal path will hit the no-IO fallback. | high | Stub `globalThis.IntersectionObserver` in a setup file. Without this, the reveal test passes for the wrong reason. |
| R4 | External CDN dependencies (Google Fonts, Unsplash) are not loadable offline and are irrelevant to behavior tests. | low | Do not await images. Stub `Image.prototype.decode` if needed. |
| R5 | Parallax writes `style.transform` on every scroll tick. No throttle. If a future test asserts only "parallax is non-jank", it will fail. | low | Do not assert performance. Only assert presence/absence of the transform string. |
| R6 | The `80px` offset is duplicated in JS (smooth scroll, active-link trigger at `+120` is a similar constant). Two related magic numbers; tests will catch breakage but the coupling is fragile. | low | If the offset is ever changed in `main.js`, both call sites must be updated; consider a small test that asserts the offset is positive and finite. |
| R7 | The 768px mobile breakpoint is duplicated between JS (`< 768`) and CSS (`max-width: 767px`). Off-by-one. | low | Add a test that asserts the JS threshold equals `CSS_BREAKPOINT + 1` to surface drift. |
| R8 | Three sections expose `aria-expanded` semantics only on the menu button. No `aria-controls` (called out in `orchestration.md` as a minor a11y improvement). | low | A11y test should assert the *present* a11y attrs, not the missing ones, to avoid noisy failures. |
| R9 | Active-link observer uses `offsetTop`/`offsetHeight`, which jsdom returns as `0` for everything not explicitly styled. Behavior assertions need a layout stub. | medium | In tests, manually set `Object.defineProperty(el, 'offsetTop', {value: N})` for the sections. |
| R10 | No CI config exists; running tests is manual until a `test` script + runner are wired. | low | Out of scope for this scout; flag for the parent. |

---

## Open Questions for the Parent

1. **Harness choice** — vitest + jsdom is recommended, but the project has no prior Node footprint. Is adding `package.json` acceptable, or should tests live in a sibling repo?
2. **Scope** — unit tests only, or also Playwright visual-regression for the 4 breakpoints? (The latter is much larger and needs browser binaries.)
3. **Refactor tolerance** — should `main.js` be lightly refactored to export the offset constant and the reveal selector list, or stay pure IIFE?
4. **CI** — is there a target runner, or is "tests pass locally" the bar?

---

## Quick Reference — selectors the test suite must cover

From `js/main.js`:
- `#navbar`, `#menuBtn`, `#navLinks`, `.footer-nav`
- `section[id]` (5 of them, must all be unique)
- `.about-card`, `.practice-item`, `.retreat-card`, `.section-head`, `.contact-info`, `.contact-image`
- `.hero`, `.hero-img img`

From `index.html` (must resolve):
- 5 nav anchors (`#home`, `#about`, `#practices`, `#retreats`, `#contact`)
- 4 in-page CTAs (`#retreats`, `#about`, `#contact` ×3, `#home`)
- 1 `mailto:` link
- All `<img>` have `alt` (9 external images + 0 local)


I have the context. The task is to create a concrete implementation plan for tests for the webpage project. Let me check if the project exists locally and verify a few key details before writing the plan.

Let me check the project directory and look at the actual files to make sure my plan is accurate.


.crew/
.pi-subagents/
AGENTS.md
assets/
css/
index.html
js/
orchestration.md


# AGENTS.md — Grounded Brotherhood Website

Static marketing site (HTML/CSS/JS). No build tools, no frameworks, no dependencies.

## Architecture

- Entry: `index.html` — single-page, section-scrolling landing page.
- Styles: `css/main.css` (design tokens + desktop-first layout, 855 lines) then `css/responsive.css` (mobile/tablet overrides, 242 lines). Order in `<head>` matters.
- JS: `js/main.js` (IIFE) — smooth-scroll nav, mobile menu, scroll-reveal observer, subtle hero parallax.
- Images: external Unsplash URLs; no local image assets to manage (empty `assets/`).

## Working conventions

- **Keep the CSS split**: `main.css` for tokens + base layout; `responsive.css` for breakpoints only. Do not merge them.
- **Color palette is locked** — tokens live in `:root` (`--ivory`, `--cream`, `--sand`, `--gold`, `--forest`, `--sage`, `--moss`, `--ink`). Prefer reusing existing tokens over adding new raw colors.
- **Fonts**: Google Fonts via CDN — `Cormorant Garamond` (serif) and `Jost` (sans). Keep `font-family` declarations referencing the custom properties.
- **Reveal classes**: JS adds `.reveal` to targeted elements; CSS transitions from `opacity: 0 / translateY(30px)` to `.visible` state. When adding new animated sections, add the selector to the `revealElements` NodeList in `main.js`.
- **Nav offset**: smooth-scroll anchors subtract 80px for fixed nav height (`--offset = 80`). Update in JS if nav height changes.
- **Parallax disabled below 768px**: `handleParallax` returns early on narrow screens.
- **No bundler / no transpile**: edit files directly; open `index.html` in browser to verify.

## Gotchas

- `index.html` links `css/main.css` and `css/responsive.css`, not `css/style.css`.
- Body class `open` toggles mobile menu and sets `overflow: hidden`. Ensure any new modals or overlays account for existing overflow locking.
- All section IDs (`#home`, `#about`, `#practices`, `#retreats`, `#contact`) are anchor targets and active-link observers. Keep IDs stable.


/**
 * Grounded Brotherhood - Main JavaScript
 * Smooth scroll, nav effects, mobile menu, scroll reveals
 */

(function () {
  'use strict';

  /* ----------------------------------------------------------------
     DOM References
     ---------------------------------------------------------------- */
  const navbar = document.getElementById('navbar');
  const menuBtn = document.getElementById('menuBtn');
  const navLinks = document.querySelectorAll('#navLinks a, .footer-nav a');
  const revealElements = document.querySelectorAll(
    '.about-card, .practice-item, .retreat-card, .section-head, .contact-info, .contact-image'
  );

  /* ----------------------------------------------------------------
     Sticky Navigation + Scroll Class
     ---------------------------------------------------------------- */
  let lastScrollY = 0;

  function updateNav() {
    const scrollY = window.scrollY || window.pageYOffset;

    if (scrollY > 60) {
      navbar.classList.add('scrolled');
    } else {
      navbar.classList.remove('scrolled');
    }

    lastScrollY = scrollY;
  }

  window.addEventListener('scroll', updateNav, { passive: true });
  updateNav();

  /* ----------------------------------------------------------------
     Mobile Menu Toggle
     ---------------------------------------------------------------- */
  function toggleMenu() {
    const isOpen = document.body.classList.toggle('open');
    menuBtn.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
    document.body.style.overflow = isOpen ? 'hidden' : '';
  }

  menuBtn.addEventListener('click', toggleMenu);

  // Close menu when clicking a link
  navLinks.forEach(function (link) {
    link.addEventListener('click', function () {
      if (document.body.classList.contains('open')) {
        toggleMenu();
      }
    });
  });

  /* ----------------------------------------------------------------
     Smooth Scroll for Anchor Links
     ---------------------------------------------------------------- */
  navLinks.forEach(function (link) {
    link.addEventListener('click', function (e) {
      const href = this.getAttribute('href');
      if (href && href.startsWith('#')) {
        e.preventDefault();
        const target = document.querySelector(href);
        if (target) {
          const offset = 80; // Account for fixed nav
          const top = target.getBoundingClientRect().top + window.scrollY - offset;
          window.scrollTo({
            top: top,
            behavior: 'smooth'
          });
        }
      }
    });
  });

  /* ----------------------------------------------------------------
     Active Nav Link Highlighting
     ---------------------------------------------------------------- */
  const sections = document.querySelectorAll('section[id]');

  function updateActiveLink() {
    const scrollY = window.scrollY + 120;

    sections.forEach(function (section) {
      const top = section.offsetTop;
      const height = section.offsetHeight;
      const id = section.getAttribute('id');

      if (scrollY >= top && scrollY < top + height) {
        navLinks.forEach(function (link) {
          link.classList.remove('active');
          if (link.getAttribute('href') === '#' + id) {
            link.classList.add('active');
          }
        });
      }
    });
  }

  window.addEventListener('scroll', updateActiveLink, { passive: true });
  updateActiveLink();

  /* ----------------------------------------------------------------
     Scroll Reveal (IntersectionObserver)
     ---------------------------------------------------------------- */
  if ('IntersectionObserver' in window) {
    const revealObserver = new IntersectionObserver(
      function (entries) {
        entries.forEach(function (entry) {
          if (entry.isIntersecting) {
            entry.target.classList.add('visible');
            revealObserver.unobserve(entry.target);
          }
        });
      },
      {
        root: null,
        rootMargin: '0px',
        threshold: 0.12
      }
    );

    revealElements.forEach(function (el) {
      el.classList.add('reveal');
      revealObserver.observe(el);
    });
  } else {
    // Fallback: show everything if IntersectionObserver not supported
    revealElements.forEach(function (el) {
      el.classList.add('reveal', 'visible');
    });
  }

  /* ----------------------------------------------------------------
     Hero Parallax (subtle, on desktop only)
     ---------------------------------------------------------------- */
  const heroImg = document.querySelector('.hero-img img');

  function handleParallax() {
    if (window.innerWidth < 768) return;

    const scrollY = window.scrollY;
    const heroSection = document.querySelector('.hero');
    const heroBottom = heroSection.offsetTop + heroSection.offsetHeight;

    if (scrollY < heroBottom) {
      heroImg.style.transform = 'scale(1.06) translateY(' + (scrollY * 0.08) + 'px)';
    }
  }

  window.addEventListener('scroll', handleParallax, { passive: true });

  /* ----------------------------------------------------------------
     Initialize
     ---------------------------------------------------------------- */
  document.addEventListener('DOMContentLoaded', function () {
    updateNav();
    updateActiveLink();
  });

})();


<!DOCTYPE html>

  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Grounded Brotherhood - Men's Circles & Retreats</title>
  <meta name="description" content="Men's circles for deep connection, emotional growth, and brotherhood. Group sessions and retreats with meditation, breathwork, music, and the power of touch." />

  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,500;1,300;1,400&family=Jost:wght@200;300;400&display=swap" rel="stylesheet">

  <link rel="stylesheet" href="css/main.css" />
  <link rel="stylesheet" href="css/responsive.css" />

    Grounded Brotherhood

      Home
      The Circle
      Practices
      Retreats
      Contact

    <button class="menu" id="menuBtn" aria-label="Menu" aria-expanded="false">

    </button>

A space to come home to yourself

Where men gather to grow together

        In a world that asks men to carry everything in silence, we offer something rare:
        a circle of brothers who see you, hear you, and walk beside you.
        No masks. No performance. Just real men doing real work.

        Join a Circle
        Learn More

"The wound is the place where the light enters you."

        Rumi

      Men's Circles — Brotherhood — Breathwork — Meditation — Power of Touch — Retreats —
      Men's Circles — Brotherhood — Breathwork — Meditation — Power of Touch — Retreats —

            The Circle

Why we gather

          For too long, men have been taught that strength means isolation. We believe the opposite:
          true strength is found in vulnerability, in community, and in the courage to show up fully.

Authentic Connection

Drop the armor. In circle, there is no hierarchy, no fixing, no advice-giving. Just men holding space for one another. Sharing truth. Witnessing without judgment.

Nature as Teacher

Our circles and retreats are held in nature. The forest, the mountains, the rivers — they remind us of something ancient in ourselves. Something wild and whole.

Inner Work

We use proven somatic and meditational techniques to help you drop from the mind into the body. To feel what has been unfelt. To heal what has been unhealed.

          Our Practices

The tools we work with

          Each practice is chosen for its ability to take you deeper — beyond words, beyond story, into the body and the present moment.

            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">
              <circle cx="12" cy="12" r="10"/>
              <path d="M12 16v-4M12 8h.01"/>
            </svg>

Meditation

Guided and silent practices to quiet the mind, open the heart, and meet yourself as you are. From mindfulness to deep trance states.

            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">
              <path d="M9 18V5l12-2v13"/>
              <circle cx="6" cy="18" r="3"/>
              <circle cx="18" cy="16" r="3"/>
            </svg>

Music & Sound

Live instruments, sound journeys, and chanting. Music is medicine. It bypasses the intellect and speaks directly to the soul.

            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">
              <path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/>
              <polyline points="14 2 14 8 20 8"/>
            </svg>

Power of Touch

Conscious, consensual touch practices. Holding, pressing, receiving. In a world starved of healthy masculine touch, we reclaim it as healing.

            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">
              <path d="M2 12h20M2 12c0 5.523 4.477 10 10 10s10-4.477 10-10"/>
              <path d="M12 2a10 10 0 0 0-10 10"/>
            </svg>

Breathwork

Transformational breathwork sessions to release stored emotion, energize the body, and access altered states of consciousness.

            Offerings

Upcoming circles & retreats

          Whether you are taking your first step or returning for deeper work, there is a container here for you.

Weekend Retreat

Deep Forest Weekend

              3 Days / 2 Nights
              Up to 12 Men

An immersive weekend in the forest with daily circles, breathwork, nature walks, and fireside gatherings. Step away from the noise and return to yourself.

            Inquire Now

Weekly Circle

The Weekly Brotherhood

              2.5 Hours
              Drop-In Welcome

Our ongoing weekly gathering. Meditation, sharing circle, and somatic practices. A consistent anchor in your week.

            Inquire Now

5-Day Intensive

The Men's Intensive

              5 Days / 4 Nights
              By Application

A profound container for men ready to do deep work. Breathwork, touch, shadow work, and 1-on-1 facilitation. Limited to 8 men.

            Inquire Now

            Reach Out

Take the first step

            Curious? Ready? Somewhere in between? We would love to hear from you.
            Every great journey begins with a simple hello.

                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">
                  <rect x="2" y="4" width="20" height="16" rx="2"/>
                  <path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/>
                </svg>

              hello@groundedbrotherhood.com

        Grounded Brotherhood

          Home
          The Circle
          Practices
          Retreats
          Contact

© 2026 Grounded Brotherhood. All rights reserved.


  letter-spacing: 0.1em;
}

/* ------------------------------------------------------------------
   Scroll Reveal
   ------------------------------------------------------------------ */
.reveal {
  opacity: 0;
  transform: translateY(40px);
  transition: opacity 0.8s ease, transform 0.8s ease;
}

.reveal.visible {
  opacity: 1;
  transform: translateY(0);
}


# Orchestration — `index.html` Error Check

## Original task
> "check this html page for errors"

## Decomposition
This is a single, well-scoped static-review task on one HTML file. It does not require multi-agent fanout; the orchestrator performed the read-only audit directly. No workers were dispatched.

| Sub-task | Agent | Output | Status |
|---|---|---|---|
| Static audit of `index.html` (structure, validity, a11y, asset wiring) | orchestrator (read-only) | this `orchestration.md` | done |

Files inspected:
- `index.html` (full read)
- `js/main.js` (full read, to verify class/ID wiring)
- `css/main.css` (grep for selectors referenced by HTML/JS)

## Per-section findings

### 1. Document shell
- `<!DOCTYPE html>`, `lang="en"`, `<meta charset>`, `<meta viewport>`, `<title>`, `<meta description>` — all present and ordered correctly.
- Google Fonts preconnect + stylesheet URL well-formed (no typos in query string).
- CSS link order matches `AGENTS.md` convention: `css/main.css` then `css/responsive.css`.

### 2. Navigation (`<nav id="navbar">`)
- Brand link + 5 anchor links with stable IDs (`#home`, `#about`, `#practices`, `#retreats`, `#contact`) — all IDs exist on `<section>` elements further down.
- Mobile menu button: `aria-label="Menu"`, `aria-expanded="false"` initial state, two `<span>`s that CSS animates to an "X" via `body.open` class. JS in `main.js` correctly toggles the attribute.
- Minor (non-blocking): button has no `aria-controls="navLinks"`. Adding it would tighten a11y but is not an error.

### 3. Hero (`<section class="hero" id="home">`)
- `.kicker`, `h1` with `<em>`, `.lead`, `.hero-actions` with two `.btn` links, `.hero-img` with image and `.floating-card` quote — all classes exist in `main.css` (lines 120, 260, 324–388).
- `alt` text present and meaningful.
- JS parallax target `.hero-img img` exists.

### 4. Marquee
- Standard duplicate-content pattern inside one inner `<div>`. `—` entities are valid HTML5.

### 5. About / Practices / Retreats / Contact
- All cards use the class names declared in `main.css` (`about-card`, `practice-item`, `retreat-card`, `retreat-tag`, `retreat-meta`, `contact-info`, `contact-image`, etc.).
- All four `.practice-item`s and all three `.retreat-card`s have well-formed inline SVGs (`viewBox` + `stroke="currentColor"`, paths and shapes close cleanly).
- Retreat card "Deep Forest Weekend" carries an extra `featured` class — fine, it's a hook for styling.

### 6. Footer
- `© 2026` — valid named entity.
- Footer-nav anchors re-use the same section IDs.

### 7. Script wiring
- `js/main.js` is an IIFE, `'use strict'`, loaded at end of `<body>` so DOM is available.
- All element IDs/classes it queries (`#navbar`, `#menuBtn`, `#navLinks`, `.hero-img img`, `section[id]`, `.about-card`, etc.) exist in the HTML.

### 8. Cross-cutting checks
- No duplicate `id` attributes.
- No `<div>` / `<ul>` / `<table>` / `<p>` containing block-level children inside inline contexts.
- All `<img>`s have `alt` (some are decorative next to text; still appropriate).
- Anchor `href="#x"` all resolve to a real element `id="x"`.

## Result

**No blocking errors found.** The page is structurally valid HTML5, IDs/anchors are consistent, CSS class names referenced in the markup all exist in the stylesheet, and the JS finds every element it queries. The page should render and behave as designed.

## Residual risks (minor, non-blocking)

1. **Decorative SVGs lack `aria-hidden="true"`** in `.practice-icon` and the email contact icon. Screen readers will announce them as graphics. Add `aria-hidden="true"` (and `focusable="false"` for IE/Edge legacy) to silence — improvement, not a bug.
2. **Mobile menu button has no `aria-controls="navLinks"`** — a11y improvement, not required.
3. **External image hosts (Unsplash CDN)** are referenced directly; if Unsplash changes URL structure, all 9 images break. Not an HTML error, but a runtime dependency worth noting.
4. **Google Fonts** is a third-party dependency with no `font-display` override in the URL — relies on default `swap`. Acceptable.
5. **No `lang` on the Rumi quote** inside `.floating-card` — not needed (it's English).
6. **Parallax `transform` write on `.hero-img img`** every scroll frame is OK because the listener is passive, but it does force style recalc; not an error, just a perf footnote.

## Commands run
None (static read-only review).

```acceptance-report
{
  "criteriaSatisfied": [
    {
      "id": "criterion-1",
      "status": "satisfied",
      "evidence": "Walked the full index.html (one file, ~190 lines), cross-checked every class/ID against css/main.css and every queried selector against js/main.js; no broken anchors, no duplicate IDs, no unclosed tags, no missing referenced assets. Residual risks are minor a11y improvements (aria-hidden on decorative SVGs, aria-controls on menu button) and external CDN dependencies, not HTML errors."
    }
  ],
  "changedFiles": [
    "orchestration.md"
  ],
  "testsAddedOrUpdated": [],
  "commandsRun": [],
  "validationOutput": [
    "Static read-only review: structure, IDs, anchors, alt text, script wiring, and CSS-class alignment all consistent. No blocking issues found."
  ],
  "residualRisks": [
    "Decorative SVGs in .practice-icon and .contact-icon lack aria-hidden=\"true\"; screen readers will announce them as graphics.",
    "Mobile menu button (id=menuBtn) lacks aria-controls=\"navLinks\"; a11y improvement only.",
    "All 9 images are loaded from images.unsplash.com; if Unsplash changes URL pattern, images break (runtime/CDN risk, not HTML).",
    "Google Fonts loaded without explicit font-display override; falls back to browser default swap."
  ],
  "noStagedFiles": true,
  "diffSummary": "No source files modified. Created orchestration.md as the audit report.",
  "reviewFindings": [
    "no blockers",
    "minor: add aria-hidden=\"true\" focusable=\"false\" to decorative SVGs",
    "minor: add aria-controls=\"navLinks\" to the mobile menu button"
  ],
  "manualNotes": "Task was a single-file static review, so no subagent fanout was warranted. I performed the read-only audit directly. The page is clean; the only callouts are a11y polish items, not errors. No code changes were made to index.html, css/*, or js/*."
}
```


/* ============================================================
   GROUNDED BROTHERHOOD - Responsive Stylesheet
   Mobile & tablet breakpoints
   ============================================================ */

/* ------------------------------------------------------------------
   Tablet (991px and below)
   ------------------------------------------------------------------ */
@media (max-width: 991px) {
  .hero {
    grid-template-columns: 1fr;
    grid-template-rows: auto auto;
  }

  .hero-copy {

[228 more lines in file. Use offset=16 to continue.]


    flex-direction: column;
    gap: 24px;
    text-align: center;
  }
}

/* ------------------------------------------------------------------
   Mobile (767px and below)
   ------------------------------------------------------------------ */
@media (max-width: 767px) {
  .nav {
    padding: 18px 24px;
  }

  .links {

[154 more lines in file. Use offset=90 to continue.]


ENOENT: no such file or directory, access '/mnt/flash1/code/webpage/.gitignore'


# Progress — Scout for webpage test mapping

**Task:** Map the relevant code for test. List files to touch, capture conventions, identify risks.
**Target project:** `/mnt/flash1/code/webpage` (Grounded Brotherhood static marketing site)
**Status:** in progress → finalizing scout report

## Steps performed
1. Inspected workspace — `webpage/` is the only candidate project (no other source repos active).
2. Read `webpage/AGENTS.md` — captured architecture + conventions.
3. Read `webpage/index.html` (292 lines) — full markup.
4. Read `webpage/js/main.js` (165 lines) — full behavior.
5. Grepped `webpage/css/main.css` and `webpage/css/responsive.css` for selectors, breakpoints, and reveal-state classes.

## Files to touch for testing (recommended scope)
- `index.html` — DOM / a11y / anchor wiring assertions
- `js/main.js` — behavior: nav scroll, mobile menu, smooth scroll, active link, IntersectionObserver, parallax
- `css/main.css` + `css/responsive.css` — breakpoint coverage, reveal/visible state, focus styles
- `orchestration.md` — append a `## Test plan` section (in-tree, matches existing audit-file convention)

## Conventions captured
- No build / no framework / no deps (AGENTS.md).
- CSS split is locked: `main.css` tokens + base, `responsive.css` breakpoints only.
- Reveal pattern: JS adds `.reveal`; CSS toggles to `.visible`; selector list lives in `main.js`.
- Nav offset constant `80` in JS — duplicated value with `--offset` token.
- Mobile menu uses `body.open` class + `aria-expanded` toggle.
- Smooth scroll uses `getBoundingClientRect` + `window.scrollTo({behavior: 'smooth'})`.

## Risks identified
- No tests exist; nothing to extend — must bootstrap test harness.
- No `package.json` / `node_modules` — need to choose lightweight harness (vitest + jsdom, or plain node assertions + jsdom).
- Three external CDN dependencies (Google Fonts + 9 Unsplash images) — no network in jsdom by default; either stub or use `URL` validation only.
- Parallax listener writes `style.transform` on every scroll event — perf-sensitive path; needs throttling assertion or guarded test.
- Reveal observer may not fire under jsdom (no layout) — needs `IntersectionObserver` stub.
- Responsive breakpoints at 479 / 767 / 991 / 1400 — visual regressions out of scope for unit tests; would need Playwright.

## Next
Write the full scout report to `context.md`.


No files found matching pattern


artifacts/