diff --git a/.claude/rules/pages.md b/.claude/rules/pages.md
new file mode 100644
index 0000000..7448711
--- /dev/null
+++ b/.claude/rules/pages.md
@@ -0,0 +1,13 @@
+---
+paths:
+ - "**/*.html"
+ - "sitemap.xml"
+ - "tools/deploy.sh"
+---
+
+# A new page, or a new top-level file or folder of the site
+
+- Copy the `
` of an existing page, with its Content-Security-Policy line.
+- Add the URL to `sitemap.xml` (`tools/check.py` fails without it).
+- Add a new top-level file or folder to the `SITE` list in `tools/deploy.sh`, or it is not published.
+- Update "Contents" in the title block of `index.html` and the sheet numbers of its sections.
diff --git a/.claude/settings.json b/.claude/settings.json
new file mode 100644
index 0000000..fd1d312
--- /dev/null
+++ b/.claude/settings.json
@@ -0,0 +1,14 @@
+{
+ "hooks": {
+ "SessionStart": [
+ {
+ "hooks": [
+ {
+ "type": "command",
+ "command": "n=$(ls \"${CLAUDE_PROJECT_DIR:-.}\"/.review/notes/*.json 2>/dev/null | wc -l); if [ \"$n\" -gt 0 ]; then echo \"$n open review notes in .review/notes/ (skill handling-review-notes)\"; fi; true"
+ }
+ ]
+ }
+ ]
+ }
+}
diff --git a/.claude/skills/adding-a-thought/SKILL.md b/.claude/skills/adding-a-thought/SKILL.md
new file mode 100644
index 0000000..4aafd42
--- /dev/null
+++ b/.claude/skills/adding-a-thought/SKILL.md
@@ -0,0 +1,74 @@
+---
+name: adding-a-thought
+description: Use when Tim gives a link (a web page, a tweet or X post, a paper, a repository, a video) or an idea and wants to think it through as a new thought on t7d.dev, when he wants to revise an existing thought, change its status, or add feedback that people gave on X or LinkedIn. Also for "new thought", "add this to my thoughts", "write a thought about".
+---
+
+# Adding a thought
+
+The thoughts in `thoughts//` are Tim's archive of ideas he works through and comes back to. Anyone can read them, but they are mostly for him: each keeps where it started, what it rests on, its status and how it changed. The reference page is `thoughts/humans-md/index.html`.
+
+## 1. Gather the starting point
+
+- Read what Tim gave you. For a web page use WebFetch, or the `web-fetch` agent. X often blocks fetches: try the post's URL, then a reader or embed URL (`https://publish.twitter.com/oembed?url=`), and if that fails, ask Tim to paste the text. Never guess what a post says.
+- Note the author, the date and one sentence of what it claims. These go into "Starting points".
+
+## 2. Think it through with Tim before you write
+
+Ask 2 to 4 questions per round with AskUserQuestion, and wait for the answers. Tim wants to iterate on the idea, not get a finished report. Good questions:
+
+- What caught his interest: the claim, the method, or a use for his own work?
+- His own practice today. The thought must be honest about it ("Mostly I go by feel" is fine, inventing a habit is not).
+- Where he disagrees or is unsure.
+- The form of the figures: what should move, what should be switchable.
+
+After a research round, share what you found with one or two more questions before you write.
+
+## 3. Research
+
+- Small: a few WebSearch and WebFetch calls yourself.
+- Big, or when Tim asks for it: the `deep-research` workflow, with the refined question as args. While it runs, ask Tim the questions from step 2.
+- Check every number and quote against a primary source. Mark what you could only see in a search snippet, and leave it out or paraphrase it. Name who coined a term only with a dated source (for example the first commit).
+
+## 4. Write
+
+The voice is DESIGN.md "Wording": first person, short sentences, plain words, no slogans. A thought is not a report:
+
+- Open with the question or the itch, in Tim's words.
+- One section per step of the thinking. Headings name their content.
+- Figures carry the ideas that are easier to see than to read. Use `figure.anim` (DESIGN.md "Animated diagram"): a drawing that plays once on screen, a two-state switch, parts to tap, a running token. Draw a narrow version (`.dg-n`) for phones. Use only the `.dg` classes, no colours, no `style` attributes.
+- End with what Tim takes from it: a rule, a draft, a file, or open questions.
+
+## 5. The page
+
+Copy `thoughts/humans-md/index.html` and keep its structure:
+
+1. ``: title, description, canonical, `og:*`, `article:published_time` (the start date). The feeds and `llms.txt` are made from it at deploy time.
+2. `.post-head`: crumb to `/thoughts/`, `h1`, `.sub`, a label with topics, and `.tmeta`: Status, Started, Last revised.
+3. `.toc` with every `h2`.
+4. The text, then in this order: Open questions (if any), Starting points, Sources (`ol.refs`, `id="s1"`...), Revisions (`ul.dates`), Feedback.
+
+Status is one of `Draft` (still forming), `Settled` (Tim stands by it) and `Changed my mind`. Set it only as Tim says; a new thought is `Draft`.
+
+Then:
+
+- Add it at the top of the list in `thoughts/index.html` (date, status, title, one sentence).
+- Add it as the first `.proj` box in the Thoughts section of `index.html`, with an inline SVG cover. Keep two boxes there; the list holds the rest. Update the count in "Contents" in the title block.
+- Add the URL to `sitemap.xml`.
+- A file to download (like `HUMANS.md`) goes into the thought's folder.
+
+## 6. Check, let Tim review, ship
+
+Follow CLAUDE.md: serve the site (`python3 -m http.server 8000`), look at 1440, 390 and 320 px in both themes, no sideways scroll, no console errors. Click every switch and pick, and look at the narrow drawings. Commit on the session's branch and push.
+
+Tim reviews a thought as a Claude artifact before it is published: run `python3 tools/preview-artifact.py thoughts//preview-` and publish its `index.html` with the Artifact tool, with every other printed file in `files`. After a change, build again and publish the same `file_path`, so the link stays the same. Comments on the artifact reach the session: answer them and change the page in the repository, never only in the bundle. `tools/ship.sh ` merges and checks the live site when Tim asks for it.
+
+## Revising a thought
+
+- Change the text, set "Last revised", and add one line at the top of Revisions: the date and what changed, and why ("Changed the third rule after feedback on LinkedIn").
+- Change the status only as Tim says.
+- Feedback: quote or summarise what people said, with a link to the post, and what it changed. If it changed nothing, say so.
+- Keep the old start date in `article:published_time`.
+
+## Sharing a thought
+
+To draft X and LinkedIn posts for a thought and bring the replies back, use the skill `sharing-a-thought` from `Tim-Conrad/Tim-Conrad-Skills`. It records each post and the feedback in the thought's Feedback section, with the revision rules above.
diff --git a/.claude/skills/handling-review-notes/SKILL.md b/.claude/skills/handling-review-notes/SKILL.md
new file mode 100644
index 0000000..d6f2012
--- /dev/null
+++ b/.claude/skills/handling-review-notes/SKILL.md
@@ -0,0 +1,20 @@
+---
+name: handling-review-notes
+description: Use when Tim mentions notes or feedback on the review site (the tab »Note«), when the session start says there are open review notes, or when you change tools/review/ or docker-compose.dev.yml.
+---
+
+# Handling review notes
+
+Tim writes notes on the review site: the tab »Note« at the right edge. The review site is this clone, served by `tools/review/devserver.py` in the container `t7d-review` (`docker-compose.dev.yml`, host name in `.env` as `REVIEW_HOST`). It shows the checked-out branch at once and loads the page again when a file changes. A note is a JSON file in `.review/notes/`.
+
+1. The SessionStart hook (`.claude/settings.json`) prints the number of open notes in `.review/notes/`. Tell Tim how many are open, and list them with `ls .review/notes/*.json`.
+2. Read the note: `note`, `path`, `viewport`, `theme`, `scroll`, `target` (`selector`, `text`, `rect`), `selection`, `branch`, `commit`.
+3. Run `uv run --no-project --with playwright python3 tools/review/show.py ` (more ids in one call). Look at `.review/notes/.png`. The red frame is the part that Tim pointed at.
+4. Do the work with the rules of this file: a branch, `DESIGN.md`, the look at three widths.
+5. After the fix, run `show.py --suffix after` and compare the two pictures.
+6. Add an `"answer"` field to the note: what changed, and the commit. Move the note to `.review/notes/done/`. Tim sees the answer under »My notes« in the tab.
+
+- Keep notes and their pictures out of git. `.review/` and `.env` are ignored.
+- A change to `devserver.py` needs `docker restart t7d-review`. A change to `review.js` or `review.css` needs only a page load.
+- The server adds only `connect-src 'self'` to the CSP of a page. Keep it that way, so that the review site keeps the rules of the real site.
+- Tests: `python3 -m unittest tools/review/test_devserver.py`.
diff --git a/.claude/skills/updating-product-reels/SKILL.md b/.claude/skills/updating-product-reels/SKILL.md
index c8f3ed1..b1bf8ba 100644
--- a/.claude/skills/updating-product-reels/SKILL.md
+++ b/.claude/skills/updating-product-reels/SKILL.md
@@ -66,7 +66,7 @@ Before a run: start the dev server in `../wortzauber` with `python3 devserver.py
`ffmpeg -i /kleinbahn-reel.mp4 -vf "fps=16/39,scale=320:180,tile=4x4" -frames:v 1 sheet.jpg` (16 pictures over a 39 s reel).
Check that each caption sits on its own shot, that no caption covers a speech bubble or a panel, and that the end card is readable.
4. `build.py publish`. The stills come from the sources, not from the reel, so they carry no caption. Which frames they are is at the top of `build.py` (`KB_PICS`, `WZ_PICS`, `WZ_PHONE`). If a shot or a take changed, check that the alt texts in `projects//index.html` still describe the pictures.
-5. Serve the site (`python3 -m http.server 8000` in this repo) and look at `/projects/kleinbahn/`, `/projects/wortzauber/` and `/#projects` at 1440 and 390 px wide. The page must not scroll sideways.
+5. Serve the site (`python3 -m http.server 8000` in this repo) and look at `/projects/kleinbahn/`, `/projects/wortzauber/` and `/#projects` at 1440, 390 and 320 px wide, in the light and the dark theme (CLAUDE.md). The page must not scroll sideways.
6. Commit the files in `assets/projects/` with the change.
## Common mistakes
diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml
new file mode 100644
index 0000000..71ee819
--- /dev/null
+++ b/.github/workflows/check.yml
@@ -0,0 +1,15 @@
+name: Check the pages
+
+on:
+ pull_request:
+
+permissions:
+ contents: read
+
+jobs:
+ check:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: CSP rules and sitemap
+ run: python3 tools/check.py
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index a5072a6..092aeb0 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -17,6 +17,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
+ - name: Check the pages
+ run: python3 tools/check.py
- name: Publish the site files to gh-pages
env:
GIT_AUTHOR_NAME: github-actions[bot]
diff --git a/.gitignore b/.gitignore
index 4b7681e..5a4d505 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,8 @@
__pycache__/
# made by tools/gen-feeds.py at deploy time
/llms.txt
+/thoughts/feed.xml
/blog/feed.xml
.review/
.env
+.preview/
diff --git a/404.html b/404.html
index d913cbf..3dd7bd9 100644
--- a/404.html
+++ b/404.html
@@ -17,7 +17,7 @@
Tim Conrad