# CLAUDE.md

> **Fluid Framework** — A modern, semantic CSS design system built on physics principles, not utility classes.

## How we build (read first)
The rules for every project are in `Agile-Media-Agency/the-fluid-stack/playbook/`
(start at `00-new-site-checklist.md`; `README.md` is the index; `design-and-ux.md`
is the page that points back here for design detail). This file holds only what is
specific to the theme and dated exceptions to the playbook. When you fix or add
something other sites could use, change the playbook in the same PR, add a row to
`playbook/FEATURES.md`, and open a `playbook-sync` issue on each sibling repo.
Project facts (domain, hosting, npm, status) live in
`neilswhite/neil-projects-registry/PROJECTS.md`, not here.

## History was rewritten on 2026-10-27 (read before any push)

Every branch and tag was rewritten to purge client student reports (Neil's
call; audit FL-01, PR #76). Any clone, worktree or branch made before
2026-10-27 08:50 UTC still carries them: **re-clone; never merge, rebase onto
or push anything from an old clone.** Old SHAs differ from the new ones (old
`main` 592f599 is new 893eb84). The repo stays private for the same reason.

**Exception, 2026-10-28: an old clone pushed the purged history back by
itself.** The MacBook Pro's `~/Projects/the-fluid-theme` predates the rewrite,
and its scheduled `gitsync.sh` (projects-tools, D-22) saved its 308 "unpushed"
old commits as branch `checkpoint/macbook-pro`, which put the reports commit
d3a678f back on a branch. The branch was deleted the same night (0 unique
files). So: a `checkpoint/<mac>` branch whose history differs from `main` is
an old clone, not work to rescue; delete it and re-clone on that Mac. gitsync
now refuses to checkpoint such a clone (projects-tools PR #4,
`purged-commits.txt`). Until every Mac has re-cloned, check
`gh api repos/Agile-Media-Agency/the-fluid-theme/branches --jq '.[].name'`
for a returning `checkpoint/*` branch.

## Exceptions (dated; read before the next build)

- **2026-10-28: the MacBook Pro stays on gitsync's hold list until it
  re-clones.** projects-tools `gitsync-hold.txt` (projects-tools #3) has
  `the-fluid-theme  macbook-pro`; the re-clone is registry
  `todo/T102-macbook-reclone-fluid-theme.md`. Any branch whose merge-base with
  `main` is `bd1cabc` ("Initial project structure", the second commit) is
  pre-rewrite: do not merge or check it out.
- **2026-10-29: a clone that only fetched a re-uploaded branch still holds
  the reports after the branch is gone.** The Mac Studio's
  `~/Projects/the-fluid-theme` fetched `checkpoint/macbook-pro`; gitsync's
  `fetch --prune` drops the ref, not the objects, so `git cat-file -t
  d3a678f` still answers `commit` until a gc. Clean it with `git update-ref
  -d refs/remotes/origin/checkpoint/<mac>`, then `git gc --prune=now`, and
  check `cat-file` fails; never `git reflog expire --all`, which also wipes
  every linked worktree's reflogs. Pending on the Mac Studio (registry
  `todo/T102-macbook-reclone-fluid-theme.md`, Mac Studio section).
- **2026-10-28: npm `engines` values must be strings, or Dependabot aborts.**
  `"engines": {"browsers": [...]}` (an array) made every Dependabot run fail with
  "engines.browsers must be a string" from Oct 8 to Oct 24, so 16 alerts got no
  fix PRs. Browser targets live in `browserslist` (informational) and, for the
  build, in the `targets` in `scripts/build-css.mjs`. Never add `engines.browsers`.
- **2026-10-29: Playwright's Firefox does not launch on the Mac Studio.**
  Firefox 157 (playwright firefox v1555, macOS 27) exits with "Could not find
  profile folder" for any profile path, sandboxed or not; WebKit and Chromium
  run. Cross-browser checks there are Chromium plus WebKit
  (`BROWSER=webkit node scripts/nav-check.mjs`); run Firefox in a cloud session
  (`/opt/pw-browsers`) or after a Playwright update, and say which ran.
- **2026-10-29: Safari picks a local font's cut by weight, not by name.**
  WebKit treats `local("Arial")` as a family and returns Arial Bold for a
  requested 530 (the inverse bar's 500 + 30), even inside a `font-weight: 100
  599` face. The fallback faces list the PostScript name first (`ArialMT`).
  Georgia has no unique regular name, so the nav brand and links show in
  Georgia Bold in Safari until Newsreader loads. Check fallback changes in
  WebKit with the woff2 requests blocked.
- **2026-10-29: lightningcss writes `border-image: none` as `border-image:`.**
  The minified shorthand is empty, so browsers drop it and the old border
  image stays (found when the forced-colours reset of the full-bleed nav did
  nothing). Reset a border image with `border-image-source: none`, and grep
  `dist/` for `:}` or `:;` after adding a `none` to a shorthand.
- **2026-10-29: main deploys once per merge, not on both the push and Release.**
  `site.yml` runs on `push: main` and again when Release finishes (so the footer
  shows the released version); deploying on both uploaded main twice a minute
  apart (14 pairs in October, e.g. runs 40812532042 and 40812587873), with Actions
  minutes near the org's monthly cap. Its `release-check` job now skips the push
  deploy whenever Release will act on the push (a changed file outside Release's
  `paths-ignore`, and no `chore: release` in the subject). **Change Release's
  `paths-ignore` or its `if:` and you must change `release-check` to match**, or
  a merge either deploys twice or not at all. The Release-triggered run deploys
  on any conclusion except `skipped`, so a failed Release still puts the merge
  live with the old version in the footer. Known gap: if Release never starts
  for a push the gate expected it to take, nothing deploys that merge; run
  `site` by hand (workflow_dispatch) or push again.

- **2026-10-30: Firefox runs in a cloud session from the Playwright Docker
  image, not the CDN.** `npx playwright install firefox` gets a 403 there
  (cdn.playwright.dev is not allowed), but mcr.microsoft.com is: the
  `playwright:v<version>-noble` amd64 image's `install firefox` layer is
  `ms-playwright/firefox-<build>` with its own NSS. Pull that layer's blob
  with curl from `/v2/playwright/blobs/<digest>` (manifest, then config
  history to find the layer), untar it, and link the folder into
  `/opt/pw-browsers`. The gates found one Firefox-only defect this way
  (gallery, `_docs/plans/phone-nav-bottom.md` §10).
- **2026-10-30: gzip sizes differ by platform.** The same `fluid.min.css`
  bytes measure 24,413 B gzip on Linux (Node 22 and 24, the CI runners) and
  24,395 B on the Mac Studio, because Node's bundled zlib differs. Leave
  about 25 B under the core budget when you measure on the Mac, or
  `prepublishOnly` (`npm run build` in Release) fails on CI.

- **2026-10-30: `[skip ci]` anywhere in a merge commit skips Release.**
  GitHub skips every push workflow when the message holds it, title or
  body, and a squash merge copies the branch commits' messages in. So #102
  (Base v2) and #104 reached `main` unreleased, and Release has no manual
  trigger. While Actions minutes are tight, put `[skip ci]` in branch
  commits only if you will clear it from the merge box; the PR that must
  release (`major` label, a file outside Release's `paths-ignore`) carries
  none, in its title or its commits. Because #102 never ran CI on `main`,
  its new `assets/js/toc-build.mjs` and font `OFL-*.txt` licences first met
  `scripts/site-guard.sh` in #105's site build, which failed on the file
  types; the guard now allows `*.mjs` and `OFL*.txt`.
- **2026-10-30: long-lived branches can reintroduce `engines.browsers`.**
  `base-v2-strong` branched before #90 dropped `engines.browsers` from
  `package.json`, so merging #102 brought the array back and silently broke
  Dependabot again until removed in the v2 finish PR. Check `package.json` for
  `engines.browsers` whenever merging a branch cut before 2026-10-29.

- **2026-11-01: thefluidtheme.com is the product site, built by Cloudflare, not Actions.**
  `site/build.mjs` (Node, no Jekyll) turns `themes/*.css` + `themes/<slug>.json` into the
  gallery, detail pages, compare, the dated New feed, zips and single-file HTML; Cloudflare
  Pages project `fluid-theme-site` (git-connected) runs `npm run
  build && node site/build.mjs` (after its own `npm clean-install`) on every push to main. `site.yml` is gone, so the
  `release-check` exception above no longer applies, and the old direct-upload project
  `thefluidtheme` is retired. The root `index.html`, `examples/`, `templates/` and
  `_config.yml` are the old Jekyll site and are no longer published. Details: `site/README.md`.
- **2026-11-01: gallery thumbnails are committed screenshots.** The Cloudflare build has no
  browser, so `site/shots.mjs` (Puppeteer, local) writes `site/shots/<slug>-{light,dark}.webp`;
  rerun it for a new or restyled theme, or the card falls back to a scaled live iframe (the
  build warns). A theme that is another theme plus an accent imports its parent
  (`@import "./gallery.css";`), never copies it. Details: `site/README.md`.

## Before Doing Anything

0. Read `_docs/STATUS.md` — **what the theme is now: one lean base + per-site child themes, the knobs, the component list, what is superseded.** It wins over every older doc, including the rest of this file.
1. Read `_docs/HANDOFF.md` — open blockers and working agreements (its history predates Pass 0).
2. Read `fluid-agent-kit/skills/` — CSS conventions, UX rules, accessibility, image optimization (`preset-composition.md` is superseded)
3. Read `_docs/UX-BEHAVIOR-SPEC.md` — 18-section behavioral specification (skim first, deep-read relevant sections)

## Quick Reference

```bash
npm run serve                  # Open examples in browser at localhost:8080
npm run build                  # Bundle + minify → dist/fluid.min.css
node scripts/visual-scan.mjs   # Layout-defect scan: showcase sites, catalogue, consumers fixture (exit 1 = flags)
node scripts/axe-check.mjs     # axe WCAG 2.1 A/AA, light/dark/OS-dark, 1280/390 (exit 1 = violations)
```

## What This Is

A CSS framework that proves modern CSS can replace the utility-class approach of Tailwind/Bootstrap:

- **Semantic HTML** over `<div class="flex flex-col p-4">` soup
- **CSS Custom Properties** for runtime theming (no rebuild needed): about 20 knobs on `:root` (`_docs/STATUS.md`)
- **One base, a child theme per site**: presets and palettes were removed in Pass 0 (2026-10-23)
- **Dark mode is `light-dark()`**: one declaration per colour role, the OS decides, `<html data-theme>` overrides
- **No build step** — link one CSS file and go

## Core Principles

1. **Semantic HTML** — Use meaningful elements, not div soup
2. **CSS Custom Properties** — All values via tokens (`var(--space-5)`)
3. **Logical Properties** — `margin-block`, `padding-inline` (not `-top`, `-left`)
4. **No Build Step** — Works directly in browsers
5. **Progressive Enhancement** — Works without JavaScript
6. **Knobs, not variations** — a site changes the knobs; components read them. No attribute-keyed skins
7. **Accessibility is non-negotiable** — Connected to disabled and autism communities
8. **Upstream-first** — see below; this one governs how every other fix gets made

## Upstream-First (the rule that governs fixes)

The sites in `showcase/` are real sites **and** the framework's test beds. Building
them is how framework bugs surface.

> **Would any other Fluid site hit this? Then it is a theme fix, not a site fix.**
> Decision test: *delete the site tomorrow — does the lesson survive?* If yes, it
> belongs in `src/`.

Spacing bugs, missing defaults, broken interactions, a11y gaps, overflow cases, and
genuinely reusable utilities all go into `src/` (or `assets/js/`) — never patched
into a single site and left there. Content, brand voice, and page-specific
composition stay in the site. A site-level patch is acceptable only as a
documented stopgap with the framework work filed.

Full rule, decision table, and the running list of what these sites have already
sent upstream: **`_docs/SHOWCASE-SITES.md`**.

After any theme change, rebuild and re-vendor into the sites:

```bash
npm run build && for s in urbictionary printsofthemasters shirtsandsquirrels; do
  cp dist/fluid.min.css showcase/$s/assets/; done
```
- **2026-10-29: never `curl … | grep -q` in `site.yml`.** Its steps run with
  `shell: bash`, which is `-e -o pipefail`: `grep -q` exits on the first match,
  curl gets "(23) Failure writing output", and the pipeline fails although the
  page is right. The first `/status/` smoke test failed this way (run
  40817625356) while https://thefluidtheme.com/status/ showed the commit. Read
  the page into a variable, then match it.

## Verification

Screenshots are for judging design; **assertions are for catching defects.** A
purely visual review twice missed a button touching the section below it, in
frames where it was plainly visible. Before shipping site changes:

```bash
npm run build
node scripts/visual-scan.mjs                                            # 0 flags
node scripts/visual-scan.mjs scripts/fixtures/visual-scan-selftest.html # must exit 1
node scripts/reveal-check.mjs                                           # 0 failures
node scripts/form-check.mjs                                             # 0 failures
node scripts/axe-check.mjs                                              # 0 violations
node scripts/focus-check.mjs                                            # 0 failures (focus rings 3:1)
npm run test:zero                                                       # Fluid Zero fixture (3 gates)
node scripts/toc-check.mjs                                              # TOC module: 0 failures
npm run test:places                                                     # outline map pins drawn, numbered, in the frame (after sync.sh)
```

The second command proves the detector still detects. A scanner that cannot fail
is worse than no scanner.

## Layer Order

```css
@layer reset, tokens, base, layouts, components, themes, utilities;
```

## Key Files

| File | Purpose |
|------|---------|
| `src/fluid.css` | Main entry point (imports everything) |
| `_docs/STATUS.md` | **What the theme is now, the knobs, what is superseded** |
| `src/core/tokens.css` | The knobs and design tokens (one `:root` block) |
| `src/components/` | Component CSS files (plus opt-in `toc.css`, `nav-modes.css`) |
| `src/sections/` | 6 section CSS files |
| `themes/` | Official child themes (`swiss.css`, `gallery.css`) and their self-hosted variable fonts (`themes/fonts/`) |
| `starters/workflows/` | Copyable automatic theme-update workflow (`update-fluid-theme.yml`) and `dependabot.yml` for sites |
| `templates/` | 19 HTML templates |
| `examples/` | Live component demos |
| `showcase/` | Real example sites + framework test beds (see SHOWCASE-SITES.md) |
| `scripts/visual-scan.mjs` | Mechanical layout-defect scanner (ship gate) |
| `scripts/axe-check.mjs` | axe gate over `scripts/fixtures/catalog.html`, showcase, gallery, templates |
| `_docs/HANDOFF.md` | Current state, blockers, working agreements |
| `_docs/UX-BEHAVIOR-SPEC.md` | UX behavioral rules (18 sections) |
| `_docs/images-guide.md` | Image optimization best practices |
| `fluid-agent-kit/` | AI agent skills and conventions |

## The Fluid Agent Kit

This project includes the **Fluid Agent Kit** (`fluid-agent-kit/`) with skill files for AI coding assistants. See `fluid-agent-kit/README.md` for details.

**Available skills:**
- `fluid-agent-kit/skills/css-conventions.md` — Layer order, tokens, logical properties
- `fluid-agent-kit/skills/ux-rules.md` — 12 behavioral rules for interactive components
- `fluid-agent-kit/skills/accessibility.md` — HTML, keyboard, motion, color standards
- `fluid-agent-kit/skills/image-optimization.md` — Responsive images, lazy loading, focal points
- `fluid-agent-kit/skills/preset-composition.md` — superseded: presets were removed in Pass 0

## UX Behavior Spec

Full spec at `_docs/UX-BEHAVIOR-SPEC.md`. Quick reference at `fluid-agent-kit/skills/ux-rules.md`.

**The non-negotiables:**
- Every destructive action requires a confirmation modal with specific title and consequence body
- Every user action requires visible feedback (toast, alert, or state change)
- Every button requires a specific label ("Save Changes" not "Submit")
- Every pattern must be consistent across all screens (search and update all instances)
- Every interactive element must be keyboard accessible with visible focus
- Every animation must respect `prefers-reduced-motion`

## Image Optimization

Full guide at `_docs/images-guide.md`. Quick reference at `fluid-agent-kit/skills/image-optimization.md`.

**The non-negotiables:**
- Every `<img>` must have `width` and `height` attributes (prevents CLS)
- Every below-fold image must have `loading="lazy"`
- Every image must have meaningful `alt` text
- Use `data-focus` for focal point control on hero backgrounds and card media
- Use `data-aspect` for consistent aspect ratios on card media
- Serve modern formats (WebP/AVIF) with `<picture>` fallbacks

## Documentation

| Document | What It Covers |
|----------|----------------|
| [STATUS.md](_docs/STATUS.md) | **What the theme is now — read first** |
| [defaults.md](_docs/defaults.md) | **Every default: the rule, why, how a site changes it** (nav colours, alignment, size limit) |
| [migrations/from-1x.md](_docs/migrations/from-1x.md) | **1.x → 2.0 migration paths** (keeping the 1.x look vs adopting Base v2, Stack `brand.ts` knobs) |
| [HANDOFF.md](_docs/HANDOFF.md) | Open blockers, working agreements, history |
| [SHOWCASE-SITES.md](_docs/SHOWCASE-SITES.md) | The showcase sites, upstream-first rule, ship gate, deploy runbook |
| [archive/](_docs/archive/README.md) | Superseded docs kept as history (the old master guide `DEVELOPMENT.md`, `PROJECT_STATUS.md`, the readiness roadmap) |
| [UX-BEHAVIOR-SPEC.md](_docs/UX-BEHAVIOR-SPEC.md) | Behavioral rules — 18 sections |
| [images-guide.md](_docs/images-guide.md) | Image optimization best practices |
| [architecture.md](_docs/architecture.md) | Technical decisions (rewritten in Pass 0) |
| [reference/components.md](_docs/reference/components.md) | Component API |
| [reference/tokens.md](_docs/reference/tokens.md) | Superseded token reference (the knobs: STATUS.md) |
| [theming-guide.md](_docs/theming-guide.md) | Superseded: presets and palettes, removed in Pass 0 |

## Version

**Current: the latest git tag** (`git describe --tags --abbrev=0`; also `package.json`). The release workflow (`.github/workflows/release.yml`) tags and changelogs every merge to `main` as a patch unless the PR is labelled `minor` or `major` (root `CONTRIBUTING.md`, "PR Labels"), so a docs-only merge still releases a patch (0.9.25 on 2026-10-18 was one). The version is bumped **at release, not per session**; do not write the number here. The old "bump in five places every session" rule is retired (decision D6, `the-fluid-stack/playbook/versioning.md`, 2026-10-18). npm serves the released version (2.0.0 since 2026-10-30); the workflow publishes with npm trusted publishing, no token (D-8, 2026-10-29; `_docs/HANDOFF.md` blocker 1). If a publish fails after the tag is pushed, fix the npm setting and run Release by hand (workflow_dispatch); it publishes without bumping.
2026-11-01 · Mac Studio · desktop (agent) · SEO: applied new page titles and meta descriptions (PR meta-titles-2026-11). Before that: 