# Baseline 1.13.0: the shared site template

This package is the shared template of Jeff Posluns's 7 project sites (secureconfig.ai, opfiles.ai, auditthis.ai, cleanlanguage.ai, aiqt.ai, grclibrary.ai and, from 1.11.0, vendorreplies.ai) and, from 1.12.0, of NodeKeeper (nodekeeper.ai), a tool outside the rainbow and the family footer: 1 stylesheet, 1 palette file, 4 fonts, 6 scripts, the logos, marks and touch icons, 16 reference pages that fix the markup, a palette contrast gate, an overflow check, a link-preview card builder with its own fonts, a template for a site's own palette file, and the 2 documents that govern the sites. A site that copies the runtime files, sets its site class and generates its pages with the reference markup looks and behaves as part of the family and differs only in its colours, logo and content. The normative description is `SPECIFICATION.md` (1.13.0); `CONTENT-GUIDELINES.md` (1.5) governs the wording, and the Clean Language standard (https://cleanlanguage.ai) governs the prose. `CHANGELOG.md` lists what changed in each version and why.

A published version never changes. Any change is a new version at `https://posluns.ca/baseline/<version>/`, with its archive `https://posluns.ca/baseline/baseline-<version>.tar.gz` and the archives' digests in `https://posluns.ca/baseline/SHA256SUMS`.

## What is in the package

| Path | Content | Copied by a site |
|---|---|---|
| `baseline.css` | The whole shared stylesheet: reset, typography, layout, every component, the footer's container query stages and their fallback. Reads only the tokens in `palettes.css` and the family and layout tokens it declares at the top. | yes |
| `palettes.css` | The site palettes, 8 from 1.12.0 (from 1.11.0 vendorreplies.ai's, from 1.12.0 NodeKeeper's) (light, dark, and the `[data-scheme]` forced variants that the light and dark switch sets), the cyclic rainbow, the footer's neutral grey and (from 1.11.0) the silver band of vendorreplies.ai's footer card. From 1.6.0 each site's dark mode is a near-black with a tint of its own hue, and dark-mode text is neutral. From 1.7.0 each palette also carries the matrix tokens: row tints, a 5-step heat ramp and 6 column-group accents. | yes |
| `gen_palettes.py`, `contrast-report.txt` | The palette generator and its report; the generator exits non-zero if any text pair misses 4.5:1 (UI pairs 3:1), or (from 1.12.0) if 2 sites' `--brand` colours lie closer than 5.5 (OKLab distance x 100). Edit the script, never the CSS. From 1.12.0 every site's design values are 1 row of its table `FAMILY`, and `python3 gen_palettes.py --slots` lists the hues free for a new rainbow site (`SPECIFICATION.md` section 6.1, "Adding a site"). | no |
| `fonts.css`, `fonts/` | Newsreader, Public Sans and JetBrains Mono as latin subsets in WOFF2, and Dancing Script, from 1.11.5 the unmodified upstream TrueType file of version 2.001 (`fonts/dancing-script-wght-normal.ttf`, 133,636 bytes, in place of the 42,708-byte latin-subset WOFF2 of earlier versions; `NOTICE.md`, item 1), all variable weight, each with its SIL OFL 1.1 licence file. The font paths are relative to `fonts.css`. | yes, with the licence files |
| `copy.js`, `select.js`, `search.js`, `rail.js`, `matrix.js` | Copy to clipboard for prompt and command blocks; the install selector; the client-side list filter, with facets and a results-only mode from 1.5.0, and, from 1.7.0, opt-in chips that show when a choice would give no results; the rail mark that follows the reader (from 1.3.0, on every page whose rail lists in-page sections); the column presets of a matrix table (from 1.5.0), with column toggles and a column highlight (from 1.7.0). Each is opt-in per page and enhances markup that works without it. Load with `defer`. | yes |
| `scheme.js` | The light and dark switch in the top bar (from 1.6.0), on every page. Load it in `<head>`, before the stylesheets, without `defer` or `async`: `<script data-cfasync="false" src=".../scheme.js"></script>`. | yes |
| `site-palette.example.css` | The template of a site's own `site-palette.css` (from 1.6.0; section "Site-owned palette colours"). | no (the site writes its own file only when the owner asks) |
| `tools/check_palette.py` | The palette contrast gate (from 1.6.0), mandatory in every site's build. | yes, to `.baseline/check_palette.py`, not served |
| `tools/build_card.js`, `tools/card-fonts/`, `tools/site-card.example.toml` | The link-preview card builder (from 1.9.0), its Inter fonts (SIL OFL 1.1, with `inter-LICENSE.txt`) and the template of its data file: from `site-card.toml` and an approved illustration it writes the 1200 by 630 card in the side or (from 1.10.0) the centred layout, from 1.11.5 with an optional metal wordmark in the centred layout, a proof image, the part of the card Microsoft Teams shows, the head tags, the touch icon, from 1.12.2 the site's `favicon.ico` (and, with `--icons-only`, the icons without a card), a before and after preview page and a report (`SPECIFICATION.md`, section 7.35). Needs Node.js 18 or later and Playwright with its full Chromium build: from 1.11.2 the builder launches the executable `chromium.executablePath()` resolves, refuses a headless-shell build, and records the launched build next to the version (the recorded values were reproduced with Chromium 153.0.8010.12, Chrome for Testing, Playwright 1.63.0). | the builder: yes, to `.baseline/`, not served; its fonts: where the site builds a card (from 1.13.0; until 1.12.4 with every builder); `tools/site-card.example.toml`: no (the site writes its own `.baseline/site-card.toml` from it; from 1.12.3, step 2) |
| `tools/check_overflow.js` | The sideways-scroll check (from 1.8.0): loads the site's pages at 320, 360, 390 and 1512 px in both schemes and fails on sideways scroll, a table box past the window edge, a script error or an HTTP error. Needs Node.js 18 or later and Playwright; recommended, not mandatory. | yes, to `.baseline/check_overflow.js`, not served, where the site runs it |
| `logos/` | Per site: `<site>.svg` (full colour: hero, rail, footer), `<site>-mono.svg`, `<site>-favicon.svg`, `<site>-card.svg` (link preview), `<site>-touch.png` (Apple touch icon, 180 by 180, opaque). From 1.11.0 also `vendorreplies.png`, vendorreplies.ai's footer icon (CONAN's crowned head, 168 by 168, transparent). From 1.12.0 also NodeKeeper's 5 files (`nodekeeper.svg` and the rest: a graphics card with 2 fans and an edge connector), which only nodekeeper.ai needs. From 1.12.1 also `<site>-favicon.ico` for every site, vendorreplies.ai included (16, 32 and 48 px frames; each site serves its own as `/favicon.ico`), and vendorreplies.ai's `vendorreplies-touch.png` (Apple touch icon, 180 by 180, opaque: the 168 px head on navy `#0b121c`), `vendorreplies-favicon-32.png` and `vendorreplies-favicon-16.png`; vendorreplies.ai has no SVG favicon or mono mark, because its logo is photographic artwork. From 1.12.2 those 11 icon files are a reference only: each site builds its icons with `tools/build_card.js` and copies from `logos/` only the files its pages load (step 2). | the files the site's pages load |
| `marks/` | GitHub, LinkedIn and ORCID marks for the footer, with `marks/NOTICE.md` (sources, dates, terms). | yes, with `marks/NOTICE.md` |
| `reference/` | 16 reference pages, 1 per page type (section "The reference pages" below); from 1.11.0 `banner.html` with its sample palette `vendorreplies-palette.css` and placeholder pictures in `media/`. | no |
| `tools/measure.js` | Measures the 2 per-site constants (section "Per-site constants"). | no |
| `tools/build_icons.py` | From 1.12.1: makes the `<site>-favicon.ico` files and vendorreplies.ai's touch icon and PNG favicons from the logos; the files in `logos/` are its output. Needs Python 3 with Pillow, and Google Chrome. From 1.12.2 a reference only: `tools/build_card.js` makes each site's icons (D-080). | no |
| `SPECIFICATION.md`, `CONTENT-GUIDELINES.md` | The specification and the content guidelines at the version published with this package. | no |
| `LICENSE`, `NOTICE.md` | CC0 1.0; the exceptions (fonts, marks, and the project names and domain names). | yes |
| `SHA256SUMS`, `CHANGELOG.md`, `README.md` | Checksums of every file; history; this file. | no |

## Adoption in a repository

1. Obtain the version as its archive, `https://posluns.ca/baseline/baseline-<version>.tar.gz`, and verify its SHA-256 against `https://posluns.ca/baseline/SHA256SUMS` before unpacking. Never verify file by file over the live domain: Cloudflare injects scripts into the HTML it serves, so an HTML file fetched from the site is not the published file. Inside the archive, `SHA256SUMS` covers every file of the version (`sha256sum -c SHA256SUMS` from the version folder).
2. Copy only the runtime and licence files (the "yes" rows above) into the site's own asset folder (proposed `assets/baseline/`): `baseline.css`, `palettes.css`, `fonts.css`, `fonts/` with the 4 licence files, `copy.js`, `select.js`, `search.js`, `rail.js`, `matrix.js`, `scheme.js`, the logo files its pages load, `marks/` with `marks/NOTICE.md`, `LICENSE`, `NOTICE.md`. A site may leave out a script that none of its pages loads (`scheme.js` is loaded on every page, so it is always copied); the pin lists only the files the site copied. From 1.12.2 the logo files a site copies are those its pages load and those its card builder reads (its `logo` and `favicon`): the 6 full-colour rainbow logos (`<site>.svg`) and `vendorreplies.png` for the family footer, its own `<site>-favicon.svg` and `<site>-touch.png` for the head (vendorreplies.ai has neither), and any other of its own marks a page loads; never an `.ico` file or vendorreplies.ai's 1.12.1 icon PNGs, which are a reference only (until 1.12.1: `logos/` whole). Copy `tools/check_palette.py` to `.baseline/check_palette.py`, outside the served tree: the build runs it from the repository, so the site copies and pins it, and never serves it. Never copy `reference/`, `tools/measure.js`, `tools/build_icons.py`, `gen_palettes.py`, `contrast-report.txt`, `site-palette.example.css`, `tools/site-card.example.toml` or the documents: `tools/site-card.example.toml` is the template from which the site writes its own `.baseline/site-card.toml`, the file the builder reads, so a site keeps no copy of it, and a site that kept a copy from an earlier version replaces it, or deletes it and its pin entry (until 1.12.2 this step did not name it). From 1.12.2 the site builds its `favicon.ico` with the card builder (step 3) and serves it at its root as `/favicon.ico` (step 5); in 1.12.1 it copied `logos/<site>-favicon.ico` there.
3. Record the baseline version and each copied file's SHA-256 in `.baseline/pin.toml`:

   ```toml
   version = "1.13.0"

   [files]
   "assets/baseline/baseline.css" = "<sha256>"
   "assets/baseline/palettes.css" = "<sha256>"
   ".baseline/check_palette.py" = "<sha256>"
   ```

   The pin covers 2 locations: `assets/baseline/` (the served runtime and licence files) and `.baseline/` (the build tool `check_palette.py`, never served). A pin gate accepts entries under either location, checks each digest, and fails on an unlisted file in `assets/baseline/`, a listed file that is missing, or an entry outside the 2 locations.

   Add an offline gate that fails if any copied file differs from the pin. Run that gate, the palette gate (mandatory from 1.6.0, below), the security.txt gate and the site's other integrity gates from the Cloudflare Pages build command, so that any failure fails the deploy. No gate reaches the network. The palette gate:

   ```sh
   python3 .baseline/check_palette.py --site <name> --palettes assets/baseline/palettes.css
   # with a site palette file:
   python3 .baseline/check_palette.py --site <name> --palettes assets/baseline/palettes.css --site-palette assets/site-palette.css
   ```

   It prints a contrast table per scheme and exits 0 when every check passes, 1 on any failure or on input it cannot read or evaluate, and 2 on a usage error. The exit code is the contract: a build reads the exit code, never the wording of the printed result line, which may change between releases.

   From 1.8.0 the palette gate also fails when neighbouring heat steps lie less than 2.5 apart (OKLab x 100), and no longer checks `--focus` on the heat steps, because the focus ring in a heat cell is drawn in `--text`. Copy `palettes.css` and `check_palette.py` from the same version: the 1.8.0 gate fails the 1.7.0 palettes.

   Recommended from 1.8.0, where the build or the site's CI has Node.js 18 or later and the `playwright` package: the overflow check, run on the built pages served locally. List the site's pages in a file, 1 path per line:

   ```sh
   python3 -m http.server 8000 --bind 127.0.0.1 --directory <build output> &
   node .baseline/check_overflow.js --base http://127.0.0.1:8000 --pages .baseline/pages.txt
   ```

   It checks each page at 320, 360, 390 and 1512 px in both schemes and fails on sideways scroll, a table box past the window edge, a script error or an HTTP error; requests to any other origin are refused. Exit codes as for the palette gate. If the site pins it, add `.baseline/check_overflow.js` to `.baseline/pin.toml`.

   From 1.9.0, the link-preview card. Copy `tools/build_card.js` to `.baseline/build_card.js` and, where the site builds a card, `tools/card-fonts/` to `.baseline/card-fonts/` (the builder reads its fonts from beside itself), pin them, and write `.baseline/site-card.toml` from `tools/site-card.example.toml` with the site's approved copy, colours and illustration (the template itself is not copied or pinned; step 2). Build on a machine with Node.js 18 or later and Playwright with its full Chromium build (`npx playwright install chromium`; from 1.11.2 the builder launches the executable `chromium.executablePath()` resolves, Playwright's full Chromium build, and refuses a headless-shell build; the recorded values were reproduced with Chromium 153.0.8010.12, Chrome for Testing, Playwright 1.63.0), never in the deploy build:

   ```sh
   node .baseline/build_card.js --data .baseline/site-card.toml --out <review folder> \
     --before site/images/<site>-card.png --palettes assets/baseline/palettes.css
   ```

   It writes `<site>-card.png`, `<site>-card-safe.png`, `<site>-card-teams.png` (from 1.10.0, the part of the card Microsoft Teams shows), `<site>-touch.png` (when the data file names a logo), `card-tags.html`, `cardpreview.html` and `card-report.txt`, with `<site>-card-before.png` and `<site>-card-before-teams.png` when `--before` is given, and exits 0 only when every check passes (`SPECIFICATION.md`, section 7.35). From 1.12.2 (D-080) the data file's `favicon` key names the site's `<site>-favicon.svg`, and the builder also writes `<site>-favicon.ico` (16, 32 and 48 px frames, each rendered at its own size), which the site commits at the root of its published tree and serves as `/favicon.ico`; it is the site's own file, not a pin entry. vendorreplies.ai names `vendorreplies.png` as both `logo` and `favicon`, with `touch_bg = "#0b121c"`, and gets its `.ico`, `vendorreplies-favicon-32.png`, `vendorreplies-favicon-16.png` and `vendorreplies-touch.png`, which it serves from a folder of its own. A site that builds no card, or keeps its own, runs `node .baseline/build_card.js --data .baseline/site-card.toml --out <folder> --icons-only`, which reads only `site`, `logo`, `favicon` and `touch_bg`. From 1.10.0 the data file may set `layout = "centre"`, which keeps the whole card inside the Teams crop; agree the layout with the owner, who sees both cards as Teams crops them in the preview. Do not replace the live card until the owner approves the before and after preview: publish `cardpreview.html` with its images at the root of the site's preview branch, `https://demo.<project>.pages.dev/cardpreview.html`, and report that URL with both SHA-256 values (auditthis.ai shows the images inline in the owner's Claude session instead). After approval, commit the card as `site/images/<site>-card.png` and the tags into the page head, deploy, check the live image's SHA-256 against the approved one, and remove the preview page, the before image and the Teams crops. Keep the proof image and the report outside the served folder. From 1.13.0 (D-103) a site that runs the builder only with `--icons-only`, which reads no font, does not copy `tools/card-fonts/`, and may delete `.baseline/card-fonts/` and its pin entries; until 1.12.4 this step told every site that holds the builder to copy the fonts.
4. Put `class="site-<name>"` on `<html>` (`site-secureconfig`, `site-opfiles`, `site-auditthis`, `site-cleanlanguage`, `site-aiqt`, `site-grclibrary`, from 1.11.0 `site-vendorreplies`, and from 1.12.0 `site-nodekeeper`). That class, the logo files, the content and an optional `site-palette.css` are the only per-site differences. A site must not add CSS that overrides `baseline.css`, with 2 exceptions: an optional `site.css`, loaded after it, may set only the per-site constants and spacing tokens of the section "Per-site constants" (`--sw` and `--logo-inset` when the site's statement or logo changes, from 1.11.1 `--band-pad`, and from 1.13.0 `--para-gap`, `--download-after` and `--download-line-gap`, each clamped between 0 and its default, so a larger value has no effect and a negative value gives 0); and, from 1.6.0, an optional `site-palette.css`, loaded immediately after `palettes.css`, may override palette, family and component tokens, from 1.9.1 on the site's own decision (section "Site-owned palette colours"). A need the template cannot meet is a change request to the baseline.
5. Generate every page from the site's own sources with the markup of the reference pages; they are the contract. Every page loads `scheme.js` in `<head>` before the stylesheets (no `defer`), links `fonts.css`, `palettes.css`, the site's `site-palette.css` where it has one, and `baseline.css` in that order, carries the favicon and touch-icon links, the skip link, the top bar with the light and dark switch, 1 `h1`, the footer and the `#copy-status` live region, and contains no inline style, inline script or `on*` attribute. A `<script type="application/ld+json">` block is data and is allowed, on live pages only, never on a noindex draft.
6. Update the site's own gates where they conflict (`SPECIFICATION.md`, section 11).
7. Add this entry to the repository's NOTICE:

   > Site template: the shared site template (baseline <version>, https://posluns.ca/baseline/) is dedicated to the public domain under CC0 1.0 Universal, with 3 exceptions. The fonts Newsreader, Public Sans, JetBrains Mono and Dancing Script and, where present, the card builder's Inter fonts are licensed by their authors under the SIL Open Font License 1.1; each font's licence file travels with its font files. The GitHub Invertocat, the LinkedIn [in] bug and the ORCID iD icon are trademarks of their owners, used under their owners' published terms as recorded in the NOTICE.md that travels with the marks, and are not covered by the CC0 grant. The project names that the logos carry (SecureConfig, OPFiles, AuditThis, CleanLanguage, AIQT, GRC Library, VendorReplies and NodeKeeper) and the projects' domain names are the projects' own names and marks, which CC0 1.0 does not license (its section 4(a)).

8. Updates arrive as 1 pull request per site that bumps the pin and the copied files; each site's own checks run as usual.

### Staging at `/draft/` and promotion

0. Where (owner, 2026-11-06, from 1.11.0). A site's draft for review is built on a branch named `draft` in the site's repository, which Cloudflare Pages deploys at its branch address, `https://draft.<project>.pages.dev`, where `<project>` is the site's Cloudflare Pages project name; the draft's pages are under `/draft/`, so the owner reviews `https://draft.<project>.pages.dev/draft/`. The live site (the production branch) does not carry the draft. Until 1.10.0 the draft was staged at `/draft/` on the live site.
1. Base path. Generate every page, asset and internal link with a configurable base path: `/draft/` while staging, `/` after promotion. The draft is self-contained: no draft page links to, or loads anything from, the live pages at the root, except a live file whose path is kept at promotion (for example `/downloads/*`, or opfiles.ai's `/adopt.md`), which a draft page may link rather than copy. The baseline files are copied under the draft (`/draft/assets/baseline/`) and move with it. Every path in this package is relative, so the files work at any base path.
2. Kept out of search and away from the live site. Every draft page carries `<meta name="robots" content="noindex, nofollow">`; `_headers` sends `X-Robots-Tag: noindex` for `/draft/*`; the draft is absent from `sitemap.xml`, `llms.txt` and every other index the live site publishes; the live pages never link to it. Check `_redirects` on the `draft` branch for a `/draft` redirect and remove it there.
3. Live site unchanged, with 1 exception. The draft stays on the `draft` branch until promotion, so building it changes no live page, path, redirect or published file. The exception is the owner-ordered live hardening, in its own pull request ahead of the draft: `/.well-known/security.txt`, `sitemap.xml` and the `Sitemap` line of `robots.txt`, the `apple-touch-icon` links and, from 1.12.1, `/favicon.ico`, the removal of a live `/draft` redirect, and the header changes of `SPECIFICATION.md` section 9. Nothing else changes live before promotion. Every existing gate keeps passing on the production branch; on the `draft` branch, gates that scan every HTML file either cover the draft under the template's rules or exclude `/draft/` explicitly, and the exclusion is removed at promotion.
4. Review. The draft is complete when every page of the site's page list exists under `/draft/` on the `draft` branch, the site's checks and the content guidelines checklist pass, and the implementer has reported to the owner with `https://draft.<project>.pages.dev/draft/`.
5. Promotion, after the owner's approval only. In 1 pull request from the `draft` branch to the production branch: remove the old pages from the published tree (they remain in git history); rebuild the draft with base path `/`; add redirects for every old public path that changes; restore the root's `sitemap.xml`, `llms.txt` and other indexes for the new pages; remove the `/draft/` noindex rules and any gate exclusion; verify every fixed path on the preview deployment before merging.

### Headers

Cloudflare sends only its automatic headers (`X-Content-Type-Options`, `Referrer-Policy`, `Access-Control-Allow-Origin`, HSTS from the zone, `Speculation-Rules` from Speed Brain); no custom Cloudflare rule. The repository's `_headers` sends `Content-Security-Policy`, `X-Frame-Options: DENY`, `Permissions-Policy` and `Cross-Origin-Opener-Policy: same-origin` on `/*`, and on `/draft/*` removes the live CSP, sets the draft CSP and `X-Robots-Tag: noindex`. The shared CSP, the 4 Cloudflare features and their needs, the hash and nonce caveat, the Speed Brain caveat and the proposed Permissions-Policy value are in `SPECIFICATION.md`, section 9.

### Site class, logo and base path

- The site class on `<html>` selects the palette (`palettes.css`) and the per-site constants in `baseline.css` (`--sw`, `--logo-inset`, `--tail-from`).
- The logo appears once per page: in the hero on the landing page (`.hero-art img`), above the rail on every other page (`.rail-logo`); the footer uses the 6 full-colour logos and, from 1.11.0, `vendorreplies.png` as the seventh card. In the head: `<link rel="icon" href="<base>assets/baseline/logos/<site>-favicon.svg" type="image/svg+xml">` and `<link rel="apple-touch-icon" href="<base>assets/baseline/logos/<site>-touch.png">`; the card logo is for link previews. From 1.12.1 every site also serves `/favicon.ico` at the root of the site, which browsers and other clients request without a link; the 2 links stay. From 1.12.2 it is the `<site>-favicon.ico` the site builds with the card builder; in 1.12.1 it was a copy of `logos/<site>-favicon.ico`. vendorreplies.ai, whose logo is photographic artwork with no SVG favicon or mono mark, links its PNG favicons, `<link rel="icon" href="<icons>/vendorreplies-favicon-32.png" type="image/png" sizes="32x32">` and `<link rel="icon" href="<icons>/vendorreplies-favicon-16.png" type="image/png" sizes="16x16">`, in place of the generic `<site>-favicon.svg` link, since it has no SVG favicon, serves `/favicon.ico` like every site, and points its touch-icon link to its `vendorreplies-touch.png` (`<link rel="apple-touch-icon" href="<icons>/vendorreplies-touch.png">`); until 1.12.1 it linked `vendorreplies.png` for both. From 1.12.2 `<icons>` is the folder of its own where it serves the 3 files it builds, outside the pinned copy folder; in 1.12.1 it was `<base>assets/baseline/logos`.
- The top bar of every site: the site's own 4 to 6 page links on 1 row, then GitHub as the last, right-most item where the site has a public repository; no Disclosure or Licence item (`SPECIFICATION.md` section 7.1). On a window too narrow for the row, the links scroll sideways and a shadow at the scrolled edge shows that more follow; no script. From 1.6.0 the light and dark switch (`button.scheme`, `scheme.js`, section 7.33) follows the links at the right-hand end, outside the scrolling row; at 1000 px and below it sits at the right of the wordmark. The page follows the device setting until the reader presses it; the choice is stored in `localStorage` (key `scheme`) and applied before the first paint on later pages. Without script the button stays hidden.
- The landing page's "About this project" line, below the value paragraph, links Disclosure, Licence, Privacy (`https://posluns.ca/privacy/`) and Security (`https://posluns.ca/security/`), separated by middle dots, with 1 space after each dot so that the line wraps on phones (from 1.7.0; section 7.24); it is the only place, besides the value paragraph's own licence link, that links the Disclosure and Licence pages. The licence page lives at `/license/` (heading "Licence"), the disclosure page at `/disclosure/`; the disclosure page links the vulnerability disclosure policy and the privacy policy.
- aiqt.ai's rules page keeps the anchors `#rule1` to `#rule5` on its 5 clause sections, so the live redirects from `/rule1` to `/rule5` keep working.

## The reference pages

Each page in `reference/` is built for 1 example site and opens directly from the folder, in light or dark mode as the operating system prefers until the switch in the top bar is pressed; every page, the 404 page included, carries the switch (from 1.6.0). Cross-links between pages that exist in the set go to that page; links to pages of the example site that are not in the set go to `landing.html`, so the set stays self-contained. The pages are markup examples, not a site.

| Page | Example site | What it fixes |
|---|---|---|
| `landing.html` | secureconfig.ai | The hero (kicker, statement, introduction, start link, logo), the "Start here" entry tiles, the "What it gives you" value paragraph, the "About this project" line, the footer |
| `document.html` | aiqt.ai, the standard | The document grid with label column, rail ("On this page"), document control strip, numbered clauses, callout, Next block |
| `reference.html` | aiqt.ai, mappings | Figures, framed registers, a fixed-layout table with `colgroup` widths and wrapping cells, a commit in 7-character form, details blocks |
| `install.html` | cleanlanguage.ai | The selector (`select.js`), download blocks, copy buttons (`copy.js`), steps, the "Last verified" line |
| `license.html` | auditthis.ai | The licence page: scope sentence, parts under another licence, the complete plain-language summary, the owner's note, Next |
| `disclosure.html` | opfiles.ai (entries quoted from the AIQT disclosure as an example of the format) | The disclosure page: opening sentence, entries of claim, limitation and evidence, the security line with the vulnerability disclosure and privacy policy links, Next |
| `404.html` | grclibrary.ai | The 404 page with the logo-only rail |
| `channels.html` | cleanlanguage.ai, by channel | The channel list page: 1 section per channel, each an `h2` and its list of rules, the rail listing the channels, the source callout |
| `checklist.html` | cleanlanguage.ai, checklist | The checklist page: groups of questions as numbered clauses (`N.1`, `N.2`), citable by number |
| `demo.html` | cleanlanguage.ai, try it | The demo page: a before-and-after pair, the prompt in a copy block, the no-script note, the install link |
| `figures.html` | auditthis.ai, references | The 4 components of 1.4.0: the inline accent (`span.accent`), compact figures (`.figures.compact`), a figure beside text (`.fig-aside`) and the brand marker (`mark.marker`) |
| `search.html` | grclibrary.ai, search | `search.js` in results mode (`data-search-mode="results"`): nothing shows until the reader types or picks a filter, title matches rank first, the query and filters in the address (`?q=`, `?domain=`, `?type=`, `?fw=`), facet chips as links; 16 illustrative rows |
| `banner.html` (from 1.11.0) | vendorreplies.ai, home | The banner (`.banner.banner-fill`, a neutral placeholder picture in `media/`), bands (`.band`, `.band-tint`, `.band-dark`), the card grid (`.boxes`, `.boxes-numbered`), the stacked and framed pair (`.pair.pair-stack`, `.msg`), `.doc.no-rail` and `measure-full`, with the footer as on vendorreplies.ai (its card current, last); the sample frame colours are in `vendorreplies-palette.css` |
| `facets.html` | grclibrary.ai, index | `search.js` facets in the default mode: every row shows, chip groups for domain, type and framework combine with each other and with the query; from 1.7.0 the domain and type chips that would show no documents are marked unavailable (`data-filter-empty="disable"`), and the type chips count the documents each would show (`data-filter-count="live"`); the same 16 illustrative rows |
| `crosswalk.html` | grclibrary.ai, crosswalk | The left-rail page (`.doc.left-rail`, `.lrail`) with 7 views as `matrix.js` presets, the matrix table (`table.matrix`, `data-col`) in the sticky scroll box (`.tablewrap.bounded`, `table.sticky.sticky-col`), `?view=`; 6 domains by 8 instruments, with illustrative identifiers and counts |
| `matrix.html` | grclibrary.ai, large matrix | The large matrix of 1.7.0 (`SPECIFICATION.md` section 7.34): 10 domains by 20 instruments in the combined form (`.tablewrap.bounded` as a named region, `table.matrix.fixed.sticky.sticky-col.pin-cap`), rem column widths (`col.r6` to `col.r24`), wrapping cells (`cells-wrap`, `td.nowrap`), the capped pinned column (`pin-cap`), stripes and row hover (`zebra`, `rowhover`), empty cells (`td.na`), column toggles and `?cols=`, heat shading with its legend (`data-heat`, `.heat-legend`), the column highlight (`data-col-highlight`), rotated headers (`th.rot`) and column groups (`data-grp`, `.grp`); views as `matrix.js` presets; illustrative identifiers, counts and shading |

Sticky tables: from 1.5.0 a table marked `.sticky` sits in a wrapper marked `.tablewrap.bounded`, a box at most 75 % of the viewport height that scrolls in both directions with the header row fixed at its top; add `.sticky-col` to the table to keep the first column at its left (`reference.html`, `crosswalk.html`; `SPECIFICATION.md` section 7.31). From 1.7.0, opt-in, add `.pin-cap` to give the pinned first column a capped width, `clamp(8rem, 30vw, 15rem)`, with wrapping labels; the `pin-r<N>` classes change it. Without `.pin-cap` the column is as in 1.6.1.

Large matrix tables (from 1.7.0, `matrix.html`, `SPECIFICATION.md` section 7.34): each capability is opt-in by a class or an attribute and works without script; the column toggles and the highlight need `matrix.js`. The matrix prints whole, in black on white, with its controls hidden.

Prompt and code blocks come in 2 variants beyond the plain copy block: `pre.prompt-block` (capped at 36rem, scrolls inside, for a prompt the reader copies) and `pre.prompt-flow` (the whole text in the page flow, for a portable text the reader reads in place); both keep the same `pre id`, copy button and status contract.

## Site-owned palette colours

From 1.6.0 a site may override any palette token (`SPECIFICATION.md` section 6.1), light and dark, in its own `site-palette.css`, with no template release. From 1.9.1 it may also override the family tokens (section 6.2, not the print tokens) and the component tokens (section 6.4: chips, buttons, navigation, rails, the install picker, the search box, tags, table headers, from 1.11.2 the warm inline ink `--ink-warm` and, from 1.11.3, the heading ink of plain and tinted bands `--heading-fg`), and it does so on its own decision: the template is a baseline, and the gate holds the contrast floor. Until 1.9.0 a site overrode a colour only at the owner's explicit request. A site still adds no CSS rules of its own; a colour no token reaches is a change request for a new component token.

1. Copy `site-palette.example.css` to the site's asset folder as `site-palette.css` (outside the pinned baseline files) and link it immediately after `palettes.css`.
2. Set each value in both forms of its scheme, with the selectors of `palettes.css`, so that the later file wins whatever the device setting and the switch:

   ```css
   /* light */
   .site-<name> { --link: #...; }
   [data-scheme="light"].site-<name> { --link: #...; }
   /* dark */
   @media (prefers-color-scheme: dark) {
     .site-<name>:not([data-scheme="light"]) { --bg: #...; }
   }
   [data-scheme="dark"].site-<name> { --bg: #...; }
   ```

   The file holds palette, family and component tokens only, as `#rgb` or `#rrggbb`, on these selectors; the gate rejects anything else. For example, chips in the site's own blue: `.site-<name> { --chip-fg: #...; --chip-border: #...; }` in both light forms, and the dark values in both dark forms.
3. Run the palette gate with `--site-palette`. From 1.9.1 it also reads `baseline.css` (the one beside `palettes.css`, or `--baseline <file>`), to resolve the family and component tokens. It fails if any pairing misses its threshold (text 4.5:1; focus ring, strong rule, rainbow ink and control borders 3:1), if the 2 forms of a scheme disagree, or if the file holds anything other than palette tokens.
4. QA, the standard grclibrary.ai set and the owner recorded: a colour change is 1 value in 1 file (in both forms of its scheme); its review is a before and after comparison, of the file and of the rendered pages, showing that only the expected value changed, plus the gate passing.

## Per-site constants

Two values in `baseline.css` depend on a site's content and are measured, not designed; the spacing tokens below them are optional and set by the site. The values shipped in `baseline.css` for the 6 approved statements and logos:

| Site | `--sw` light | `--sw` dark | `--logo-inset` |
|---|---|---|---|
| secureconfig.ai | 13.712 | 14.612 | 0.115 |
| opfiles.ai | 15.070 | 16.070 | 0.1225 |
| auditthis.ai | 12.562 | 13.337 | 0.2075 |
| cleanlanguage.ai | 12.824 | 13.724 | 0.185 |
| aiqt.ai | 19.868 | 21.218 | 0.0975 |
| grclibrary.ai | 13.069 | 13.894 | 0.115 |

- `--sw` (per site, light and dark): the hero statement's width on 1 line in em; dark mode adds 0.01em of tracking, hence the larger dark value. `font-size: clamp(2.6rem, 98cqw / var(--sw), 5rem)` puts the statement on 1 line whenever that fits at 2.6rem or more. The dark value applies in system dark mode and under a forced dark scheme.
- `--logo-inset` (per site): the logo artwork's visible left margin as a fraction of the SVG box, used to indent the rail's text under the artwork's left edge.
- `--band-pad` (from 1.11.1, optional, set by the site, not measured): the top and bottom padding of every `.band`, default `clamp(2.2rem, 5vw, 4rem)`. A site sets it in its `site.css`, for example `.site-vendorreplies { --band-pad: clamp(1.1rem, 2.5vw, 2rem); }` for half the default.
- `--para-gap` (from 1.13.0, optional, set by the site, not measured): the space under a paragraph, list, definition list, quotation or details block in a document, default 1.1rem; decrease only: `baseline.css` clamps it between 0 and 1.1rem, so a larger value has no effect and a negative value gives 0.
- `--download-after` (from 1.13.0, optional): the space under a download block, default 1.6rem (25.6 px); decrease only: `baseline.css` clamps it between 0 and 1.6rem, so a larger value has no effect and a negative value gives 0. For example, `.site-vendorreplies { --download-after: 1.1rem; }` for 17.6 px under a file-only block; `0px` makes the next block touch it.
- `--download-line-gap` (from 1.13.0, optional): the space under the file line where more lines follow it in a download block, default the paragraph gap in effect (`--para-gap`); decrease only: `baseline.css` clamps it between 0 and that paragraph gap, so a larger value has no effect and a negative value gives 0. It applies wherever the paragraph rule sets the file line's margin, at any depth of a document. It merges with the size line's 0.3rem top margin, so below 0.3rem it has no effect above a size line. Inside a callout, a pair or another box that sets its own paragraph margin the file line keeps that margin, and the token has no effect there.
- Give each of these 3 tokens a length, such as `0px` or `0.8rem`. A unitless number (a bare `0` included) or any other value that is not a length does not go through the clamp: it makes each margin declaration that reads the token invalid when the value is computed, so every margin that declaration sets is 0 on the elements where it applies. With `--para-gap`, each paragraph, list, definition list, quotation, grid-level `details` block and file line whose margin the token's rules set has a 0 bottom margin (its top and side margins are 0 already); with `--download-line-gap`, the file line followed by more lines has a 0 bottom margin where the paragraph rule sets it; with `--download-after`, the download block has a 0 bottom margin and keeps its 0.4rem top margin, which is a separate declaration. Nothing grows.
- A site re-measures only when its statement's words or its logo change, as a development task on any machine with Node and Playwright, never as a build step: `node tools/measure.js statement <url or file of the landing page>` prints the light and dark `--sw`; `node tools/measure.js logo logos/<site>.svg` prints `--logo-inset`. Set the new values in the site's own `site.css`, loaded after `baseline.css` (`.site-<name> .statement { --sw: ...; }`, the dark value inside `@media (prefers-color-scheme: dark)`, and `.site-<name> .rail { --logo-inset: ...; }`). Never edit the copied `baseline.css`: the pin and its checksum must keep matching. The tool makes no network requests other than loading the page you name (`npx playwright install chromium` once).

## Security contact

Each domain publishes `/.well-known/security.txt` with the W3 fields, pointing at the owner's vulnerability disclosure policy, `https://posluns.ca/security/`; a gate checks the file. The disclosure page's closing line links the same policy and the universal privacy policy, `https://posluns.ca/privacy/`.

## Checking a build

- No en or em dash in any file; no inline style or script (JSON-LD data blocks excepted, live pages only); no `on*` attribute; every external link with `target="_blank" rel="noopener noreferrer"` and a visually hidden "(opens in a new tab)".
- The palette gate passes (`.baseline/check_palette.py`, with the site's `site-palette.css` where it has one).
- Render at 320, 390, 820, 1180, 1512 and 2000 px in light and dark: no horizontal scroll (a bounded table box may scroll inside itself); the light and dark switch visible at the right-hand end of the top bar and toggling the scheme, the choice kept on the next page with no flash of the other scheme, and the button absent without script; the top bar's links on 1 row at every width (scrolling on phones, with the edge shadow), GitHub last where present; exactly 1 GitHub mark displayed in the footer and no blank tile; the footer on 1 line from 600 px of content width and on 2 lines below it; the hero statement on 1 line at 1440 px and above for a 1-sentence statement, in both schemes.
- The landing page within 2 screens at 1180 by 820; body text 20 px on desktop and 18 px on phones.
