diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..5bdb0ec
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,104 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## What this is
+
+The OpenAEC Foundation website (https://open-aec.com) — ~47 pages of hand-written static HTML/CSS/JS. **No build step, no `package.json`, no framework, no bundler.** Node is used only for a set of generator scripts under `scripts/`. Deploy is rsync to `/var/www/open-aec.com` on push to `main` (`.github/workflows/deploy.yml`). Everything generated is committed — `.gitignore` contains only `.worktrees/`.
+
+`README.md` is badly stale (says 16 repos, NL/EN only, GitHub Pages). Do not trust it as a spec.
+
+## Commands
+
+Serve locally (the `.claude/launch.json` entry is named `website`, `npx serve` on port 4500):
+
+```bash
+npx serve
+```
+
+Refresh all GitHub-derived data (stats, news, downloads, homepage chips, api, md mirrors) and commit:
+
+```bash
+node scripts/update-website.js
+```
+
+Add `--no-commit` to skip the commit. Requires `GITHUB_TOKEN` in the environment (60 vs 5000 req/h). The 7 steps and their read/write dependencies are documented in `.claude/skills/update-website/SKILL.md` — run them individually only in that order.
+
+### Tests
+
+There is no test runner. The two checker scripts are the only tests and are **not wired into CI** — run them by hand after touching the homepage, a product page, or the generators:
+
+```bash
+node scripts/check-open-pile-plan-studio.js && node scripts/check-homepage-catalog-product-pages.js
+```
+
+They use `node:assert/strict` and fail with a Dutch message. `check-open-pile-plan-studio.js` doubles as the executable checklist for adding a new tool (see below). `check-homepage-catalog-product-pages.js` pins several literal CSS colour values in `bim-validator/index.html` and the exact screenshot order in Open Speech Studio — those assertions are deliberate, not accidental.
+
+## Architecture
+
+### Two name spaces per tool
+
+Every tool has a **page slug** and a **GitHub repo name**, and they usually differ (`open-pile-plan-studio` vs `pile-plan-studio`). Slug is used for the directory, `meta[name=i18n-page]`, `shared/translations/*`, `md/*.md`, and the `id` in `api/tools.json`. Repo name is used for `data-repo`, `data-release-notes`, `data/release-notes/*.json`, and the repo lists inside the generator scripts. `build-homepage-stats.js` lowercases repo names when matching; everything else preserves casing.
+
+### i18n — two separate engines
+
+Four languages: **nl (default, the literal HTML text), en, fr, tr**. Preference in `localStorage` under `openaec-lang`. Language is client-side only — there are no per-language URLs and no `hreflang` anywhere.
+
+- **Subpages** use `shared/i18n.js`. A page declares ``; translations load from `/shared/translations/.json` (EN) and `.fr.json` / `.tr.json` (with EN fallback). Nodes carry `data-i18n="dotted.key"`, attributes use `data-i18n-attr`. NL originals are cached into `data-i18n-nl` on first switch. Without the meta tag only inline-attribute translation runs.
+- **The homepage does not use it.** `index.html` has a self-contained inline `const translations = { nl, en, fr, tr }` (~line 1686) plus its own `detectLang()`/`i18n` object. Adding a tool means adding `tools.items.` to **all four** objects — the pile-plan checker asserts exactly 4 occurrences of the key.
+- Nav links use inline `data-i18n-en` / `-fr` / `-tr` attributes in `shared/nav.html`, not JSON.
+
+`shared/nav.js` fetches and injects `shared/nav.html` into `#shared-nav` (auto-creating the placeholder if absent), then dispatches a `nav:loaded` CustomEvent; `shared/i18n.js` listens for it to translate the late-injected nav. There is no MutationObserver — deliberately.
+
+### Product page skeleton
+
+`/index.html`, ``, head order: charset → viewport → title → description → `meta name="i18n-page"` → favicon → `/shared/style.css` → inline `SoftwareApplication` JSON-LD → page-local `