# Shared site template: specification (1.13.0)

Status: 1.13.0, carrying the owner's decisions of 2026-11-03 through grclibrary.ai's change request (search results mode and facets, the matrix with presets, the left-rail page), the sticky table correction (the sticky scroll box), the hidden-content correction, the decisions after 1.5.1 (dark mode near black with the site's own tint, the light and dark switch, site-owned palette colours), the documentation corrections reported by auditthis.ai after adopting 1.6.0, and the owner's decisions of 2026-11-04 (the About line that wraps at 320 px, and the large matrix table capabilities requested for crosswalk pages), and, in 1.8, the corrections reported after 1.7.0 (long links in running text, group tags in a single-group view) with the owner's further decisions of 2026-11-04 (long text in documents, tables on paper, the chip list indent, the distance between heat steps, and an overflow check for the sites), and, in 1.8.1, 2 documentation corrections reported by auditthis.ai after adopting 1.8.0, and, in 1.9.0, the owner's decisions of 2026-11-05 on the link-preview card builder handed over by auditthis.ai, and, in 1.9.1, the owner's decisions of 2026-11-05 on chip states and on colours a site may set without a template change, and, in 1.9.2, the correction of stray pixels in cards reported by auditthis.ai, and, in 1.10.0, the owner's decisions of 2026-11-05 on cards that Microsoft Teams crops and the correction of the builder's version label reported by auditthis.ai, and, in 1.11.0, the owner's decisions of 2026-11-06 on vendorreplies.ai joining the family (the seventh footer card on silver, the banner, the bands, the card grid, the line-width options, and the stacked and framed before-and-after pair), and, in 1.11.1, the owner's decisions of 2026-11-06 on the footer names on desktop screens from a 1280 px window and the current site's cue, and, in 1.11.2, the owner's decisions of 2026-11-06 on the warm inline ink requested by vendorreplies.ai and of 2026-11-07 on the card builder's full-Chromium launch, and, in 1.11.3, the corrections approved by the owner on 2026-11-07 (the palette gate's version hints, the package headers, the card checks' build gate and the documents), the owner's decision of 2026-11-07 on the heading ink of plain and tinted bands requested by vendorreplies.ai, and the owner's direction of 2026-11-07 that every site, vendorreplies.ai included, has consistent security headers and configuration, and, in 1.11.4, the corrections approved by the owner on 2026-11-07 (the card builder's dithering comment and report inputs, the `--before` IHDR length check, the ancestor-safe heading-ink selector, and the repository facts), and, in 1.11.5, the corrections and the card options approved by the owner on 2026-11-07 (D-052, the 1.11.4 QA findings; D-054, an optional metal wordmark for the card and a centred-layout background that passes the subline contrast check) with the detail choices of 2026-11-08 (D-057 and D-058, below) and the count of the project sites in the content guidelines (D-050), and, in 1.12.0, the owner's decisions of 2026-11-08 (one family table for the palettes with a helper for the hues free in the rainbow, and a minimum distance between site colours, D-061; NodeKeeper's palette and logos, outside the rainbow and the family footer, D-066 to D-068) with the owner's ruling of 2026-11-08 that nodekeeper.ai's own site shows the family footer as is (D-075), and, in 1.12.1, the icons the owner approved on 2026-11-09 (D-079: a `favicon.ico` for every site, and vendorreplies.ai's touch icon and PNG favicons), and, in 1.12.2, the owner's decision of 2026-11-09 that each site builds its `favicon.ico` with the card builder (D-080), and, in 1.12.3, a documentation correction reported by auditthis.ai on 2026-11-10 (whether a site copies `tools/site-card.example.toml`), and, in 1.12.4, the owner's decision of 2026-11-12 on cleanlanguage.ai's request (D-094: the README's NOTICE entry names the project names and domain names as a third exception; line 1 of the card data-file template says a site does not copy it) with the vote panel's ruling of the same day that the entry's font exception also names the card builder's Inter fonts (D-095) and the owner's ruling of the same day that `NOTICE.md` item 3 and the entry say that the project names and domain names are the projects' own names and marks, which CC0 1.0 does not license (D-096), and the vote panel's ruling of the same day that the entry names no path (D-098), and, in 1.13.0, the owner's ruling of 2026-11-12 on vendorreplies.ai's request for block spacing (D-097: no gap grows; the last line of a download block in a document ends flush with the block; 3 spacing tokens a site may only decrease), the owner's ruling of the same day that a site copies the card builder's fonts only where it builds a card (D-103), and a correction to `rail.js` for an address fragment it cannot decode, published with baseline package 1.13.0 at https://posluns.ca/baseline/1.13.0/SPECIFICATION.md (1.0 was approved on 2026-11-02 and published with 1.0.0). A correction is a new version with a changelog entry. The owner chose the Atrium direction and approved its design through review round 11; this document describes the Atrium as built in the mockups (`site/drafts/atrium/` on the `mockups` branch of jposluns/posluns_site, `build.py` and `template.css`) and is the normative document the project repositories implement from. The content guidelines 1.5 (`CONTENT-GUIDELINES.md` in the same package) govern the wording; the Clean Language standard (https://cleanlanguage.ai) governs the prose.

Every value in this document is taken from `template.css` or `build.py`. Where a value is rounded or inferred, the text says so.

## 1. Purpose and scope

One template for the sites in the table below (7 project sites and, from 1.12.0, NodeKeeper, a tool), so that each looks and behaves as part of one family and differs only in its colours, logo, illustration and content. Sites 1 to 6 form the rainbow (the 6-stop ring of section 6.1 and the rotated footer cards of section 7.19); vendorreplies.ai is the seventh card of the family footer, outside the rainbow; NodeKeeper is in neither:

| Order | Site | Hue | Repository that holds the site |
|---|---|---|---|
| 1 | secureconfig.ai | red | jposluns/secureconfig (`site/`) |
| 2 | opfiles.ai | orange | jposluns/guardrails (`opf/site/`) |
| 3 | auditthis.ai | yellow | jposluns/auditthis.ai (`site/`) |
| 4 | cleanlanguage.ai | green | jposluns/cleanlanguage (`site/`) |
| 5 | aiqt.ai | blue | jposluns/guardrails (`site/`) |
| 6 | grclibrary.ai | purple | jposluns/grc_library (`.web/`, built at deploy) |
| 7 | vendorreplies.ai | coral | jposluns/vendorreplies (private) |
| 8 | nodekeeper.ai (from 1.12.0) | teal | Not yet confirmed: on 2026-11-08 jposluns/nodekeeper could not be read from the template owner's account, and nodekeeper.ai served no page |

posluns.ca keeps its current design and hosts the template package. vendorreplies.ai is the seventh site, since 1.11.0 (owner, 2026-11-06; section 2): its palette and footer card come from the template, it sits outside the 6-stop rainbow, and it keeps its own link-preview card, not built by `build_card.js`. NodeKeeper (nodekeeper.ai) uses the template from 1.12.0 (owner, 2026-11-08, D-066 to D-068): the package carries its palette (`.site-nodekeeper`) and its 5 logo files; it is outside the rainbow and has no card in the family footer, and nothing in this document describes its product.

## 2. Decisions recorded

All decisions are the owner's. Dates are 2026-11-02 unless stated.

| Topic | Decision | Source |
|---|---|---|
| Hosting | Each site stays in its project's repository; no separate site repositories | Owner, 2026-11-01 |
| Distribution | Pinned copies from `https://posluns.ca/baseline/<version>/`; no runtime loading from posluns.ca | Owner, option B |
| Direction | The Atrium (direction C) for all 6 sites | Owner, round 3 |
| AIQT | aiqt.ai stays on the Atrium and brings in the Standard direction's document elements: document control strip, numbered clauses, framed notes, framed registers | Owner, round 9 |
| Site shape | Each site is a small multi-page site; the landing page is at most 2 screens tall at 1180 by 820 | Owner, round 4 |
| Width | Content wraps to the full page width with 1 consistent gutter (`--pad`) on every page | Owner, round 4B |
| Hero | The logo sits at the right of the hero, right-justified, with a margin of half a logo width; the logo appears once per page | Owner, rounds 3 and 4B |
| Inner pages | The logo sits above the right rail; the rail's text starts under the logo artwork's left edge; the rail lists only the page's own sections ("On this page"), never a copy of the site menu | Owner, rounds 4B and 9 |
| Wordmark | Card-style wordmark in the top bar: bold name, gold `.ai`, gold rule beneath | Owner, round 5 |
| Hero statement | On 1 line whenever it fits the text column at 2.6rem or larger (3.2rem until 1.2.1; the owner, 2026-11-03: a statement that fits on 1 line must not break); otherwise 1 break, before the emphasized phrase or between the 2 sentences, never mid-phrase | Owner, rounds 9 and 10 |
| Entry points | "Start here" as 1 row of equal tiles read left to right, number above title, metadata under the title; 1 vertical list at 1000 px and below | Owner, round 9 |
| Value paragraph | "What it gives you": 1 paragraph under 100 words for a non-developer reader, ending with 1 plain sentence of the material facts; replaces Benefits and gaps | Owner, round 11 |
| Licence page | At `/license/` with the heading "Licence"; linked from the landing page's About line (section 7.24) and its value paragraph, not from the top bar (since 1.2.0); the owner's licence note confirmed | Owner, rounds 6, 7 and 9 |
| Disclosure and Licence on every site | Every site has a Disclosure page at `/disclosure/` and a Licence page at `/license/`, both linked from the landing page's About line on every site (section 7.24; the top bar carried them in 1.0.0 to 1.1.0, and 1.2.0 moved them). The Disclosure page follows the format of AIQT's `DISCLOSURE.md` (for each thing the project does: the claim, its limitation, and the evidence), written from that project's own sources | Owner, 2026-11-02 |
| AIQT rules | The 5 rules are sections of 1 page, `/rules`, keeping the live anchors `#rule1` to `#rule5`, so the existing permanent redirects from `/rule1` to `/rule5` (aiqt.ai `site/_redirects`, lines 8 to 12) keep working unchanged. The mockup's `#rule-1` form is not used | Owner, 2026-11-02 |
| Footer | 1 contiguous bar at 100 % of the content width: the 6 project cards with the current site first on the rotated cyclic rainbow, then GitHub, LinkedIn and ORCID as icon-only tiles, then the signature "A Jeff@Posluns.ca Project" linking to posluns.ca; the band fades to neutral grey over the tail; names hide below the stage width; phones show 2 lines with the grey band as the separator; no Licence link in the footer | Owner, rounds 7, 8 and 9; bar made contiguous after round 11 |
| Third-party marks | GitHub, LinkedIn and ORCID marks unaltered, from their owners' official files, with no plate, and visibly no larger than the project logos (GitHub's secondary-placement rule) | Owner, round 6 |
| Tables | A long cell wraps within its own cell; a commit is shown in its 7-character form with the full hash available | Owner, round 9 |
| Copy | The Clean Language skill is required for all copy; the content guidelines govern structure and length | Owner, rounds 2 and 9 |
| Typography | Serif for text and headings, sans for labels and navigation, monospace for code, script for the signature; self-hosted OFL fonts | Owner |
| Colour | Per-site warm palette, light and dark, chosen by the operating system (no toggle until 1.5.1; from 1.6.0 the reader can override it with the switch in the top bar, section 7.33); primary hue from the 6-colour rainbow in the order above | Owner |
| Scripts | Small shared set, same-origin files, each opt-in, every page usable without JavaScript | Owner |
| Logos | Vector marks for all 6: full colour, single colour, favicon and card versions | Owner |
| Package licence | Template files CC0 1.0, except the 4 fonts (SIL OFL 1.1, licence files shipped) and the GitHub, LinkedIn and ORCID marks (their owners' trademarks, used under their brand terms and excluded from the CC0 grant), as of 2026-11-02; the current exceptions are in section 4, item 8 | Owner |
| Fallback | Browsers without container queries get a footer that wraps instead of overflowing | Coordinator, after round 11 |
| Rollout | All 5 repositories start together once baseline 1.0.0 is published (supersedes the auditthis.ai pilot). Each builds the new site into `/draft/` on its own domain, alongside the live site; after the owner approves a draft, the old pages leave the published tree and the draft moves to the root (section 4.1) | Owner, 2026-11-02 |
| Favicons | The 16 px favicon of secureconfig.ai is simplified in baseline 1.0.0 to the padlock alone; grclibrary.ai keeps its multiple-book favicon, by the owner's choice over a simplified 1-book version. opfiles.ai and cleanlanguage.ai read at 32 px and may be simplified in a later version | Owner, 2026-11-02 |
| Cloudflare features | All 4 allowed on every site: JavaScript Detections, the Web Analytics beacon, Rocket Loader and Speed Brain | Owner, 2026-11-03 |
| Headers | Cloudflare sends only the headers it generates automatically; every other header comes from the repository's `_headers`; no custom Cloudflare rule and no managed header transform (section 9) | Owner, 2026-11-03 |
| CSP | The same policy on every site for draft and template pages (section 9); live pages widened only where needed until promotion | Owner, 2026-11-03 |
| security.txt | Each repository serves `/.well-known/security.txt` with the W3 fields, checked by a gate; Cloudflare's security.txt feature stays off | Owner, 2026-11-03 |
| Pull request order | On every site the live-hardening pull request first, then the `/draft/` rebuild (section 4.1) | Owner, 2026-11-03 |
| Privacy policy | 1 universal policy at `https://posluns.ca/privacy/`; every Disclosure page links it beside the vulnerability-disclosure line | Owner, 2026-11-03 |
| Pin and copy set | Sites copy only the runtime and licence files and record them in `.baseline/pin.toml` (section 4) | Owner, 2026-11-03 |
| JSON-LD | `<script type="application/ld+json">` blocks are data, allowed on live pages only, never on noindex drafts (section 5) | Owner, 2026-11-03 |
| Extra pages | Pages outside a site's section 11 list are built from the template's components; the channel list, checklist and demo page types are added to the template (sections 7.21 to 7.23) | Owner, 2026-11-03 |
| Build command | Each site's Cloudflare Pages build command runs its offline integrity gates and fails the deploy on any failure (section 4) | Owner, 2026-11-03 |
| NOTICE entry | Every adopting repository's NOTICE gains a site-template entry (section 4) | Owner, 2026-11-03 |
| Draft links | Draft pages may link live files whose paths are kept at promotion (`/downloads/*`, opfiles.ai's `/adopt.md`) rather than copying them | Owner, 2026-11-03 |
| Top bar | No wrapping: the site's 4 to 6 page links on 1 row, and Disclosure, Licence and GitHub as a stack of 3 equal buttons at the far right (section 7.1) | Owner, 2026-11-03 |
| Flowing prompt block | A variant of the prompt block with no cap and no inner scroll, for portable texts read in place (section 7.11) | Owner, 2026-11-03 |
| Apple touch icons | Per site a 180 by 180 opaque PNG in the package, linked by every page (section 5); the sites add the link once the template ships the icons | Owner, 2026-11-03 |
| Permissions-Policy | 1 value for all sites and posluns.ca (section 9), the union of guardrails' proposal and posluns.ca's earlier list; confirmed by the owner | Owner, 2026-11-03 |
| Top bar, revised | The bar holds the site's own page links and, where the site has a public repository, GitHub as the last, right-most item; Disclosure and Licence leave the bar (the owner: they are not interesting to most readers) and the stacked buttons of 1.1.0 go (section 7.1) | Owner, 2026-11-03, after 1.1.0 |
| About this project | Below the landing page's value paragraph, a region labelled "About this project" with 1 line of links: Disclosure, Licence, Privacy (`https://posluns.ca/privacy/`) and Security (`https://posluns.ca/security/`), separated by middle dots (section 7.24); Disclosure and Licence are reached from the landing page only, besides the value paragraph's own licence link; inner pages do not link them | Owner, 2026-11-03, after 1.1.0 (chose variant B) |
| SecureConfig wording | The value paragraph reads "encryption left off, default credentials for logins, factory settings kept" | Owner, 2026-11-03 |
| SecureConfig introduction | "…leave admin consoles and AI tools on default credentials, and keep default settings unless told otherwise." (was "without a login"), matching the value paragraph | Owner, 2026-11-03 |
| Rail follows the reader | The rail marks the section the reader is on, with `aria-current="location"`, through a 4th script, `rail.js`; no rail link is marked in the markup (the static first-item mark of 1.0.0 to 1.2.1 claimed a location the reader was not at). Requested by the auditthis.ai instance at the owner's request | Owner, 2026-11-03 |
| Text emphasis and figures | 4 additions requested by the auditthis.ai instance at the owner's request, all adopted by the owner: an inline accent for numbers and key terms in running text (7.25), compact figures for up to 6 figures on 1 line (7.26), a figure beside a short paragraph (7.27), and a brand marker that emphasizes text in the site's rainbow colour while the text keeps `--text` (7.28) | Owner, 2026-11-03 |
| secureconfig.ai CSP | The site moves to `'self'` files: its CSP allows the same-origin stylesheet and scripts, its element allowlist accepts the template's elements, and its gates keep refusing inline code | Owner |
| Search and facets | grclibrary.ai's requests 1 and 2 are met by extending `search.js`, not by a new script: a results-only mode, the query in the address, title ranking and facet chip groups, each opt-in by markup, with pages on 1.4.1 markup behaving as before (section 7.29) | Owner, 2026-11-03 |
| Matrix with presets | grclibrary.ai's request 3: a 5th script, `matrix.js`, shows a preset's columns of a matrix table and hides the rest, with a deep link by `?view=` (section 7.30) | Owner, 2026-11-03 |
| Sticky scroll box | Defect: a `.sticky` header stuck only to its `.tablewrap`, so it scrolled away with the page. A sticky table now sits in a box bounded to 75 % of the viewport height that scrolls in both directions, with an optional sticky first column (section 7.31) | Owner, 2026-11-03 |
| Left-rail page | grclibrary.ai's amendment 4: an opt-in page layout, `.doc.left-rail`, with 1 sticky side list on the left in place of the right rail, so a wide table gains the width (section 7.32) | Owner, 2026-11-03 |
| Hidden content | Defect reported by cleanlanguage.ai and confirmed on aiqt.ai: `.doc section, .doc article { display: contents; }` overrode the browser's `[hidden]` rule, so the install selector (7.12) hid nothing in any version from 1.0.0 to 1.5.0. From 1.5.1, `[hidden] { display: none !important; }` applies to every element, so no component's display rule can show hidden content; every site moves to 1.5.1 | Owner, 2026-11-03 |
| Dark mode near black | grclibrary.ai reported its dark `--bg` (#1f1718 in 1.5.1) as a warm reddish near-black that is hard on the eyes; the cause was the shared warm ochre mixed into every surface. From 1.6.0 every site's dark mode is a near-black with a tint of its own hue: dark `--bg` at OKLCH lightness 0.155 and chroma 0.030, the other dark surfaces at their 1.5.1 lightness steps from it, and no warm ochre in any dark neutral. The 3 warm sites' dark hues are spread apart (red 18, orange 55, gold 95) so that red and orange stay distinguishable, and dark text and muted text are neutral on every site. 1 rule for all 6 sites with no per-site exception; secureconfig.ai's dark overrides of 1.0.0 to 1.5.1 are folded into it. Light mode is unchanged (section 6.1) | Owner, 2026-11-03 |
| Light and dark switch | A switch at the right-hand end of the top bar on every page: the page follows the device setting until the reader presses it, and the choice is stored and applied before the first paint on later pages, through a 6th script, `scheme.js` (section 7.33) | Owner, 2026-11-03 |
| Site-owned palette colours | From 1.6.0 a site may override any palette token, light and dark, in its own `site-palette.css`, loaded after `palettes.css`, with no template release (section 4, item 4). Until 1.9.0 a site overrode a palette token only at the owner's explicit request; from 1.9.1 a site sets its own colours (row "Local colour customization"). `tools/check_palette.py` is a mandatory build gate on every site, with or without a `site-palette.css`. The QA standard, from grclibrary.ai: changing a colour is 1 value in 1 file (written in each of the 2 forms of its scheme, section 4, item 4), and its QA is a before and after comparison showing that only the expected value changed, plus the checker passing | Owner, 2026-11-03 |
| Version 1.6.0 on every site | Every site moves to 1.6.0 | Owner, 2026-11-03 |
| About line wraps | Defect reported from aiqt.ai and opfiles.ai and reproduced by the template owner on `reference/landing.html` 1.6.1: at 300 to 360 px the page was 379 px wide, because the About line's links and dots had no whitespace between them, so the line could not break. From 1.7.0 the markup has 1 ordinary space after each dot, so the line breaks only after a dot and no dot starts a line, and `.region .about` carries `overflow-wrap: anywhere` as a safety net for pages on the older markup (section 7.24). The template's overflow checks include 320 px | Owner, 2026-11-04 |
| Large matrix tables | The table request for crosswalk pages, parts 1 and 2 (12 items), adopted in full and additive by markup: column widths in rem, wrapping cells, a capped pinned column (opt-in, `pin-cap`), row stripes and row hover, empty cells, the combined form documented, column toggles with a shareable state, heat shading with a legend, a column highlight, rotated headers, print rules and column groups (section 7.34). Every colour comes from palette tokens with text at 4.5:1 in both schemes, nothing rests on colour alone, there is no motion, and nothing exists only on hover. Markup without the new classes and attributes renders as in 1.6.1 | Owner, 2026-11-04 |
| Version 1.7.0 on every site | Every site moves to 1.7.0. The reason is the About line's markup (section 7.24); the matrix table features and the facet chips that would show nothing are opt-in | Owner, 2026-11-04 |
| Pinned-column cap opt-in | The capped pinned column is behind its own class, `table.sticky-col.pin-cap`; `table.sticky-col` without it renders exactly as in 1.6.1 (section 7.31) | Owner, 2026-11-04 |
| Half the window for data | At 320 px the capped pinned column leaves at least half of the window (not of the table box) for data, also at 360 and 390 px; the default cap's minimum is 8rem (section 7.34) | Owner, 2026-11-04 |
| Light heat ramp spread | The 5 light heat steps spread as far apart as the text, link and focus checks allow; the dark ramp and every other palette value unchanged (section 6.1) | Owner, 2026-11-04 |
| Even gaps around the About dots | The dot's right padding gives up the width of the space after it, so the gaps either side of each dot are equal (section 7.24) | Owner, 2026-11-04 |
| Facet chips that would show nothing | grclibrary.ai's request: per chip group, opt-in, `search.js` marks a chip that would show no documents with the current query and other choices as unavailable (`aria-disabled`, still focusable, activation refused with a reason in the status) and can show each chip's would-show count (section 7.29). Pages without the attributes behave as in 1.6.1 | Owner, 2026-11-04 |
| Long links in running text | auditthis.ai's report: a long URL used as link text in a document paragraph has no break opportunity, so at 320 to 360 px it widened the page (388 px on auditthis.ai's /method/). From 1.8.0 a link inside a paragraph, list item or definition in `.doc` may break where nothing else fits (`overflow-wrap: anywhere`), as the About line does; ordinary words still wrap at spaces | Owner, 2026-11-04 |
| Group tags in a single-group view | grclibrary.ai's report: when every visible column of a matrix belongs to 1 group, each column repeats the same group tag on 1 line and the table grows wider than its box (118 px at 1512 px). From 1.8.0 `matrix.js` sets `data-one-grp` on the table while all visible column headers share 1 `data-grp`, and the tags are not shown; the accent under each header stays, and the tags return when a second group is visible. The owner chose this over a new header class, so sites change no markup | Owner, 2026-11-04 |
| Long text in documents | The template owner's review after the long-link report: a 90-character token (a URL as plain text, an identifier, a hash) in a document paragraph, list item, definition list, heading, quotation, figure caption or callout still widened the page at 320 and 360 px (16 cases on `document.html`). From 1.8.0 those text blocks in `.doc` break a token wider than the line where nothing else fits, which also covers the links of the row above; table cells keep their own rules (section 7.10) | Owner, 2026-11-04 |
| Tables on paper | The register on `reference.html` printed 675 px of its 1219 px table, cutting off its last rows and its right-hand columns, and the matrix print rules of 1.7.0 removed only the height cap, so a matrix wider than the page ran past the paper's edge (194 px on `crosswalk.html` at A4 width). From 1.8.0 every table prints whole in both directions: no height cap or scrolling, the header row repeats, cells wrap, and each table fits the page width (section 7.34) | Owner, 2026-11-04 |
| Chip list indent | A chip list inside `.doc` took the 1.2rem list indent of `.doc ul`, so chips started about 19 px right of the text edge. From 1.8.0 `ul.chips` starts at the text edge (section 7.29) | Owner, 2026-11-04 |
| Heat steps apart | The light heat steps of 4 sites measured 1.8 to 2.3 apart (OKLab x 100), near the figure of about 2 that the 1.6.0 changelog takes as the side-by-side difference most readers notice, because the focus ring's 3:1 on every step limited the ramp. From 1.8.0 the focus ring inside a heat cell is drawn in `--text`, which every step holds at 4.5:1, so `--focus` no longer limits the light ramp; the light ramp's chroma is capped at 0.10, so no step is a fluorescent tint; neighbouring steps must lie at least 2.5 apart, a floor the generator and `check_palette.py` enforce. Only the light `--heat-1` to `--heat-4` change; the dark ramp and every other palette value keep their 1.7.0 values (section 6.1) | Owner, 2026-11-04 |
| Overflow check for the sites | The About line and long-link defects were each found by a site after publication. From 1.8.0 the package carries `tools/check_overflow.js`, which loads a 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; recommended in each site's checks, not mandatory, because it needs Node.js and Playwright (section 4, item 3) | Owner, 2026-11-04 |
| Version 1.8.0 | 1.7.1, prepared with the 2 corrections, was not published; 1.8.0 carries them with the changes above. Stylesheet, script, palette and tool changes only, with no markup change for any site; sites may move at their next update, and auditthis.ai and grclibrary.ai, which reported the corrections, move | Owner, 2026-11-04 |
| Copying scripts and tools | auditthis.ai's report on adopting 1.8.0: section 4, item 2 listed every script to copy while each script is opt-in per page, and it said never to copy the other files of `tools/` while item 3 recommends `check_overflow.js`. From 1.8.1 item 2 says that a site may leave out a script that none of its pages loads, and names `tools/measure.js` as the tool never to copy, as the README already did | Owner, 2026-11-04 |
| Version 1.8.1 | Documentation only: `SPECIFICATION.md`, `README.md` and `CHANGELOG.md` change, and every runtime file, tool and reference page is byte-identical to 1.8.0. Sites need not move, and no notice is sent | Owner, 2026-11-04 |
| Link-preview card builder | The card hand-over from auditthis.ai (2026-11-05): link-preview cards were made by a 1-round script whose site details were in code and whose text font depended on the host. From 1.9.0 the package carries `tools/build_card.js`, which builds each site's 1200 by 630 card, a proof image, the head tags, the touch icon, a before and after preview page and a report from 1 data file and 1 approved illustration, with the round-2 layout approved on 2026-10-31, and fails on fit, contrast, input and repeatability checks (section 7.35). It builds images from approved art; logos and illustrations stay the owner's to approve | Owner, 2026-11-05 |
| Card font | The builder renders with Inter 4.000, bundled in `tools/card-fonts/` (SIL OFL 1.1) and never served: round 2 rendered with Inter on its host, so the approved look is kept. Public Sans would have changed every card | Owner, 2026-11-05 |
| Card colours | Each site's word colour and background stay data in its own `site-card.toml`, with the round-2 values; they are not derived from palette tokens | Owner, 2026-11-05 |
| Card packaging | The builder lives in `tools/`; a site copies it, with its fonts, to `.baseline/`, pins it, runs it locally where Node.js and Playwright are installed, and commits the output. A new site needs no template release | Owner, 2026-11-05 |
| Cards replaced only after approval | Every site regenerates its card with the builder, and no site replaces its live card until the owner approves a before and after preview: on a preview branch at `https://demo.<project>.pages.dev/cardpreview.html`, or inline in the owner's Claude session for auditthis.ai and posluns.ca (section 7.35) | Owner, 2026-11-05 |
| Chip states (1.9.1) | The owner's request: on grclibrary.ai/documents an available chip and an unavailable one differed only in border style (solid against dashed), both in `--text-muted`, and in light mode the difference was not seen at a glance. From 1.9.1 an available chip reads in `--text` with a `--text-muted` border. An unavailable chip is greyed out so the eye skips it: its word is struck through (its count is not), and in light mode it sits on a `--rule` fill in `--text-muted` with no visible border (3.65:1 to 3.68:1 on the fill; an inactive control is exempt from the 4.5:1 text rule, and the gate holds it at 3:1); in dark mode it has no fill, `--text-muted` text and a dashed `--rule-strong` border. The owner chose the greyed-out look after reviewing a darker, a white and 3 grey fills for available chips. A site using the chips that would show nothing should also turn on live counts, so an unavailable chip reads 0 (section 7.29) | Owner, 2026-11-05 |
| Local colour customization | The template gives a baseline, not a constraint (owner). From 1.9.1 a site may override, on its own decision and with no template release, any palette token (section 6.1), family token (section 6.2, except the print tokens) or component token (section 6.4) in its `site-palette.css`, in both forms of each scheme. The palette gate checks every pair and is mandatory; the QA standard of the row above still applies. A site still adds no CSS rules of its own | Owner, 2026-11-05 |
| Card stray pixels (1.9.2) | auditthis.ai's report: its card from `build_card.js` 1.9.0 had 13 isolated grey and blue pixels at the centre of the top-left background gradient, outside the safe area, which no check looked at. The full-colour render was clean; the 256-colour step caused them, because the dithering carried its error forward until it reached a distant palette colour. From 1.9.2 a dithered pixel stays within 24 levels of its own colour, and a new check fails a card whose background outside the safe area differs from the full-colour render by more than 32 levels (section 7.35). Cards built with 1.9.0 or 1.9.1 are rebuilt with 1.9.2 before approval | Template owner, 2026-11-05 |
| Cards in the Teams crop (1.10.0) | The owner's observation: Microsoft Teams showed posluns.ca's card cropped to its centre, cutting the wordmark and the emblem. Measured from that preview by matching it to the card: Teams keeps the full height and shows the centre 784 px of the 1200 px width (x 206 to 990 in that preview, within 2 px of the centred x 208 to 992 the builder uses). No page tag gives Teams a different image. From 1.10.0 a site may set `layout = "centre"`, which keeps every glyph and the illustration at least 40 px inside that crop on every platform's full image; the side layout stays the default; the builder reports, for either layout, what Teams would cut, and the preview page shows the before and after cards as Teams crops them, so the owner judges the crop with the card (section 7.35). The owner chose an option over a new default | Owner, 2026-11-05 |
| Builder version label (1.10.0) | auditthis.ai's report: `build_card.js` 1.9.2 named itself "baseline 1.9.0" in `card-report.txt` and `card-tags.html`, while the report is the record of which builder made a card. From 1.10.0 the builder takes the label from 1 constant, `VERSION`, and the release checks fail when it differs from the package's version | Template owner, 2026-11-05 |
| vendorreplies.ai joins the family (1.11.0) | The owner's decision of 2026-11-06: vendorreplies.ai becomes the seventh site. It is always the last project card in the family footer, after the 6 rotated rainbow cards, on every site including its own, outside the 6-stop rainbow; its band is a deep metallic silver like CONAN's armour (`--band-silver`, `--band-sheen`, `--band-silver-deep`), the last rainbow card fades into it, and the grey tail starts from it (section 7.19). Its logo is CONAN's crowned head, cut from the site's own touch icon (`logos/vendorreplies.png`, under the CC0 grant as artwork; NOTICE.md). It keeps its own link-preview card, not built by `build_card.js`. `gen_palettes.py` gives it a palette at its coral's hue (OKLCH 34) under `EXTRA_SITES`; the 6 existing palettes are unchanged. The owner chose the icon after reviewing 4 crops of its art, lightened versions and a cut-out of the full figure | Owner, 2026-11-06 |
| Footer with 7 names (1.11.0) | With 7 named cards the names clip at every scaled size below 1493 px of content width, so 1.11.0 removes stage B (names at scaled sizes from 1040 to 1399 px): the names show at full size from 1500 px of content width and the logos only below it, with the project and mark tiles sharing the width 7 to 3. Found on the owner's review of the mock-ups, where the names were cut at a 1440 px window | Template owner, 2026-11-06 |
| Banner, bands and card grid (1.11.0) | For vendorreplies.ai's page, available to every site: a banner (a full-width picture under the masthead with the title and tagline set over it, section 7.36), bands (full-width sections, plain, tinted or dark, section 7.37) and a card grid (short boxes side by side, stacking on phones, section 7.38). The owner chose template components over a local exception, and bands and a card grid over the template's existing equivalents | Owner, 2026-11-06 |
| Line width (1.11.0) | The owner's decision: "make both options available, keeping current settings for the project sites, and allowing each to set custom per text box/paragraph". The reading widths stay the default; `.measure-full`, `.measure-wide`, `.measure-narrow` and `.measure-read` set the width on a page, a section or a single block, and `.doc.no-rail` gives the reserved rail column to the content on a page with no rail (section 7.5) | Owner, 2026-11-06 |
| Stacked and framed pair (1.11.0) | The owner's review of vendorreplies.ai's inbound message and reply: side by side, the reply was tall and narrow and the inbound box mostly empty. `.pair-stack` puts the 2 boxes one above the other at every width, each as tall as its content, and `.msg` frames quoted text on the same inner panel as a code block (section 7.18). The default pair is unchanged | Owner, 2026-11-06 |
| Box colours (1.11.0) | The owner's decision: the colours of the new boxes can be changed without a template update. The bands, the banner frame, the before-and-after boxes and the card grid read component tokens with the 1.10.0 colours as defaults, and the palette gate checks each (section 6.4) | Owner, 2026-11-06 |
| Drafts on a branch (1.11.0) | The owner's decision of 2026-11-06: website drafts for review are staged at `https://draft.<project>.pages.dev/draft/`, the Cloudflare Pages deployment of a `draft` branch, not at `/draft/` on the live site (section 4.1) | Owner, 2026-11-06 |
| Footer names from 1280 px (1.11.1) | The owner's decision: "the smallest reasonable screen on a computer is probably 1280 x 1024; below that, it's a mobile device". A compact bar (band 0.4rem, logos 1.9rem, marks 1.5rem, names 0.85rem, signature 1.25rem; tiles kept at 2.75rem for 44 px targets); names above 1159 px of footer width, on 1 line with ".ai" beside them; while they show, the 3 marks grouped beside the signature (the owner's proposal) and "A" and "Project" shown from 1240 px; the current tile's gold line at every width (the owner's choice of 5 options) | Owner, 2026-11-06 |
| Band spacing (1.11.1) | vendorreplies.ai asked for its bands' spacing at half the default. The owner chose a site-settable token over a local CSS override or a change for every site: `--band-pad`, set in a site's `site.css`, defaulting to the 1.11.0 spacing | Owner, 2026-11-06 |
| Warm ink (1.11.2) | vendorreplies.ai asked for a second, warm accent in running text. The owner approved a component token (2026-11-06): `span.warm` reads `--ink-warm`, declared in `baseline.css` inside `:where(:root)` and defaulting to `var(--accent)` (the request's `var(--brand)` misses the 4.5:1 text floor on 6 of the 7 sites in light mode on the grounds the gate checks, and `--brand` is never used for text, section 7.25), so a site sets its own warm value in its `site-palette.css` and the gate holds it at 4.5:1 as text on every surface and tint it lists (section 6.4) | Owner, 2026-11-06 |
| Card renderer (1.11.2) | Playwright 1.63's default headless launch uses its chromium-headless-shell build, not the full Chromium build the recorded values were reproduced with, and another build may draw text edges differently. From 1.11.2 the builder resolves `chromium.executablePath()` once, launches both browsers with exactly that executable, records the launched executable's name next to the version, and refuses a headless-shell build before rendering; the release checks fail on a headless-shell build, or on a 1.11.2-or-later report without a build, before comparing a metric. The builder uses Playwright's full Chromium build; the recorded values were reproduced with Chromium 153.0.8010.12 (Chrome for Testing, Playwright 1.63.0) | Owner, 2026-11-07 |
| Security headers on all 7 sites (1.11.3) | The owner's direction of 2026-11-07: every site, vendorreplies.ai included, has consistent security headers and configuration (section 9); section 9 records it beside the 2026-11-03 decision, which stands | Owner, 2026-11-07 |
| Heading ink (1.11.3) | vendorreplies.ai asked for a settable colour for the `h2` and `h3` headings of its plain and tinted bands. The owner approved a component token (2026-11-07): those headings (an `h3` inside a card-grid box in such a band included) read `--heading-fg`, declared in `baseline.css` inside `:where(:root)` and defaulting to `var(--text)`, the colour they inherited until 1.11.2, so with the token unset nothing changes; a dark band's headings keep `--band-dark-heading`. A site sets it in its `site-palette.css`, in both forms of each scheme, and the gate holds it at 4.5:1 as text on `--bg` and `--band-tint-bg` (section 6.4) | Owner, 2026-11-07 |
| Card metal wordmark (1.11.5) | An optional field of the card's data file, off by default, so a card without it is byte-identical to 1.11.4's: `word_style = "metal"` (centred layout only) sets the wordmark in the posluns.ca series v6 gradients (section 7.35) | Owner, 2026-11-07 (D-054) |
| Centred card background (1.11.5) | A background for the centred layout darkens the whole safe area, above any colour splotch; the centred variant of the posluns.ca series background ("wide": its 2 darkening ellipses on top, at 640 by 480 and 800 by 580 px) passes on all 6 series cards (section 7.35). The builder and its contrast rule are unchanged | Owner, 2026-11-07 (D-054); D-057 by the vote panel under the unattended procedure, 2026-11-08 |
| Dancing Script from upstream (1.11.5) | From 1.11.5 the package carries the unmodified upstream file of Dancing Script 2.001 (google/fonts commit f8dc91f6f13564fd038c60c02e82f305be8c551d, `ofl/dancingscript/DancingScript[wght].ttf`, SHA-256 `21808625578fe8d8cd10cb684be546dca077b27cd03a53a2f1ec11dc743c924c`, 133,636 bytes), as the served font (`fonts/dancing-script-wght-normal.ttf`, in TrueType: the OFL counts a change of format as a Modified Version, and a Modified Version may not use the Reserved Font Name "Dancing Script"). Earlier versions served a latin-subset WOFF2 of it; published versions stay as they are | Vote panel under the unattended procedure, 2026-11-08 (D-058) |
| NodeKeeper uses the template (1.12.0) | The owner's decisions of 2026-11-08: NodeKeeper (nodekeeper.ai), a tool, gets a palette like vendorreplies.ai's, outside the rainbow: no rainbow stop, column-group accent or slot (D-066); a teal at OKLCH hue 195, accent chroma 0.110, `--brand` `#06999a` light and `#29bbbb` dark. Its logo is a graphics card with 2 fans and an edge connector, 5 files like the 6 rainbow marks, the card variant in the family's card colours (D-067). It has no card in the family footer, whose markup is unchanged (D-068); its own site shows the shared family footer as is, the 6 rainbow cards and vendorreplies.ai's, with no card of its own (owner, 2026-11-08, attended, D-075; section 7.19). No site is notified of 1.12.0 until NodeKeeper's release (D-064) | Owner, 2026-11-08 (D-064, D-066 to D-068, D-075) |
| One family table (1.12.0) | `gen_palettes.py` takes every site's design values from 1 table, `FAMILY`, so a new site is 1 row, and a rainbow site takes a hue between 2 neighbours without a change to any other site's colours (section 6.1, "Adding a site"). The `--brand` colours of any 2 sites lie at least 5.5 apart (OKLab distance x 100) in each scheme; the closest pair, secureconfig.ai and vendorreplies.ai, is 5.8 apart in dark mode | Owner, 2026-11-08 (D-061 and the 5.5 minimum) |
| Icons for every site (1.12.1) | The owner's ruling of 2026-11-09 on the drafted icons (D-079, verbatim): "approved, and once 1.12.1 is released inform the orchestrators with web sites". The icons: a `favicon.ico` for every site, 16, 32 and 48 px frames each rendered from the site's SVG favicon at its own size, served at the site's root as `/favicon.ico` beside the SVG favicon and touch-icon links, which stay; and, for vendorreplies.ai, a 180 by 180 touch icon (the 168 px head unscaled on navy `#0b121c`, the background of the site's own touch-icon source) and 32 and 16 px PNG favicons, with a `favicon.ico` of the same 3 sizes. vendorreplies.ai has no SVG favicon and no mono mark: its logo is photographic artwork, and a vector or single-colour version would be a redrawing, not a copy (section 5). Once 1.12.1 is published, a notice goes to every orchestrator with a web site | Owner, 2026-11-09 (D-079) |
| Icons built by each site (1.12.2) | The owner's decision of 2026-11-09 (D-080, 17:30Z), asked whether 1.12.1 should have let each site make its own `favicon.ico` with its card builder, which already makes the touch icon, instead of shipping ready-made files; the option chosen, verbatim: "1.12.2: builder makes them (Recommended)". From 1.12.2 `tools/build_card.js` also writes `<site>-favicon.ico` (16, 32 and 48 px frames, each rendered from the site's `<site>-favicon.svg` at its own size in the builder's Chromium) from the data file's `favicon` key, and, for vendorreplies.ai, which has no SVG favicon, the `.ico`, the PNG favicons and the touch icon from `vendorreplies.png` (section 7.35); on the recorded Chromium and zlib its files equal 1.12.1's byte for byte. 1.12.1's ready-made files in `logos/` and `tools/build_icons.py` stay in the package as a reference only, and no site copies them (section 4, item 2). The sites were told on 2026-11-09 to hold, and get a notice to take 1.12.2 and use the builder | Owner, 2026-11-09 (D-080) |
| The card data-file template (1.12.3) | auditthis.ai asked on 2026-11-10 whether a site copies and pins `tools/site-card.example.toml`: section 11's lists of files to replace named it, while section 4, item 2 never said to copy it. posluns answered the same day: leave it out. The file is the template of the card builder's data file; the builder reads the site's own `site-card.toml`, written from it. From 1.12.3 section 4, item 2 says a site does not copy the template, and section 11 says that a site that kept a copy from an earlier version replaces it, or deletes it and its pin entry (finding SPEC-S11-EXAMPLE-TOML) | posluns, 2026-11-10, answering auditthis.ai |
| The NOTICE entry's third exception (1.12.4) | cleanlanguage.ai asked on 2026-11-12 that the NOTICE entry of README adoption step 7, which said "with 2 exceptions" (the fonts and the marks), also name the project names and domain names, which `NOTICE.md` item 3 of 1.12.3 kept with their projects, so that a site's NOTICE carries a true count; cleanlanguage.ai's gate requires that entry to equal step 7 exactly. The owner's decision of 2026-11-12 (D-094): from 1.12.4 the entry names 3 exceptions (section 4, item 8), and line 1 of `tools/site-card.example.toml` says that the file is a template a site does not copy (the wording deferred from the 1.12.3 QA). The vote panel's ruling of the same day (D-095): the entry's font exception also names the card builder's Inter fonts, which `NOTICE.md` item 4 licenses under the SIL Open Font License 1.1, so the count stays 3; the entry names them "where present" (after QA round 2). The owner's ruling of the same day (D-096): `NOTICE.md` item 3 and the entry say that the project names and domain names are the projects' own names and marks, which CC0 1.0 does not license (its section 4(a)), in place of the 1.12.3 wording. The vote panel's ruling of the same day (D-098): the entry names no path, because the template only proposes `assets/baseline/` (section 4, item 2) and a repository may keep its copies elsewhere; each font's licence file travels with its font files, and the marks' terms are recorded in the `NOTICE.md` that travels with the marks, so the same entry is true in every repository | Owner, 2026-11-12 (D-094, D-096); vote panel, 2026-11-12 (D-095, D-098) |
| Block spacing (1.13.0) | vendorreplies.ai asked on 2026-11-12 for block spacing in line-height units across the template, prompted by a 43.2 px gap under a download line. The owner's ruling of the same day (D-097): no gap grows ("gaps are mostly right or should be smaller"). The cause: `.doc` is a grid and a download block is a grid item, so the margin under its last line did not collapse into the 1.6rem margin under the block, and the 2 added up. From 1.13.0 the last element of a download block in a document ends flush with the block, so the space under a block is 1.6rem (25.6 px) whatever that element is; a block whose last element is hidden keeps the margin of its visible last line, so its gap stays as in 1.12.4 (section 7.11). A site may decrease 3 spacing tokens in its `site.css`, each defaulting to the 1.12.4 value: `--para-gap` (1.1rem, the space under a paragraph, list, quotation or details block in a document), `--download-after` (1.6rem, the space under a download block) and `--download-line-gap` (the paragraph gap, the space under the file line where more lines follow it) (sections 4, 7.5 and 7.11). `baseline.css` clamps each token between 0 and its default with `max(0px, min())`, and `--download-line-gap` between 0 and the paragraph gap in effect, so a larger value has no effect and a negative value gives 0 (a negative gap would make blocks overlap). The file line's token sets no margin of its own: it passes through the paragraph rule, so wherever a box sets its own paragraph margin (a callout, 0.8rem; a pair, 0) the file line keeps it, as in 1.12.4. Two earlier forms let a gap grow: a rule for every download block in a document raised a file line inside a callout from 0.8rem to 1.1rem (found by the 1.13.0 release check), and a rule scoped to the grid's level, by its higher specificity, raised the file line to the size line on a `section` that carries a box class (`.pair`, 4.8 to 17.6 px; `.callout`, 12.8 to 17.6 px; 1.13.0 QA round 1). Rendered before and after on the reference pages and on vendorreplies.ai on 2026-11-12 | Owner, 2026-11-12 (D-097) |
| Card fonts only where a card is built (1.13.0) | `tools/build_card.js --icons-only` returns before it reads `card-fonts/`, while section 4, item 3 and README adoption step 3 told every site that holds the builder to copy `tools/card-fonts/` (recorded as open in 1.12.4 QA round 3). The owner's ruling (D-103): from 1.13.0 a site copies `tools/card-fonts/` only where it builds a card, and a site that runs the builder only with `--icons-only` may delete `.baseline/card-fonts/` and its pin entries. Checked on 2026-11-12: a copy of the 1.13.0 builder with no `card-fonts/` beside it wrote auditthis.ai's `favicon.ico` with `--icons-only`, byte-identical to `logos/auditthis-favicon.ico`. Documentation only: the builder is byte-identical to 1.12.4's apart from its version label (sections 4 and 11) | Owner, 2026-11-12 (D-103) |
| Address fragments `rail.js` cannot decode (1.13.0) | `rail.js` decoded the address fragment with `decodeURIComponent` unguarded, so a page opened at a fragment that is not valid percent-encoding (for example `#%E0`) threw a URIError on load and marked no rail link until the reader scrolled. Reported by vendorreplies.ai and verified by posluns. From 1.13.0 such a fragment matches no section, so the reading line decides, as for any fragment that names no listed section, and a rail link whose fragment is not valid percent-encoding (`href="#%E0"`), which stopped the whole script on load, is skipped while the other links work (1.13.0 QA round 1) (section 7.6) | posluns, 2026-11-12, on vendorreplies.ai's report |
| Version 1.13.0 | The block spacing of D-097: `baseline.css` changes 3 margins to read `--para-gap` and 1 to read `--download-after`, each clamped between 0 and its default with `max(0px, min())`, passes `--download-line-gap` to the paragraph margin of a download block's file line, and ends the last element of a download block in a document flush with the block. With the tokens unset, every computed style is as in 1.12.4 except that bottom margin, so the space under a download block in a document falls to 1.6rem (from 43.2 px under a paragraph or file line, from 40 px under a size line or checksum), and the content below it moves up by that amount. Every other gap is unchanged, and no gap grows. `rail.js` treats an address fragment it cannot decode as naming no section, and skips a rail link whose fragment it cannot decode. A site copies `tools/card-fonts/` only where it builds a card (D-103). `baseline.css`, `rail.js`, the card builder's version label and the documents change. Every site that shows a download block in a document sees the smaller gap when it moves (section 11) | Owner, 2026-11-12 (D-097, D-103); posluns, 2026-11-12 (`rail.js`) |
| Version 1.12.4 | Documentation only: the NOTICE entry's third exception, the Inter fonts in its font exception, and an entry that names no path (README adoption step 7; sections 3, 4 and 11). `SPECIFICATION.md`, `README.md` and `CHANGELOG.md` change, with `NOTICE.md` (item 3, D-096), the `baseline.css` header comment and the card builder's version label, which name the version, lines 1 and 2 of `tools/site-card.example.toml`, comments, and `SHA256SUMS`; every other file is byte-identical to 1.12.3, and every reference page, card and icon is as in 1.12.3. Sites need not move; a site that moves replaces `baseline.css` (its header comment), `NOTICE.md` (item 3) and `tools/build_card.js` (its version label), and the site-template entry in its NOTICE; a site that kept a copy of `tools/site-card.example.toml` from an earlier version replaces it, or deletes it and its pin entry (section 11) | Owner, 2026-11-12 (D-094, D-096); vote panel, 2026-11-12 (D-095, D-098) |
| Version 1.12.3 | Documentation only: whether a site copies `tools/site-card.example.toml` (sections 3, 4, 7.35 and 11; finding SPEC-S11-EXAMPLE-TOML, 2026-11-10). `SPECIFICATION.md`, `README.md` and `CHANGELOG.md` change, with the `baseline.css` header comment and the card builder's version label, which name the version, and `SHA256SUMS`; every other file is byte-identical to 1.12.2, and every reference page, card and icon is as in 1.12.2. Sites need not move (section 11) | posluns, 2026-11-10 |
| Version 1.12.2 | The icons of D-080: `tools/build_card.js` writes `<site>-favicon.ico` from a new optional `favicon` key, accepts a PNG logo or favicon (vendorreplies.ai), and gains `--icons-only`, which writes the icons without a card; `tools/site-card.example.toml` documents the key; the `baseline.css` header and the builder's version label change; the guidance on `logos/` and `/favicon.ico` changes (sections 3, 4, 5, 7.35 and 11); and the About line row and the shared header policy name no count of sites (D-050). No CSS rule, token, class, logo file or reference page markup changes, and every reference page renders as in 1.12.1. With no `favicon` key and an SVG logo every card and touch icon is byte-identical to 1.12.1's. A site that moves replaces its copied `favicon.ico` with its build (section 11) | Owner, 2026-11-09 (D-080) |
| Version 1.12.1 | The icons of D-079: 11 files in `logos/` (a `<site>-favicon.ico` for each of the 8 sites; `vendorreplies-touch.png`, `vendorreplies-favicon-32.png` and `vendorreplies-favicon-16.png`) and `tools/build_icons.py`, which makes them; the `baseline.css` header, `NOTICE.md` and the card builder's version label change. No CSS rule, token, class or reference page markup changes, and every reference page renders as in 1.12.0. Every site adds `/favicon.ico`; vendorreplies.ai also changes its icon links (section 11) | Owner, 2026-11-09 (D-079) |
| Version 1.12.0 | NodeKeeper's palette and logos and the family table: `palettes.css` gains the 4 `.site-nodekeeper` blocks and every other token keeps its 1.11.5 value; `gen_palettes.py` is rewritten round the table, with the slot helper (`--slots`) and the minimum `--brand` distance; the `palettes.css`, `gen_palettes.py` and `baseline.css` headers name no count of sites; 5 logo files are added. No markup changes, and every reference page renders as in 1.11.5. No site needs to change | Owner, 2026-11-08 |
| Version 1.11.5 | The 1.11.4 QA corrections (D-052: the builder's `--before` header, the CHANGELOG's `SHA256SUMS` sentence, notices that list the files per starting version, and a release check of the report's inputs), the content guidelines 1.5 (7 project sites; D-050), the `baseline.css` header naming 1.11.5, the card options of D-054: the optional metal wordmark and the rule for a centred background, and Dancing Script from upstream, served. With the metal wordmark off every card is byte-identical to 1.11.4's; the reference pages render as in 1.11.4 | Owner, 2026-11-07 (D-052, D-054); 2026-11-08 (D-057 and D-058, vote panel) |
| Version 1.11.4 | Corrections only, with no new token, no markup change and no rendering change: the card builder's dithering comment describes the code (the fallback is the palette colour nearest the pixel's own, which can lie more than 24 levels away; the 1.9.2 row above stands as written and section 7.35 is corrected), its report lists the palette CSS it reads for the touch icon, a `--before` image with a malformed IHDR length fails with the builder's own message, the `--heading-fg` selector keeps the headings of a dark band nested inside a plain or tinted band (markup section 7.37 forbids) on `--band-dark-heading`, and section 1 records vendorreplies.ai's repository and the count of the project repositories; only `baseline.css`, `tools/build_card.js` and the documents change, and every reference page renders as in 1.11.3. Sites move at their next update; card rebuilds are not requested | Owner, 2026-11-07 (D-035) |
| Version 1.11.3 | Corrections and 1 new optional component token, with no rendering change while `--heading-fg` is unset: the palette gate's missing-token hint names the version that introduced the token (1.11.0 for the band, banner, pair and box tokens; 1.11.3 for `--heading-fg`), the `baseline.css` header names this version and the 7 sites and the `palettes.css` header names the 7 sites, the card builder's version label reads 1.11.3 and it rejects a `--before` image not in the exact proportion 1200:630 (exit 1, before the image is decoded, a browser is launched or a file is written), the `h2` and `h3` headings of plain and tinted bands read `--heading-fg` (default `var(--text)`; requested by vendorreplies.ai), and the release checks harden the card build gate, add an HTTP status check to `t_v1111.js`'s page comparison and fail `t_compat.js` on console or page errors; `baseline.css`, `palettes.css`, `gen_palettes.py`, `tools/check_palette.py`, `tools/build_card.js` and the documents change. With `--heading-fg` unset every reference page renders as in 1.11.2. Section 9 records the owner's direction that every site, vendorreplies.ai included, has consistent security headers and configuration. Sites move at their next update; card rebuilds are not requested | Owner, 2026-11-07 |
| Version 1.11.2 | The warm inline ink `span.warm` and its component token `--ink-warm`, and the card builder's full-Chromium launch; `baseline.css`, `tools/check_palette.py`, `tools/build_card.js` and the documents change. With `--ink-warm` unset every reference page renders as in 1.11.1. A site that adopts the warm ink replaces `baseline.css` and `tools/check_palette.py` together; no markup changes | Owner, 2026-11-06 (the warm ink) and 2026-11-07 (the card builder's full-Chromium launch) |
| Version 1.11.1 | The footer names from a 1233 px window, the grouped marks, the current tile's line at every width and the `--band-pad` token; `baseline.css`, the card builder's version label and the documents change. Every reference page renders as in 1.11.0 apart from the footer. Every site moves, by replacing `baseline.css` | Owner, 2026-11-06 |
| Version 1.11.0 | vendorreplies.ai's seventh footer card and its icon, the footer stages for 7 names, the banner, bands, card grid, line-width options and the stacked and framed pair, with their component tokens; `baseline.css`, `palettes.css`, `gen_palettes.py`, `contrast-report.txt`, `tools/check_palette.py`, the footer of every reference page, `reference/banner.html` and its sample palette and pictures, `logos/vendorreplies.png`, the card builder's version label and the documents change. Every reference page is pixel-identical to 1.10.0 apart from the footer. Every site moves, for the seventh footer card; the other components are optional | Owner, 2026-11-06 |
| Version 1.10.0 | The centred card layout, the Teams crop in the report and the preview, and the version label; only `tools/build_card.js`, `tools/site-card.example.toml` and the documents change, and every runtime file, logo and reference page is byte-identical to 1.9.2. A side-layout card is pixel-identical to 1.9.2's. Every site that builds its card uses 1.10.0, chooses its layout with the owner, and rebuilds any card made with an earlier builder before approval, so that the report names the builder that made it | Owner, 2026-11-05 |
| Version 1.9.2 | The card builder correction; `tools/build_card.js` and the documents change. Every site that builds its card uses 1.9.2 | Template owner, 2026-11-05 |
| Version 1.9.1 | Chip states and component tokens: `baseline.css`, `tools/check_palette.py`, `site-palette.example.css` and the documents change. Sites may move at their next update; grclibrary.ai, whose page prompted the change, moves | Owner, 2026-11-05 |
| Version 1.9.0 | Adds the card builder, its fonts and an example data file; every runtime file, logo and reference page is byte-identical to 1.8.1. Every site moves, to regenerate its card; opfiles.ai, which has no approved card illustration, builds its card once the owner approves one | Owner, 2026-11-05 |

## 3. The package

Published at `https://posluns.ca/baseline/<version>/`, source in `site/baseline/<version>/` of jposluns/posluns_site, whose deploy fails if any file differs from the version's `SHA256SUMS`. A published version never changes; any change is a new version. The files are the ones the mockups use, under their final names:

| Path | Content | Mockup source |
|---|---|---|
| `baseline.css` | The whole shared stylesheet: reset, typography, layout, every component, the container query stages and the fallback. Reads only the tokens in `palettes.css` and the tokens it declares itself (section 6). The review chrome and the `/drafts/` font path are removed. | `site/drafts/atrium/template.css` |
| `palettes.css` | The site palettes, 8 from 1.12.0 (light, dark, and the `[data-scheme]` forced variants that the switch sets) and the cyclic rainbow (`--rainbow-N`, `--rainbow-join-N`, `--rainbow-ink-N`, `--rainbow-band`, `.rainbow`, `.slice-N`). Generated by `gen_palettes.py`, which exits non-zero if any text pair misses WCAG 2.2 AA; `contrast-report.txt` is its output. From 1.6.0 the dark neutrals take the site's hue alone (section 2, "Dark mode near black"). From 1.7.0 each palette also carries the matrix tokens (section 6.1). | `site/drafts/assets/palettes.css`, `scripts/drafts/palettes/` |
| `site-palette.example.css` | From 1.6.0: the template of a site's own `site-palette.css`, showing 1 dark `--bg` override in both dark forms (section 4, item 4). | Written for the package |
| `tools/check_palette.py` | From 1.6.0: the contrast gate every site runs in its build (section 4, items 2 and 4). Python 3 standard library, no network. Reads `palettes.css` and an optional `site-palette.css`, resolves the tokens of 1 site in the 6 states a page can be in (device light or dark; no `data-scheme`, `light` or `dark`), checks every pairing of `contrast-report.txt` at the same thresholds (from 1.7.0 the matrix pairs too), and checks that the states of each scheme agree. Exit status 0 on pass, 1 on any failure or on input it cannot read or evaluate, 2 on a usage error. | Written for the package |
| `tools/build_card.js`, `tools/card-fonts/`, `tools/site-card.example.toml` | From 1.9.0: the link-preview card builder, its 3 Inter font files with their licence, and the template of its data file (section 7.35), from which a site writes its own `site-card.toml`; the template is not copied (section 4, item 2). Node.js 18 or later with the `playwright` package and its full Chromium build (`npx playwright install chromium`; from 1.11.2 the builder launches the executable `chromium.executablePath()` resolves and refuses a headless-shell build); no other dependency, no network. Exit status 0 on pass, 1 on any failed check or unreadable input, 2 on a usage error. From 1.12.2 it also writes the site's `favicon.ico` (the data file's `favicon` key) and, with `--icons-only`, the icons without a card (section 7.35; D-080). | Written for the package; Inter 4.000 by The Inter Project Authors, SIL OFL 1.1 |
| `tools/build_icons.py` | From 1.12.1: makes each `<site>-favicon.ico` from `<site>-favicon.svg` (each frame rendered at its own size in headless Chrome, never downscaled from a larger frame) and vendorreplies.ai's `.ico`, PNG favicons and touch icon from `vendorreplies.png`; the files in `logos/` are its output, made with Google Chrome 157.0.8103.38 and Pillow 12.1.1, with which it reproduces them byte for byte. Python 3 with Pillow, and Google Chrome or Chromium (`--chrome`); no network. Exit status 0 on success, 1 on a failed render or an unreadable input, 2 on a usage error. From 1.12.2 a reference only: `tools/build_card.js` makes each site's icons (D-080), and the script stays so that the reference files in `logos/` can be reproduced. Never copied by a site. | Written for the package |
| `tools/check_overflow.js` | From 1.8.0: the sideways-scroll check (section 4, item 3). Node.js 18 or later with the `playwright` package and its Chromium. Loads each named page of a locally served site at 320, 360, 390 and 1512 px (`--widths`) in the light and dark schemes (`--schemes`), and fails a page that is wider than the window, has a `.tablewrap` box past the window edge, throws a script error or answers with an HTTP error. Requests to any origin other than `--base` are refused, so it never reaches the network. Exit status 0 on pass, 1 on any failure or on a page it cannot load, 2 on a usage error. | Written for the package |
| `fonts.css`, `fonts/*.woff2`, `fonts/dancing-script-wght-normal.ttf`, `fonts/*-LICENSE.txt` | The 4 families the Atrium uses, variable weight: Newsreader (text and headings, upright and italic), Public Sans (labels, navigation, UI, upright and italic) and JetBrains Mono (code, upright and italic), latin subsets in WOFF2, and Dancing Script (the signature, upright), from 1.11.5 the unmodified upstream TrueType file of version 2.001 (section 2, "Dancing Script from upstream"; until 1.11.4 a latin-subset WOFF2). Each with its SIL OFL 1.1 licence file. The Dancing Script `@font-face` moves from `template.css` into `fonts.css`. | `site/drafts/assets/fonts.css`, `fonts/` |
| `copy.js` | Copy to clipboard for prompt and command blocks. | `site/drafts/atrium/copy.js` |
| `select.js` | The install selector (aiqt.ai, cleanlanguage.ai). | `site/drafts/atrium/select.js` |
| `search.js` | Client-side filter for a list already on the page, with facets and a results-only mode from 1.5.0 (grclibrary.ai), and chips that would show nothing marked unavailable, with live counts, from 1.7.0 (opt-in). | `site/drafts/atrium/search.js`, extended in the package |
| `rail.js` | The rail mark that follows the reader (from 1.3.0). | Written for the package |
| `matrix.js` | Column presets for a matrix table (from 1.5.0, grclibrary.ai's crosswalk); column toggles and a column highlight (from 1.7.0). | Written for the package |
| `scheme.js` | The light and dark switch in the top bar (from 1.6.0); loaded in `<head>` without `defer` (section 7.33). | Written for the package |
| `logos/<site>.svg`, `<site>-mono.svg`, `<site>-favicon.svg`, `<site>-card.svg`, `<site>-touch.png` | The 6 rainbow marks and, from 1.12.0, NodeKeeper's, 5 files each: full colour (hero, rail and footer), single colour, favicon, link-preview card, and the Apple touch icon (180 by 180 opaque PNG: the full-colour logo at 144 px centred on the site's light `--bg`, rendered with headless Chromium). From 1.11.0 also `vendorreplies.png`, vendorreplies.ai's footer icon (168 by 168, transparent). From 1.12.1 also `<site>-favicon.ico` for each of the 8 sites, vendorreplies.ai included (16, 32 and 48 px frames, 32-bit RGBA, stored as PNG; each site serves its own as `/favicon.ico`, section 5), and vendorreplies.ai's `vendorreplies-touch.png` (180 by 180, opaque: the 168 px head unscaled on navy `#0b121c`), `vendorreplies-favicon-32.png` and `vendorreplies-favicon-16.png`, all made by `tools/build_icons.py`. From 1.12.2 these 11 files are a reference only: each site builds its own `favicon.ico` (and vendorreplies.ai its PNG favicons and touch icon) with `tools/build_card.js`, and no site copies them (section 4, item 2). vendorreplies.ai has no `-favicon.svg`, `-mono.svg` or `-card.svg`: its logo is photographic artwork. | `site/drafts/assets/logos/`, rendered in the package |
| `marks/github-invertocat-black.svg`, `marks/github-invertocat-white.svg`, `marks/linkedin-in-bug.png`, `marks/orcid-id.svg`, `marks/NOTICE.md` | The footer's third-party marks and a NOTICE recording each file's source, retrieval date and terms: GitHub Invertocat (black and white) from `https://brand.github.com/GitHub_Logos.zip`, 2026-11-02, used as a social button under GitHub's logo rules; LinkedIn [in] bug from the official `in-logo.zip` at brand.linkedin.com/downloads, 2026-11-02, under the LinkedIn Brand and User Agreements; ORCID iD icon from `https://orcid.org/assets/vectors/orcid.logo.icon.svg`, 2026-11-02, under ORCID's brand guidelines. These are trademarks of their owners and are not covered by the CC0 grant. | `site/drafts/assets/footer/` |
| `reference/landing.html`, `document.html`, `reference.html`, `install.html`, `license.html`, `disclosure.html`, `404.html`, `channels.html`, `checklist.html`, `demo.html`, `figures.html`, `search.html`, `facets.html`, `crosswalk.html`, `matrix.html` | 1 reference page per page type with every component in its required markup (from 1.6.0 every page, the 404 page included, carries the light and dark switch): the landing page (hero, entry tiles, value paragraph, footer); a document page with the rail; a reference page with figures and a framed register table; an install page with the selector and copy blocks; the licence page; the disclosure page; the 404 page; from 1.1.0, the channel list, checklist and demo pages (sections 7.21 to 7.23); from 1.4.0, the text emphasis and figure components (sections 7.25 to 7.28); from 1.5.0, the search page in results mode with facets, the faceted index and the crosswalk on a left-rail page (sections 7.29 to 7.32); and, from 1.7.0, the large matrix (section 7.34). | Generated from `build.py` and the live cleanlanguage.ai pages |
| `SPECIFICATION.md`, `CONTENT-GUIDELINES.md` | This document and the content guidelines, at the version published with the package. | `docs/` |
| `SHA256SUMS` | Checksum of every file in the version. Beside the version folder, `baseline-<version>.tar.gz` is the reproducible archive of the whole version and `baseline/SHA256SUMS` lists the archives' digests. | |
| `README.md`, `CHANGELOG.md`, `LICENSE`, `NOTICE.md` | Use, history, the CC0 text, and the exceptions (fonts, marks, and the project names and domain names). | |

Excluded from the package: the mockup review bar and review notes (`.review`, `.review-notes` and the `role="note"` bar), the `/drafts/atrium/` and `/drafts/assets/` paths (the package uses root-relative paths under the site's own asset folder), the dark twins `*-dark.html` and the fixed `data-scheme` attribute on `<html>` in the markup (a page follows the operating system; from 1.6.0 `scheme.js` sets `data-scheme` at runtime from the reader's choice, section 7.33), the `<site>-dark.html` stubs, the mockup-only assets (`hub.css`, `cards/`, the unused font families), and `build.py` itself (each repository keeps its own generator and implements the markup in section 7).

Versioning follows SemVer: patch for fixes that change no markup contract, minor for new components or tokens, major for any change to a class name, token name or required markup.

Headers for `/baseline/*` on posluns.ca: `Cache-Control: public, max-age=31536000, immutable` and `X-Robots-Tag: noindex`. Sites copy the files at build time and never fetch them at runtime, so no CORS header is required.

## 4. Adoption in each repository

1. Obtain the version as its archive, `https://posluns.ca/baseline/baseline-<version>.tar.gz`, and verify the archive's digest against `https://posluns.ca/baseline/SHA256SUMS` before unpacking. Never verify file by file over the live domain: Cloudflare injects scripts into HTML served there, so an HTML file fetched from the site is not the published file.
2. Copy only the runtime and licence files into the site's own asset folder (proposed `assets/baseline/`): `baseline.css`, `palettes.css`, `fonts.css` and `fonts/` with the 4 licence files, `copy.js`, `select.js`, `search.js`, `rail.js`, `matrix.js`, `scheme.js`, the logo files its pages load (below), `marks/` with `marks/NOTICE.md`, `LICENSE` and `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); a pin lists only the files the site copied. From 1.12.2 a site copies from `logos/` the files its pages load and the files its builder reads (the `logo` and `favicon` of its `site-card.toml`), and pins them: the 6 full-colour rainbow logos (`<site>.svg`) and `vendorreplies.png`, which the family footer shows (section 7.19); its own `<site>-favicon.svg` and `<site>-touch.png`, which the head links (section 5, item 1; vendorreplies.ai has neither and links the icons it builds); and any other of its own marks a page loads (`<site>-mono.svg`, `<site>-card.svg`). It copies no `.ico` file and none of `vendorreplies-touch.png`, `vendorreplies-favicon-32.png` and `vendorreplies-favicon-16.png`: those 11 files of 1.12.1 are a reference only. Until 1.12.1 this item said to copy `logos/` whole. Copy `tools/check_palette.py` to `.baseline/check_palette.py`, outside the served tree: it is a build tool, so the site copies and pins it (the Cloudflare Pages build has only the repository, so a gate cannot run from the package itself), and never serves it. From 1.8.0 `tools/check_overflow.js` may be copied to `.baseline/check_overflow.js` in the same way (item 3). 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 of the card builder's data file: the site writes its own `site-card.toml` from it (item 3), and the builder reads that file, never the template; a site that kept a copy of the template from an earlier version replaces it, or deletes it and its pin entry (from 1.12.3; until 1.12.2 this item did not name it, while section 11 listed it among the files to replace). From 1.12.2 (D-080) a site builds its `favicon.ico` with `tools/build_card.js`: its data file's `favicon` key names its `<site>-favicon.svg` (vendorreplies.ai: `vendorreplies.png`, which also gives its PNG favicons and, as its `logo`, its touch icon), and `--icons-only` builds the icons alone where the site builds no card (section 7.35). The site serves the built `<site>-favicon.ico` at its root as `/favicon.ico` (section 5, item 1). The built icons are the site's own files, like its card: kept outside the pinned copy folder, committed where the site serves them, and not pin entries; the builder's report gives each one's SHA-256. In 1.12.1 a site copied the package's `logos/<site>-favicon.ico` to `/favicon.ico`; on the recorded Chromium and zlib the build is byte-identical to that file (section 7.35).
3. Record the baseline version and each copied file's SHA-256 in `.baseline/pin.toml` (TOML: a `version` key and a table of path to digest), `.baseline/check_palette.py` included; the site's own `site-palette.css` is not pinned. From 1.6.0 the pin covers 2 locations: the served copy folder (`assets/baseline/`), holding the runtime and licence files, and `.baseline/`, holding the build tool `check_palette.py`, which is never served. A pin gate accepts entries under either location, checks each entry's digest, and fails on a file in the copy folder that the pin does not list, on a listed file that is missing, and on any entry outside the 2 locations. Add an offline gate that fails if any copied file differs from the pin, and run that gate, the palette gate (`python3 .baseline/check_palette.py --site <name> --palettes <asset folder>/palettes.css`, with `--site-palette <asset folder>/site-palette.css` where the site has one; mandatory on every site from 1.6.0), 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. From 1.8.0 the package also carries `tools/check_overflow.js` (section 3): copy it to `.baseline/check_overflow.js` (pinned like `check_palette.py`) and run it on the built pages, served locally, wherever the build or the site's CI has Node.js and Playwright; it is recommended, not mandatory, and it reaches no network beyond the local server. From 1.9.0 copy `tools/build_card.js` to `.baseline/` in the same way and, where the site builds a card, `tools/card-fonts/` (with `inter-LICENSE.txt`), keep the site's `site-card.toml` (written from `tools/site-card.example.toml`, which is not copied; item 2) and approved illustration beside the pin, and build the card locally (section 7.35); it is a build tool, never served, and never run in the deploy build. From 1.13.0 (D-103) a site that runs the builder only with `--icons-only` (section 7.35), 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 item told every site that holds the builder to copy the fonts.
4. Put `class="site-<name>"` on `<html>`. That class, the logo files, the content and, from 1.6.0, 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 `baseline.css`, may set only the measured per-site constants `--sw` and `--logo-inset` when the site's statement or logo changes and, from 1.11.1, the bands' spacing `--band-pad` (section 7.37) and, from 1.13.0, the document spacing `--para-gap`, `--download-after` and `--download-line-gap`, each lowered only: `baseline.css` clamps each between 0 and its default, so a larger value has no effect and a negative value gives 0; each takes a length, such as `0px`, because a unitless or other value that is not a length makes the declarations that read it invalid, which gives 0 margins there (sections 7.5 and 7.11) (README, "Per-site constants");
   - from 1.6.0, an optional `site-palette.css`, linked immediately after `palettes.css`, may override any palette token of section 6.1 (the 30 site tokens, 17 until 1.6.1, the rainbow tokens and `--band-grey`), light and dark, and nothing else but the tokens named below. From 1.9.1 it may also override the family tokens of section 6.2 (not the print tokens) and the component tokens of section 6.4, and a site sets its own colours with no request to the owner (owner, 2026-11-05; until 1.9.0 only at the owner's explicit request). The file uses the selectors of `palettes.css`, so that the later file wins in every state, and sets each overridden value in both forms of its scheme: light in `.site-<name>` and `[data-scheme="light"].site-<name>`; dark in `@media (prefers-color-scheme: dark) { .site-<name>:not([data-scheme="light"]) { ... } }` and `[data-scheme="dark"].site-<name>`. `site-palette.example.css` in the package is the template (1 dark `--bg` value in its 2 forms). The palette gate (item 3) fails if any pairing misses its threshold, if the 2 forms of a scheme disagree, or if the file holds anything other than palette tokens on the site's `<html>` selectors. QA for a colour change (grclibrary.ai's standard, recorded by the owner): 1 value changed in 1 file, a before and after comparison of the file and of the rendered pages showing that only the expected value changed, and the gate passing.

   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 in section 7; the reference pages are the contract.
6. Update the site's own gates where they conflict (section 11).
7. Updates arrive as 1 pull request per site that bumps the pin and the copied files; each site's own checks run as usual.
8. Add to the repository's NOTICE the site-template entry given in the package README. The entry names no path, so the same entry is true in every repository (D-098). It dedicates the template to the public domain under CC0 1.0, with 3 exceptions. The first is the 4 fonts and, where present, the card builder's Inter fonts, under the SIL Open Font License 1.1; each font's licence file travels with its font files. The second is the GitHub, LinkedIn and ORCID marks, under their owners' terms as recorded in the `NOTICE.md` that travels with the marks. The third is the project names and the projects' domain names, which are the projects' own names and marks and which CC0 1.0 does not license (its section 4(a)). The entry names 3 exceptions from 1.12.4 (D-094, D-095, D-096 and D-098). Through 1.12.3 it named 2 exceptions and did not name the Inter fonts, although `NOTICE.md` item 3 already kept the names and domain names with their projects and item 4 already licensed the Inter fonts under the SIL Open Font License 1.1.

### 4.1 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, deployed by Cloudflare Pages at `https://draft.<project>.pages.dev` (`<project>` is the site's Cloudflare Pages project name), with its pages under `/draft/`: the owner reviews `https://draft.<project>.pages.dev/draft/`. The production branch does not carry the draft until promotion. Until 1.10.0 the draft was staged at `/draft/` on the live site. Link-preview card previews stay at `https://demo.<project>.pages.dev/cardpreview.html` (section 7.35).
1. **Base path.** Every page, asset and internal link is generated with a configurable base path (`/draft/` while staging, `/` after promotion), so that the draft is self-contained: no draft page links to, or loads anything from, the live pages at the root. The baseline files are copied under the draft (`/draft/assets/baseline/`) and move with it.
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.
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, which goes 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 from `_redirects` (aiqt.ai and opfiles.ai carried one; every site checks), and the header changes of section 9. Nothing else changes live before promotion. A draft page may link a live file whose path is kept at promotion (for example `/downloads/*` or opfiles.ai's `/adopt.md`) rather than copying it under `/draft/`. 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 (section 11) 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 (section 11 lists the fixed paths each site must keep or redirect); 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.

## 5. Page anatomy

Every page, in this order (from `render_page()` in `build.py`):

1. `<html lang="en" class="site-<name>">`; `<head>` with `charset`, `viewport`, the page title in the form `<Page> | <Name>.ai`, `<link rel="icon" href=".../<site>-favicon.svg" type="image/svg+xml">`, `<link rel="apple-touch-icon" href=".../<site>-touch.png">`, then (from 1.6.0) `<script data-cfasync="false" src=".../scheme.js"></script>` with no `defer` or `async`, then `fonts.css`, `palettes.css`, the site's `site-palette.css` where it has one, and `baseline.css` in that order, then the page's other scripts with `defer`. From 1.12.1 every site also serves a `favicon.ico` at its root as `/favicon.ico`, which browsers and other clients request without a link, and keeps the SVG favicon and touch-icon links above; from 1.12.2 that file is the `<site>-favicon.ico` the site builds with `tools/build_card.js` (section 4, item 2), in 1.12.1 a copy of the package's `logos/<site>-favicon.ico`. vendorreplies.ai, whose logo is photographic artwork with no SVG favicon or mono mark, builds its icons from `vendorreplies.png` and links its PNG favicons, `<link rel="icon" href=".../vendorreplies-favicon-32.png" type="image/png" sizes="32x32">` and `<link rel="icon" href=".../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=".../vendorreplies-touch.png">`); until 1.12.1 it linked `vendorreplies.png` for both. From 1.12.2 these 3 files are its builds, served from a folder of its own outside the pinned copy folder; in 1.12.1 they were the package's files in `logos/`. A `<script type="application/ld+json">` block is data, allowed on live pages only, never on a noindex draft.
2. Skip link `<a class="skip" href="#main-content">Skip to content</a>`, first element of `body`, visible on focus.
3. `<header class="mast">`: the wordmark `a.brand` linking to the home page, then `<nav class="nav" aria-label="Site">` with the site's 4 to 6 page links on 1 row and, where the site has a public repository, GitHub as the last item (`target="_blank" rel="noopener noreferrer"` and a visually hidden "(opens in a new tab)"); the current page marked `aria-current="page"`; then, from 1.6.0, the light and dark switch `button.scheme` after the `nav` (section 7.33). No Disclosure or Licence item, nothing wraps, and no script in the links (section 7.1).
4. `<main id="main-content">`: exactly 1 `h1`; the landing page's content (section 7.2 to 7.4) or a document grid (section 7.5), which a page may set as a left-rail page with 1 side list on the left in place of the right rail (section 7.32). No breadcrumb is used in the mockups.
5. `<footer class="family wrap">`: the family footer (section 7.19).

From 1.11.0 a page may carry a banner (section 7.36) between the masthead and `<main>`; its `h1` is then the page's only `h1`. A page may also set `<main>` as a sequence of bands (section 7.37), each holding its own `.wrap`.
6. `<p class="vh" id="copy-status" role="status" aria-live="polite"></p>`: the live region `copy.js` writes to.

There is no inline style, no inline script and no `on*` attribute anywhere; `build.py` refuses to write a page that contains one, or an en or em dash.

## 6. Tokens

### 6.1 Colour (`palettes.css`)

Identical names on every site, values per site and scheme: `--bg`, `--surface-raised`, `--surface-sunken`, `--text`, `--text-muted`, `--rule`, `--rule-strong`, `--accent`, `--on-accent`, `--link`, `--link-hover`, `--focus`, `--selection`, `--code-bg`, `--callout-bg`, `--glow`, `--brand`, from 1.7.0 the 13 matrix tokens `--row-tint`, `--row-hover`, `--heat-0` to `--heat-4` and `--grp-1` to `--grp-6` (below), plus `--rainbow-1` to `--rainbow-6`, `--rainbow-join-1` to `--rainbow-join-6`, `--rainbow-ink-1` to `--rainbow-ink-6` and `--rainbow-band`.

`--accent` is the text-safe form of the site's hue: it meets 4.5:1 as text, so on the yellow and orange sites in light mode it is a dark gold or burnt orange. `--brand` is the site's true rainbow colour (light: red #c9302d, orange #ec7c0e, yellow #f4c929, green #59a847, blue #136dbf, purple #9049a5) and is decorative only: the start link's underline, the Next block's rule, the figures' rule, callout and note edges, the register caption's edge, SVG arrows, the current-page cue on tags. It is never used for text.

Measured minima across all 6 sites and both modes (from the 1.6.0 `contrast-report.txt`): text 13.59:1 on the 5 surfaces and 6.78:1 on `--selection`, muted text 4.62:1, links 5.17:1, link hover 7.03:1, accent as text 4.60:1, text on accent 5.41:1, focus ring 3.14:1, strong rules 3.05:1. (Until 1.5.1 this line gave text 6.8:1, links 5.23:1, text on accent 5.44:1, focus 3.11:1 and strong rules 3.07:1, which did not match the report shipped with 1.5.1: its light values, unchanged in 1.6.0, give the minima above.)

Dark mode from 1.6.0: every dark surface and rule (the 5 surfaces, `--rule`, `--rule-strong`) takes the site's hue alone, with no warm ochre, and dark `--text` and `--text-muted` are neutral (chroma 0). The design values of dark `--bg` are OKLCH lightness 0.155 and chroma 0.030 at the site's dark hue, which is the site's own hue except for the 3 warm sites, spread apart so that they stay distinguishable (secureconfig 18, opfiles 55, auditthis 95); the emitted hex values, measured back in OKLCH: secureconfig #170707 (0.155, 0.029, hue 21), opfiles #160802 (0.154, 0.031, 53), auditthis #100c01 (0.155, 0.028, 95), cleanlanguage #060f04 (0.155, 0.029, 139), aiqt #040d19 (0.156, 0.030, 253), grclibrary #110814 (0.154, 0.029, 318). The closest pair, red and orange, measures 1.64 apart (OKLab distance x 100). The other dark surfaces keep their 1.5.1 lightness steps from `--bg` (raised +0.05, sunken -0.045, code -0.05, callout +0.065 with chroma 0.040, rule +0.145), every surface chroma scaled by the same factor. Light mode is byte-identical to 1.5.1.

Matrix tokens from 1.7.0 (owner, 2026-11-04), new tokens only: every token above keeps its 1.6.1 value in both schemes (`palettes.css` is byte-identical to 1.6.1 once the 312 new lines are removed). Each tint is a surface that text sits on, so `gen_palettes.py` tunes it from a design lightness towards `--bg` until the text on it passes.

- `--row-tint` (the zebra stripe) and `--row-hover` (the hovered row and the highlighted column): `--text`, `--text-muted`, `--link`, `--link-hover` and `--accent` at 4.6:1 or more, `--focus` at 3.1:1 or more. Measured minima across the 6 sites: light 4.60:1 (accent on `--row-hover`), muted text 4.62:1, focus 3.12:1; dark muted text 5.82:1, accent 5.50:1, focus 5.87:1.
- `--heat-0` to `--heat-4` (the heat ramp, section 7.34): a heat cell's own text is `--text` and its count may be a link, so each step is tuned for `--text`, `--link` and `--link-hover` at 4.6:1; `--text-muted` and `--accent` are never drawn on a heat step (the stylesheet sets a heat cell's muted lines to `--text`). From 1.8.0 the focus ring inside a heat cell is drawn in `--text` (section 7.34), so `--focus` is not drawn on a heat step; until 1.7.0 each step was also tuned for `--focus` at 3.1:1, which limited the light ramp. Measured minima on the heat steps: light text 11.80:1, links 4.61:1; dark text 11.09:1, links 5.26:1. The dark steps grow in chroma and depth from design values and measure 3.0 to 3.8 apart (OKLab distance x 100), unchanged from 1.7.0. The light ramp is spread as far as the checks allow (owner, 2026-11-04): `--heat-0` is white, `--heat-4` is the colour at the site's hue farthest from it that passes every heat check (searched over lightness and over chroma up to 0.10 from 1.8.0, so no step is a fluorescent tint), and `--heat-1` to `--heat-3` sit at equal OKLab steps between them, each checked. Adjacent light steps measure: secureconfig 2.8 to 2.9, opfiles 3.0 to 3.2, auditthis 3.7 to 3.8, cleanlanguage 3.8 to 4.0, aiqt 3.0 to 3.2, grclibrary 3.1 (in 1.7.0: 2.2 to 2.3, 1.9 to 2.1, 2.9, 4.5 to 5.5, 1.8 to 2.1, 2.3 to 2.6). From 1.8.0 neighbouring steps must lie at least 2.5 apart in both schemes; the generator and `tools/check_palette.py` fail below it. The figure is the owner's design floor, above the 2 that the 1.6.0 changelog takes as the side-by-side difference most readers notice; it is not taken from a standard. The printed count carries the value; the shading reinforces it.
- `--grp-1` to `--grp-6` (column-group accents, section 7.34): the 6 rainbow hues in the order of section 1, each tuned as text on that site's 5 surfaces and 2 row tints at 4.6:1. Measured minima: light 4.60:1, dark 6.30:1.

`contrast-report.txt` lists every pair per site and scheme, with the heat step distances; 0 pairs and 0 distances fail.

From 1.11.0 `palettes.css` also holds `.site-vendorreplies` (a palette built like the others at OKLCH hue 34, chroma 0.160, its coral; no rainbow stop or column-group accent) and, beside `--band-grey`, the decorative band tokens `--band-silver`, `--band-sheen` and `--band-silver-deep` (light #8e949b, #c9ced4, #6f757c; dark #a7adb4, #dde1e5, #848a91). The 6 existing palettes keep their 1.10.0 values; the palette gate passes for 7 sites, worst text pair 4.60:1.

From 1.12.0 `palettes.css` also holds `.site-nodekeeper` (a palette built like the others at OKLCH hue 195, chroma 0.110, a teal; `--brand` `#06999a` light and `#29bbbb` dark, `--accent` `#007474` and `#21adad`; no rainbow stop or column-group accent of its own, and like vendorreplies.ai's palette it carries `--grp-1` to `--grp-6`, the 6 rainbow sites' accents tuned on its surfaces). Every other selector and token keeps its 1.11.5 value, the `:root` rainbow included; the palette gate passes for 8 sites, worst text pair 4.60:1. Its nearest `--brand` is cleanlanguage.ai's, 13.5 apart in light mode and 13.2 in dark mode.

**Adding a site (from 1.12.0).** `gen_palettes.py` takes every site's design values from 1 table, `FAMILY`: name, hue name, OKLCH hue, accent chroma, the `--brand` (lightness, chroma) in each scheme, the site's number in the rainbow (or none) and its exceptions to the shared rules. The other names the script used until 1.11.5 are derived from it, and with the 1.11.5 rows it writes 1.11.5's `palettes.css` (apart from its header line), `contrast-report.txt` and `palette-values.md` byte for byte. It refuses a table with a repeated name or hue, rainbow numbers that do not run 1 to N, or 2 `--brand` colours closer than 5.5 (OKLab distance x 100) in either scheme. The ring's order round the circle comes from the hues, and the numbered tokens (`--rainbow-N`, `--rainbow-join-N`, `--rainbow-ink-N`, `.slice-N`, `--grp-N`) stay with their sites, so a site added later keeps every existing site's number. `python3 gen_palettes.py --slots` writes nothing and lists, for each pair of rainbow neighbours, the midpoint of their hues with the mean of their `--brand` lightness and chroma, and, where a site outside the rainbow sits between them, the midpoints on either side of it; a slot is usable when its `--brand` lies at least 5.5 from every site's, rainbow or not, in both schemes, and, tried as the next rainbow stop (ring number N + 1, so its own stop, its joins to both neighbours and its rainbow ink are checked too), every check passes. With the 1.12.0 table the usable slots are at hues 73.5, 116, 167.5, 223.5, 285 and 352.5: the green to blue midpoint, 196, lies on NodeKeeper's teal (2.4 apart in light mode), so that gap offers the midpoints on either side of it, and the red to orange gap holds vendorreplies.ai and offers none.

- A site outside the rainbow (as vendorreplies.ai and NodeKeeper) is 1 row with no rainbow number. It changes no other site's token and no markup; a minor release.
- A site in the rainbow is 1 row with the next number, at a usable slot. Its palette and the generated `palettes.css` (stops, joins, inks, slices, band and ring for N stops) need no other edit, and no existing site's palette token changes; every palette gains `--grp-N` for it. The `:root` joins on either side of it change, because they join new neighbours. The release that adds it must also, with its own owner decisions: give `baseline.css` the footer rules for N cards (section 7.19, written for 6 rotated cards), measure the footer's width with N names, write the footer markup of every reference page, add `th[data-grp]` rules for the new accent, and decide whether the numbered tokens are joined by name-keyed ones.

A site may override these tokens only as section 4, item 4 sets out.

### 6.2 Family tokens (declared in `baseline.css`; from 1.9.1 inside `:where()`, so a site may override them, except the print tokens)

| Token | Light | Dark | Use |
|---|---|---|---|
| `--gold` | #8a6a0a | #e6c15a | The `.ai` of the wordmark and the cards, the signature's `@` and `.ca`, the hero kicker |
| `--gold-rule` | #d4a52a | #d4a52a | The rule under the wordmark; the hover and current cues in the footer |
| `--silver` | #6a6a6a | #cfcfcf | The signature |
| `--ord-progress`, `--ord-speed`, `--ord-cost` | #3b752e, #a25100, #bc3732 | #6faa62, #e1791b, #ef675c | aiqt.ai's priority ordering only (the text-safe accents of the green, orange and red sites) |
| `--print-paper`, `--print-ink` | #ffffff, #000000 | #ffffff, #000000 | From 1.7.0: matrix tables on paper (section 7.34) |

Dark values apply under `prefers-color-scheme: dark` unless `data-scheme="light"` forces light, and under `[data-scheme="dark"]`.

From 1.9.1 the gate checks `--gold`, `--silver` and the 3 ordering colours at 4.5:1 on `--bg`; `--gold-rule` is decorative.

### 6.3 Layout and type (declared in `baseline.css`)

| Token or value | Value | Where |
|---|---|---|
| `--pad` | `clamp(1rem, 3vw, 3.2rem)` | The page gutter: `.wrap`, `.mast`, `.family` |
| `--art` | `clamp(11rem, 22vw, 30rem)` | The hero logo column |
| `--railw` | `clamp(13rem, 13vw, 17rem)` | The rail column of the document grid |
| Body text | `1.25rem/1.6` Newsreader (20 px at the 16 px default); `1.125rem` (18 px) at 600 px and below | `body` |
| Hero introduction | `clamp(1.35rem, 1.35vw, 1.8rem)`, line height 1.5, measure 68ch; 1.2rem at 600 px and below | `.hero .intro` |
| Hero statement | `clamp(2.6rem, 98cqw / var(--sw), 5rem)`, weight 300, line height 1.06, where `--sw` is the statement's 1-line width in em (section 7.2); `clamp(2.4rem, 98cqw / var(--sw), 3.2rem)` at 1000 px and below; 2.2rem at 600 px and below | `.statement` |
| Document headings | h1 2.8rem/1.1 weight 300; h2 2rem/1.18 weight 400; h3 1.45rem/1.35 weight 500; at 600 px and below h1 2.2rem, h2 1.7rem | `.doc h1`, `.doc h2`, `.doc h3` |
| Label voice | `500 0.72rem/1.4` Public Sans, tracking 0.2em, uppercase, `--text-muted` | `.lbl`, `.label` |
| Navigation | `500 0.74rem/1` Public Sans, tracking 0.18em, uppercase | `.nav` |
| Wordmark | `700 clamp(1.6rem, 1.9vw, 2.2rem)/1` Public Sans, tracking -0.015em | `.brand` |
| Measure | 72ch for text blocks in the document grid (a lead inside `.doc` included); 84ch for steps; 66ch for the meta line; 68ch for the value paragraph; 52ch for a lead outside the grid. From 1.11.0 these are the defaults of `--measure` and `--measure-lead`, which the classes of section 7.5 set | `.doc` rules, `.region .value`, `.lead` |
| Document grid | columns `9rem minmax(0, 1fr) var(--railw)`, column gap `clamp(2rem, 3vw, 4rem)`; 1 column at 1000 px and below | `.doc` |
| Document spacing (from 1.13.0) | `--para-gap` 1.1rem under `p`, `ul`, `ol`, `dl`, `blockquote` and a grid-level `details`; `--download-after` 1.6rem under `.download`; `--download-line-gap` (default and ceiling the paragraph gap in effect, floor 0) under a `.download` file line followed by more lines, wherever the paragraph rule sets that line's margin; the last element of a download block in a document ends flush with it. Read with these defaults, clamped between 0 and them with `max(0px, min())`, and not declared (the template sets `--file-line-gap`, its own, on the file line only); a site sets them in `site.css`, decrease only: a larger value has no effect and a negative value gives 0; a value that is not a length, a unitless `0` included, makes the declarations that read it invalid, so the margins they set are 0 (section 7.11) | `.doc` rules, `.download` |
| Region grid | columns `9rem minmax(0, 1fr)`, gap `0.4rem 2.5rem`, bottom padding 2.6rem; 1 column at 1000 px and below | `.region` |
| Focus ring | `3px solid var(--focus)`, offset 2px | `:focus-visible` |
| Footer stages (container width of `.family`, which is the content width) | From 1.11.1: A: above 1159 px, names shown, the 3 marks grouped beside the signature, "A" and "Project" from 1240 px; C: 1159 px and below, names visually hidden, equal tiles (in 1.11.0: A from 1500 px, full size; until 1.10.0: A from 1400 px, B from 1040 to 1399 px with names at scaled sizes, C below 1040 px); the signature scales below 750 px, drops "A" and "Project" below 650 px (and shows them again between 420 and 599 px), scales again below 450 px; phones: below 600 px the marks and the signature move to a second line | `@container footer (...)` |
| Hero container | `.hero-text` is an inline-size container; the statement size uses its `cqw` | `.hero-text` |
| Scale | Perfect fourth (1.333) on a 1.1875rem base, applied by hand in the values above | Comment at the top of the stylesheet |

Magic numbers in the CSS, with the token name proposed for the package: the label column width 9rem (`--label-col`); the 3px top band of the footer (`--band`, 0.55rem of padding in the CSS, which is 8.8px, so the comment's "3 px" describes the gap that no longer exists and must be corrected); the footer tile height 3.4rem and logo 2.75rem (`--tile`, `--tile-logo`); the mark size 2rem (`--mark`); the footer grey `--band-grey` #4a4a4a light and #5c5c5c dark (declared on `.family`; propose moving it to `palettes.css` as a neutral token); the hero text and logo ratios `--sw` and `--logo-inset` (per-site measured constants, section 7.2 and 7.6; they belong with the logos in the package's README as the values to re-measure when a statement or a logo changes); the fixed-table column widths `col.w11` to `col.w26` (specific to aiqt.ai's framework registry; generalize to `col.w<N>` for N in 5-step values or set widths per table).

### 6.4 Component tokens (from 1.9.1, declared in `baseline.css` inside `:where(:root)`)

Each defaults to a palette token, so the template's look holds until a site overrides one in its `site-palette.css` (section 4, item 4). The gate checks the pairs named here; a token with no pair is decorative.

| Tokens | Default | Gate |
|---|---|---|
| `--chip-fg`, `--chip-border` | `--text`, `--text-muted` (from 1.9.1; `--text-muted`, `--rule-strong` until 1.9.0) | text 4.5:1, border 3:1 on `--bg` |
| `--chip-hover-fg`, `--chip-hover-border` | `--text`, `--text` | 4.5:1, 3:1 on `--bg` |
| `--chip-pressed-fg`, `--chip-pressed-border` | `--accent`, `--accent` | 4.5:1, 3:1 on `--bg` |
| `--chip-unavailable-fg`, `--chip-unavailable-bg`, `--chip-unavailable-border` | `--text-muted`; light `--rule`, dark `--bg`; light `--rule`, dark `--rule-strong` | text 3:1 on `--chip-unavailable-bg` (an inactive control); the fill and border are decorative |
| `--button-fg`, `--button-border`, `--button-hover-fg`, `--button-hover-border` | `--text-muted`, `--rule-strong`, `--text`, `--text` (copy and inline buttons) | text 4.5:1 on `--bg` and `--code-bg`; borders 3:1 on `--bg` |
| `--nav-fg`, `--nav-hover-fg`, `--nav-current-fg` | `--text-muted`, `--text`, `--accent` (top bar links and the scheme switch) | 4.5:1 on `--bg` |
| `--rail-fg`, `--rail-hover-fg`, `--rail-current-fg` | `--text-muted`, `--text`, `--accent` (rail and left rail) | 4.5:1 on `--bg` |
| `--picker-fg`, `--picker-hover-fg`, `--picker-current-fg`, `--picker-current-rule` | `--text-muted`, `--text`, `--accent`, `--brand` (install selector) | text 4.5:1 on `--bg`; the rule is decorative |
| `--input-fg`, `--input-bg`, `--input-border`, `--input-placeholder` | `--text`, `--surface-raised`, `--rule-strong`, `--text-muted` (search box) | text and placeholder 4.5:1 on `--input-bg`; border 3:1 on `--bg` |
| `--tag-fg`, `--tag-border`, `--tag-accent-fg`, `--tag-accent-border` | `--text-muted`, `--rule-strong`, `--accent`, `--brand` | `--tag-fg` 4.5:1 on `--bg` and `--surface-sunken`; `--tag-accent-fg` 4.5:1 on `--bg`; borders decorative |
| `--th-fg`, `--th-rule` | `--text-muted`, `--rule-strong` (table headers) | 4.5:1 on `--bg`; the rule is decorative |
| `--band-tint-bg` (from 1.11.0) | `--surface-sunken` (the tinted band) | `--chip-fg`, `--nav-fg`, `--th-fg`, `--tag-fg` 4.5:1 on it |
| `--band-dark-bg`, `--band-dark-fg`, `--band-dark-heading`, `--band-dark-link`, `--band-dark-focus` (from 1.11.0) | light: `--text`, `--bg`, `--glow`, `--glow`, `--bg`; dark: `--surface-sunken`, `--text`, `--accent`, `--link`, `--focus` (the dark band) | text, heading and link 4.5:1 on `--band-dark-bg`; focus 3:1 |
| `--banner-from`, `--banner-to` (from 1.11.0) | `--brand`, `--accent` (the banner's frame) | decorative; the banner text sits on the picture or on `--surface-raised` |
| `--pair-before-bg`, `--pair-before-bg-on-tint`, `--pair-after-bg`, `--pair-panel-bg` (from 1.11.0) | `--surface-sunken`, `--bg`, `--surface-raised`, `--code-bg` (the before-and-after boxes and their inner panel) | `--text` and `--link` 4.5:1 on each; `--text-muted` on the 3 boxes; `--accent` on `--pair-after-bg` |
| `--box-bg`, `--box-border` (from 1.11.0) | `--surface-raised`, `--rule` (the card grid) | `--text`, `--text-muted`, `--link` and `--accent` 4.5:1 on `--box-bg`; the border is decorative |
| `--ink-warm` (from 1.11.2) | `--accent` (the warm inline ink, `span.warm`) | 4.5:1 as text on the 5 surfaces, the 2 row tints, `--band-tint-bg`, `--pair-after-bg` and `--box-bg` |
| `--heading-fg` (from 1.11.3) | `--text` (the `h2` and `h3` headings of plain and tinted bands, a box's `h3` in such a band included; `.band-dark` headings keep `--band-dark-heading`) | 4.5:1 as text on `--bg` and `--band-tint-bg` |

Every other component reads the palette tokens of section 6.1 directly, so a site changes it through those. A colour need that no token covers is a change request to the baseline: a new component token, not site CSS.

## 7. Components

Each component has 1 required markup pattern; the reference pages carry them in full. Class names are kept as built (no `bl-` prefix), because the mockups, the gates and this document all use them.

### 7.1 Top bar and wordmark

Markup: `<header class="mast"><a class="brand" href="/"><span class="name">Name<span class="tld">.ai</span></span><span class="rule" aria-hidden="true"></span></a><nav class="nav" aria-label="Site"><a href="...">Page</a>...<a href="https://github.com/..." target="_blank" rel="noopener noreferrer">GitHub<span class="vh"> (opens in a new tab)</span></a></nav></header>`. The name is the site's word in Public Sans 700 with the `.ai` in `--gold` and a 3px rule in `--gold-rule` beneath.

The bar is a grid of the wordmark, the links and, from 1.6.0, the light and dark switch (section 7.33), and never wraps. `nav.nav` holds the site's own 4 to 6 page links on 1 row, right-aligned, tracked uppercase in `--text-muted` (the current page in `--accent`), the size scaling with the window between 0.66rem and 0.74rem; GitHub is the last, right-most item where the site has a public repository; Disclosure and Licence are not in the bar (section 7.24). When the row cannot hold the links it scrolls sideways (`overflow-x: auto`, thin scrollbar) and a shadow appears at the edge where links continue, drawn with `background-attachment: local` gradients (`color-mix` of `--rule-strong` with transparency) and no script; the shadow shows only on the overflowing side and disappears when the row is scrolled to that end. Measured in the reference pages: at 1180 and 1512 px every site's links fit on 1 row with no scrolling (6 links plus GitHub on the widest); at 1000 px and below the wordmark takes the first row and the links a full-width second row, left-aligned, still 1 row, with no scrolling at 820 on any reference page. Measured on the 1.6.0 reference pages at 390 px, with the fonts loaded: the row scrolls by 0 px on grclibrary.ai's pages with 4 short links and GitHub (44 px on its 404 page, whose links are longer), 55 to 100 px on the pages with 4 links and GitHub, 163 px with cleanlanguage.ai's 6 links and 206 px with auditthis.ai's 5 long links. The 1.5.1 pages give the same values; until 1.5.1 this section gave about 130 px with 6 links and none with 4, which these measurements do not reproduce. The switch sits outside the scrolling row and does not change it at any width (measured at 390, 820, 1000, 1180, 1512 and 2000 px: every reference page's row scrolls by the same amount as in 1.5.1). The bar is 71 px tall at 1180 px and 112 px at 390 px, as in 1.5.1.

### 7.2 Hero

Markup: `<section class="hero" aria-label="Introduction"><div class="hero-text"><p class="lbl gold">kicker</p><h1 class="statement">... <em>emphasized ending</em></h1><p class="intro">...</p><a class="start" href="...">Start text <span class="arrow" aria-hidden="true">&rarr;</span></a></div><div class="hero-art"><img src="logo.svg" alt="..."></div></section>`. The hero is a 2-column grid: the text block, then the logo column of width `--art`, right-justified over a radial glow (`--glow`), with a right margin of half the logo width (`padding-right: max(0px, calc(var(--art) * 0.5 - var(--pad)))`) so that the logo's margin equals half its width at every width where that exceeds the gutter. The logo fades in over 400 ms unless the reader prefers reduced motion.

The statement: 1 sentence with an emphasized ending (`<em>` in `--accent`, italic, never split), or 2 sentences with the second in `<em>`; a 2-sentence statement carries `class="statement two"`. The size is `clamp(2.6rem, 98cqw / var(--sw), 5rem)`: the statement fills 98 % of the text column on 1 line whenever that gives 2.6rem or more, and never drops below the floor; below the floor the `em` moves whole to a second line. `--sw` is the statement's 1-line width in em, measured per site and scheme (dark mode adds 0.01em of tracking): secureconfig 13.712 light and 14.612 dark; opfiles 15.070 and 16.070; auditthis 12.562 and 13.337; cleanlanguage 12.824 and 13.724; aiqt 19.868 and 21.218; grclibrary 13.069 and 13.894. A site that changes its statement must re-measure `--sw` (render the statement at 100px with `white-space: nowrap` and divide its width by 100). At 1000 px and below the floor is 2.4rem and the cap 3.2rem; at 600 px and below the size is 2.2rem, the grid becomes kicker beside a 5.5rem logo with the title, introduction and start link full width, and a 2-sentence statement's `em` becomes a balanced block.

The introduction is at most 3 sentences and about 60 words (content guidelines section 2). aiqt.ai's hero carries the priority ordering as `<span class="ordering">` inside the introduction.

### 7.3 Entry tiles ("Start here")

Markup: a labelled region (section 7.5) whose content is `<ol class="entries"><li><a href="..."><span class="t">Title</span><span class="n">metadata</span><span class="d">1 line</span></a></li>...</ol>`, 3 to 6 items. Each tile is a vertical stack: the numeral (CSS counter, Newsreader 300 2.3rem in `--accent`), the title (Newsreader 600 1.4rem), the metadata (JetBrains Mono 0.8rem, `--text-muted`) directly under the title, then the description (Public Sans 1.05rem). The tiles form 1 row of equal columns (`grid-auto-flow: column`) read left to right; with 5 or more items the title drops to 1.2rem and the description to 0.95rem (`:has(li:nth-child(5))`). At 1000 px and below the list is 1 vertical sequence with the numeral beside its own title.

### 7.4 Value paragraph ("What it gives you")

Markup: a labelled region with the label "What it gives you" and `<p class="value">...</p>`, measure 68ch, body size. Under 100 words (the mockups run 81 to 91), written for a capable non-developer, building on the introduction, ending with 1 plain sentence of the material facts; the licence link sits inside it (for example "free to use under CC0"). It replaces Benefits and gaps; fuller scope statements live on the relevant inner page as a Scope paragraph (section 7.14). Content guidelines section 7 governs the wording.

### 7.5 Labelled regions and the document grid

Regions: `<div class="region"><p class="lbl">Label</p><div>content</div></div>`, a 2-column grid of label column (9rem) and content; the landing page's "Start here" and "What it gives you" are regions. Document pages: `<div class="doc">` holding, in order, `<p class="lbl first">kicker</p>`, `<h1>`, the rail (section 7.6), an optional `<p class="meta">` line (Public Sans 1.05rem, 66ch), then blocks. Every direct child sits in the text column except `.lbl` (label column; `.lbl.sec` precedes an `h2`, `.lbl.blk` precedes a block, `.lbl.accent` in `--accent`), `.rail` (rail column, sticky at 1.5rem from the top), `.wide` (text column, used for tables and figures) and `.full` (all 3 columns). `section` and `article` children use `display: contents` so their children take the grid. Text blocks have a measure of 72ch. At 1000 px and below the grid is 1 column with labels above their content.

Line width (from 1.11.0, owner, 2026-11-06): the widths above are the defaults of `--measure` (72ch; 84ch for steps, 66ch for the meta line) and `--measure-lead` (52ch for a lead outside the grid). A class on `<body>`, `<main>`, a band, `.doc`, a section or a single block sets them for everything inside it: `.measure-full` (no limit), `.measure-wide` (96ch, a lead 72ch), `.measure-narrow` (60ch, a lead 44ch) and `.measure-read` (the defaults, to restore them inside a wider page). `.doc.no-rail` gives the rail column to the content on a page with no rail (2 columns above 1000 px, `.full` spanning both); without it the rail column stays reserved, as until 1.10.0. Measured: the 15 reference pages, which use none of these, are pixel-identical to 1.10.0 apart from the footer.

Long text in documents (from 1.8.0): in `.doc`, a paragraph, list item, term, definition, heading (`h1` to `h4`), quotation, figure caption or callout breaks a token wider than the line where nothing else fits (`overflow-wrap: anywhere`), so a long URL, as link text or as plain text, a long identifier or a hash wraps at the column edge and the page never scrolls sideways at 320 px. A break inside a word happens only when that word alone is wider than the line, so ordinary words still wrap at spaces (a hyphenated word may still break at its own hyphen, as before). Inline code already broke this way; code blocks keep their own rules, and table cells keep theirs (section 7.10): a wide table scrolls inside its `.tablewrap`. `reference/document.html` carries a long link as an example.

Spacing (from 1.13.0, D-097): in a document, a paragraph, list, definition list, quotation or grid-level `details` block has `--para-gap` (default 1.1rem) under it. A site may set a smaller value in its `site.css` (`.site-<name> { --para-gap: 0.9rem; }`); `baseline.css` clamps it between 0 and 1.1rem with `max(0px, min())`, so a larger value has no effect and a negative value gives 0 (section 4, item 4). Give it a length, such as `0px`: a unitless number or any other value that is not a length makes each declaration that reads it invalid at computed-value time, so every margin that declaration sets is 0 and the clamp plays no part (section 7.11). Grid items' margins do not collapse, so a gap between 2 grid items is the first one's bottom margin plus the second one's top margin. A box with its own paragraph margin (a callout, 0.8rem) keeps it.

### 7.6 Rail

Markup: `<nav class="rail" aria-label="On this page"><img class="rail-logo" src="logo.svg" alt="..."><p class="lbl">On this page</p><ol><li><a href="#id">Section</a></li>...</ol></nav>` on a page with sections worth listing, with `<script src=".../rail.js" defer></script>` on that page; no link carries `aria-current` in the markup. `rail.js` marks the link of the section the reader is on with `aria-current="location"` (the WAI-ARIA token for the current location within a set): the last section whose top has passed a line a quarter of the viewport down; at the end of the page, the last section in view; after a rail click or on a page opened at `#id`, that section. From 1.13.0 a fragment that is not valid percent-encoding (such as `#%E0`) names no section and throws no error, so the reading line decides; until 1.12.4 it threw a URIError on load and marked no link until the reader scrolled. A rail link whose fragment is not valid percent-encoding (`href="#%E0"`) is skipped: it is never marked, and the other links work; until 1.13.0 it stopped the script on load, so no link was ever marked on that page. While the script runs, at most 1 link is marked, and exactly 1 once a listed section has reached the screen; before that (a page opened at the top whose first section starts below the screen, as on phones where the rail sits inline above a long introduction) no link is marked, which is accurate: the reader is still in the introduction. Without script no link is marked. The CSS colours `[aria-current="location"]` and, for pages built on 1.2.1 and earlier, `[aria-current="true"]`, which `rail.js` clears on load; `<div class="rail"><img class="rail-logo" ...></div>` (logo only) on any other page, including the licence and 404 pages. The list is numbered by CSS counter in `--accent`; it is the page's own sections and never the site menu. The logo fills the rail width; the label and list are indented by `calc(100% * var(--logo-inset))`, where `--logo-inset` is the logo artwork's visible left margin as a fraction of the SVG box (secureconfig 0.115, opfiles 0.1225, auditthis 0.2075, cleanlanguage 0.185, aiqt 0.0975, grclibrary 0.115; measured by rendering each logo at 400 px), so the text starts under the artwork's left edge within 0.1 px. At 1000 px and below the rail sits under the `h1`, the logo is capped at 7rem, the indent becomes `calc(7rem * var(--logo-inset))` and the list runs in 2 columns (1 column at 600 px and below).

### 7.7 Document control strip (aiqt.ai)

Markup: `<dl class="control"><div><dt>Document</dt><dd>aiqt.ai</dd></div><div><dt>Version</dt><dd>AIQT 1.0.5</dd></div><div><dt>Status</dt><dd>...</dd></div><div><dt>Source</dt><dd><a ...>jposluns/guardrails</a></dd></div><div><dt>Licence</dt><dd><a href="/license/">Apache 2.0, see NOTICE</a></dd></div></dl>`, placed after the meta line on every inner page. JetBrains Mono 0.8rem, uppercase `dt`, hairlines above and below in `--rule-strong`, wrapping as needed. The version and status must come from the repository's `VERSION` and its own published wording.

### 7.8 Numbered clauses (aiqt.ai)

Markup: `<h2 class="clause" id="st-1"><span class="num">1</span><span>Title</span></h2>` for a clause heading (the number in JetBrains Mono 1.1rem, `--accent`, in a 2.6rem column), and `<ol class="clauses"><li><span class="n">2.1</span><div><p><strong>Term.</strong> Text</p></div></li>...</ol>` for the clause list (number column 2.8rem, measure 72ch). The clause ids are the anchors the rail lists.

### 7.9 Framed notes (aiqt.ai) and callouts (all sites)

Framed note: `<div class="note"><span class="label">Rule 1</span><div><p>...</p></div></div>`: a 1px frame in `--rule-strong` with a 4px left edge in `--brand`, the label in a 5.2rem column, background `--callout-bg`, measure 72ch; at 600 px and below the label sits above the text. Callout (every site): `<div class="callout">...</div>` or `<div class="callout rule">` (4px inset edge in `--brand`), preceded by its label in the label column; `--callout-bg`, 1.15rem text, no border.

### 7.10 Framed registers and tables

Table: `<div class="tablewrap" tabindex="0"><table class="[sticky] [fixed]"><caption class="vh">...</caption>[<colgroup><col class="w26">...</colgroup>]<thead><tr><th>...</th></tr></thead><tbody>...</tbody></table></div>`. The wrapper scrolls horizontally and is focusable; `th` is the label voice with the only hairline in the design beneath it; `.sticky` keeps the header row at the top of its scroll box while the reader scrolls the table inside it, in a wrapper bounded to 75 % of the viewport height (section 7.31; in 1.4.1 and earlier the header stuck only to an unbounded wrapper, so it scrolled away with the page, contrary to this section's wording until 1.5); `td.id` and `.mono` are JetBrains Mono; `td.num` right-aligned; `td .sub` a muted second line; `.tag`, `.tag.accent`, `.tag.fill` are pill tags. Cells use `overflow-wrap: break-word` (on `td` and on `code` inside cells; `td.id code` stays on 1 line): in an auto-layout table a column keeps its typical width and the table scrolls inside its wrapper on narrow screens instead of crushing its columns, and in a fixed-layout table a long token wraps inside its own cell. `table.fixed` uses fixed layout with `colgroup` widths and a minimum width of 56rem, so 1 long cell cannot widen a column; its headers may wrap. (The mockups use `overflow-wrap: anywhere`, which crushes an auto-layout table's columns at 390 px; baseline 1.0.0 corrects it.) From 1.7.0 a `table.fixed` may size its columns in rem with `col.r6` to `col.r24` in place of the percentages, so that a wide table grows past the page and scrolls inside its wrapper; `td.nowrap` keeps a cell on 1 line; `td.na` marks an empty cell (section 7.34). A commit hash is written as `<code class="hash" title="full 40-character hash">7 characters</code>`. On paper every table prints whole, in both directions, and fits the page width (from 1.8.0; section 7.34, Print).

Framed register (aiqt.ai): `<div class="register"><p class="reg-cap"><b>Register 1</b><span>Title</span><span class="count">17 rows</span></p> + table</div>`: a 1px frame with a caption strip (`--surface-sunken`, 4px `--brand` edge, count in JetBrains Mono at the right).

Figures (reference pages): `<div class="figures"><div class="fig"><span class="num">17</span><span class="cap">frameworks</span></div>...</div>`, Newsreader 300 3rem numbers over a 3px `--brand` rule.

### 7.11 Code and prompt blocks with copy

Markup: `<pre id="X" class="hascopy"><button class="copy" type="button" data-copy="X" hidden>Copy</button>text</pre>`. Two variants for a long text: `class="prompt-block"` with `tabindex="0"` caps the block at 36rem and scrolls inside it, for a prompt the reader copies rather than reads (the page keeps its shape); `class="prompt-flow"` (from 1.1.0) shows the whole text in the page flow with no cap and no inner scroll, for a portable text the reader reads in place, such as cleanlanguage.ai's instructions page. Both keep the same `pre id`, copy button and status contract. JetBrains Mono 0.95rem on `--code-bg`, wrapping (`pre-wrap`, `overflow-wrap: anywhere`); the full text is always in the page. `copy.js` reveals the button when the Clipboard API exists, copies the text without the button's label, shows "Copied" for 3 s and writes to `#copy-status`; without script the button stays hidden and the text is selectable. Inline `code` uses the same face at 0.9em. The download block (`.download`: file link `p.file`, `.lbl.size`, `code.sum` checksum, optionally a `pre` or a plain `p`) belongs with it. From 1.13.0 (D-097) the last element of a download block in a document ends flush with the block, so the space under the block is `--download-after` (default 1.6rem, 25.6 px) whatever that element is. Until 1.12.4 the 2 margins added up in the document grid: 43.2 px under a file line or a paragraph, 40 px under a size line or checksum. The last line ends flush only when it is the block's last element: a block whose last element is hidden keeps the margin of its visible last line, so its gap stays as in 1.12.4. The space under the file line, where more lines follow it, is `--download-line-gap`: its default and its ceiling are the paragraph gap in effect (`--para-gap`, 1.1rem unless the site lowers it), and its floor is 0; it merges with the size line's 0.3rem top margin. It applies wherever the paragraph rule sets the file line's margin, at any depth of a document: a block at the grid's level, in a `section` or `article` at any depth, or in a `details`. 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. Outside a document neither the token nor the flush last line applies. A site may lower either token in its `site.css`; `baseline.css` clamps each between 0 and its default with `max(0px, min())`, so a larger value has no effect and a negative value gives 0. For example, `.site-vendorreplies { --download-after: 1.1rem; }` makes the space under a file-only block 17.6 px. Setting it to `0px` makes the next block touch the file line. Each token takes a length, such as `0px`. A unitless number (a bare `0` included) or any other value that is not a length makes each margin declaration that reads the token invalid at computed-value time, so every margin that declaration sets takes its initial value, 0, in place of the clamped value: with `--para-gap`, the whole margin of each paragraph, list, definition list, quotation, grid-level `details` block and file line whose margin the token's rules set (their top and side margins are 0 already, so the bottom margin falls to 0); with `--download-line-gap`, the whole margin of the file line followed by more lines, where the paragraph rule sets it; with `--download-after`, the block's bottom margin only, because the block's 0.4rem top margin is a separate declaration and stays. Where a box's own paragraph rule sets the margin, the token is not read and nothing changes. Nothing grows. Measured on 2026-11-12 (Chromium 153.0.8010.12) with `0`, `2` and `foo` against `0px`: the same computed margins on every side, and `CSS.supports` rejects each of these declarations with the value substituted and accepts it with `0px`.

### 7.12 Selector (install pages)

Markup: `<ul class="picker" data-picker><li><a href="#claude" data-family-link="claude">Claude</a></li>...</ul><button class="btn picker-reset" type="button" data-picker-reset hidden>Show all assistants</button><p class="vh" role="status" aria-live="polite" data-picker-status></p>` followed by 1 `<section id="claude" data-family-section="claude">` per assistant, each with its `h2`, a `<p class="verified">Last verified: ...</p>` line and its steps (`<ol class="steps">`, Newsreader 300 2.6rem numerals). Without script every section is visible and the links are in-page anchors; `select.js` shows 1 section, marks the chosen link `aria-current="true"`, reveals the reset button, writes the status, and accepts `?ai=<family>` and `#<family>` on load and on later hash changes.

### 7.13 Index lists

Markup: `<ul class="plan [one|three] [described]"><li><a href="...">Title</a><span class="n">count or type</span><small>1 line</small></li>...</ul>`: 2 columns (3 with `.three`, 1 with `.one`; 2 at 1000 px and below, 1 at 600 px and below), serif titles with a monospace figure at the right, an optional description line. Used for guide indexes, domains, types and the search results; `.chips` is the row of filter pills on the search page, and `.search` the form (`<form class="search" role="search" data-search>` with a labelled `input type="search"` and a `data-search-status` line) that `search.js` filters by `data-search` text on each `li`. From 1.5.0 the list (`data-search-scope`) can run in results mode, show nothing until the reader types, and be filtered by chip groups (facets); section 7.29 gives the contract. Without script every row shows.

### 7.14 Scope paragraph

Markup: `<p class="lbl blk">Scope</p><p>...</p>` near the top of an inner page: the fuller statement of what the project covers and does not do, moved from the landing page's former lists; plain prose, no list. Present on secureconfig.ai (checklist, guides), opfiles.ai (standard), auditthis.ai (method), cleanlanguage.ai (standard, install), aiqt.ai (install) and grclibrary.ai (browse).

### 7.15 Next block

Markup: `<div class="next"><p class="lbl">Next</p>[<p>1 line</p>]<a class="start" href="...">Text <span class="arrow" aria-hidden="true">&rarr;</span></a></div>`, closing every inner page above a 3px `--brand` rule; the licence page's Next leads home and, where the project has a public repository, names it.

### 7.16 Licence page

Path `/license/` (`license/index.html`), heading "Licence" in the label and "Licence" as the navigation item; `h1` is the licence's name. Content, in order: the scope sentence with links to the licence file in the repository (where the project has a public repository) and the canonical licence text; "Parts under a different licence" where applicable; "What you may do", a complete plain-language summary of the licence's conditions and limits with the clause numbers read from the licence text and the sentence "The licence text governs; this is a summary, not legal advice."; the owner's note in a callout ("Jeff Posluns prefers to publish openly and releases each project under the licence that best fits it: open source licences for software, and public domain or Creative Commons licences for documentation and content."); the Next block. No file named `license.html` may exist beside the folder (it would shadow the folder on Cloudflare Pages).

### 7.17 404 page

The document grid with the label "404", `h1` "That page is not here.", a lead sentence naming what the top bar links to, the logo-only rail and a Next block to the home page. The header and footer are the same as every page.

### 7.18 Before and after, diagrams, records

Kept from the mockups where a site uses them: `.pair` (2 columns from 1001 px, `.before` on `--pair-before-bg`, `.after` on `--pair-after-bg`; from 1.11.0 `.pair-stack` keeps the 2 boxes one above the other at every width, each as tall as its content, `<div class="msg">` frames quoted text on the inner panel `--pair-panel-bg` in the page's text face, a code block in a box takes the same panel, a stacked before box holding a `.msg` takes the after box's surface, and on a tinted band a before box takes `--pair-before-bg-on-tint`); `.figure` with an inline SVG `.wide` and a `.stack` variant shown at 600 px and below, with a `figcaption`; `dl.record` (tracked `dt`, serif `dd`; `.cols` for a grid of pairs); `.srcview` (source beside view from 1001 px).

### 7.19 Footer

Markup: `<footer class="family wrap"><div class="bar"><ul class="cards rainbow" aria-label="The 7 project sites"><li class="slice slice-N"><a class="card" href="..."><img src="logo.svg" alt=""><span class="cname">Name<span class="tld">.ai</span></span></a></li> x 6<li class="slice slice-vr"><a class="card" href="https://vendorreplies.ai/" ...><img src="vendorreplies.png" alt=""><span class="cname">VendorReplies<span class="tld">.ai</span></span></a></li></ul><ul class="tail" aria-label="Jeff Posluns elsewhere"><li class="tile tile-1"><a class="card" href="https://github.com/jposluns" ...><img class="only-light" src="github-invertocat-black.svg" alt=""><img class="only-dark" src="github-invertocat-white.svg" alt=""><span class="cname vh">GitHub</span><span class="vh"> (opens in a new tab)</span></a></li><li class="tile tile-2">LinkedIn</li><li class="tile tile-3">ORCID</li></ul><div class="tile sigtile"><a class="sig" href="https://posluns.ca/" ... aria-label="A Jeff at Posluns dot ca project: posluns.ca (opens in a new tab)"><span class="pre">A </span>Jeff<span class="gold">@</span>Posluns<span class="gold">.ca</span><span class="post"> Project</span></a></div></div></footer>`.

Behaviour: the bar is 1 flex row of tiles with no gaps, rounded corners (0.5rem) and the exact width of the content (the footer is `.wrap`, so its edges are the page gutter). Each tile carries a 0.4rem band at its top (0.55rem until 1.11.0): the project cards use `palettes.css`'s `.slice-N` gradients (card N's own hue, joined to its neighbours by the shared join colours), with the first card starting and the last card ending on its own hue; the 6 cards start with the current site and continue round the cycle, so the purple-to-red join falls mid-bar on 5 sites; the current site's card has `aria-current="page"`, a visually hidden "(this site)" and a 3 px gold line under the whole tile at every width (from 1.11.1; until 1.11.0 a 2 px line under the name while the names showed); the other 5 link to their sites in a new tab. The GitHub tile's band fades from the last card's hue (`--tail-from`, set per site) to `--band-grey` over its first 20 %; the LinkedIn, ORCID and signature tiles are solid grey. From 1.11.1 project logos are 1.9rem in 2.75rem tiles (2.75rem in 3.4rem until 1.11.0), names 0.85rem and the signature 1.25rem; the 3 marks are 1.5rem (2rem until 1.11.0) with `object-fit: contain`, unaltered, on the common tile surface, and visibly smaller than every project logo; GitHub's black Invertocat shows in light mode and its white one in dark mode (`img.only-light`, `img.only-dark`). Hover and the current cue are an inset gold line. Every target is at least 44 px tall; the bar is 50.4 px tall from 1.11.1.

The seventh card (from 1.11.0): vendorreplies.ai's card, `li.slice.slice-vr`, comes after the 6 rotated cards on every site; on vendorreplies.ai the 6 cards keep their base order (SecureConfig first) and the seventh card is the current one. The last rainbow card's band ends on `--band-silver` (`:has(+ .slice-vr)`), the seventh card's band runs from silver through `--band-sheen` and `--band-silver-deep` back to silver, and the GitHub tile's band starts from silver; below 600 px it stays solid grey. A browser without `:has()` (before Chromium 105, Safari 15.4, Firefox 121) keeps the 1.10.0 colours with a plain silver seventh band.

A site outside the footer (from 1.12.0, NodeKeeper, D-068) has no card in it, and no footer rule or markup changes for it in 1.12.0. On its own pages such a site shows the shared family footer as is (owner, 2026-11-08, attended, D-075): the 6 rainbow cards and vendorreplies.ai's, with the footer markup unchanged; as built, the 6 cards keep their base order (SecureConfig first), vendorreplies.ai's card is last and no card is marked current (no `aria-current`, no "(this site)").

Stages, by the footer's container width, from 1.11.1: above 1159 px (`(width > 1159px)`, the exact complement of the next stage, so that a fractional width falls in 1 stage) the names show (they need 1157 px with the short signature); GitHub, LinkedIn and ORCID sit together beside the signature at 1.35rem, 4.8 px apart, under 1 band that fades from silver (from `--tail-from` on a bar without the seventh card) to grey; the signature drops "A" and "Project" below 1240 px. At 1159 px and below (1.11.0: below 1500 px, with 7 names needing 1493 px at the 1.11.0 sizes) the names are visually hidden (still the accessible names), the 10 tiles share the width left by the signature 7 to 3 (until 1.10.0: full size from 1400 px, names at scaled sizes from 1040 to 1399 px, logos only below 1040 px with 9 tiles) and each logo fills its tile up to 2.75rem; the signature scales between 750 and 650 px, loses "A" and "Project" below 650 px and scales again below 450 px; below 600 px the project cards take the whole first line and the 3 marks and the signature form a second line under a solid grey band, logos and marks back at full size. Measured for 1.11.1 on all 7 sites from 660 to 2000 px in both schemes: no overflow and no clipped name; names from a 1233 px window (a 1280 px window with a 17 px scroll bar gives 1187 px); "A" and "Project" from a 1320 px window; in the logos-only stage the 10 tiles equal (91.3 px each at a 1200 px window); every footer link at least 44 px tall and 24 px wide. Measured for 1.11.0: names from a 1588 px window; at 390 px 2 lines. Without container queries (`@supports not (container-type: inline-size)`) the bar and the cards wrap instead of overflowing.

### 7.20 Disclosure page

Path `/disclosure/`, heading "Disclosure". Content, in order: an opening sentence stating that the page sets out what the project claims, the limit of each claim, and the evidence; the entries, each an `h2` topic with 3 paragraphs led by `<strong>Claim:</strong>`, `<strong>Limitation:</strong>` and `<strong>Evidence:</strong>` (the evidence paragraph holds the links), in the format of AIQT's `DISCLOSURE.md` and written from the site's own sources; a closing paragraph under the label "Security" with 2 links: "To report a security vulnerability, see the vulnerability disclosure policy." (`https://posluns.ca/security/`) and the universal privacy policy (`https://posluns.ca/privacy/`), both with the new-tab hint; the Next block. The rail lists the entries. From 1.6.0, before the Security paragraph, a paragraph under the label "Your browser" states what the light and dark switch stores: "If you use the light and dark switch, your choice is kept in your browser's local storage under the name scheme, so the site opens in that scheme next time. It is not sent to this site, and nothing is stored until you press the switch." (`reference/disclosure.html`). A site checks its own Disclosure claims against this: a page that says the site stores nothing in the browser is inaccurate from 1.6.0.

### 7.21 Channel list page

A document page whose sections are the channels or contexts a standard adapts to (cleanlanguage.ai's `/channels/`): the kicker, `h1`, the rail listing every channel, a meta line, a callout naming the source file, then per channel a section label (`Channel N`), an `h2` with the channel's id and 1 list of its rules (`ul`), with a paragraph before or after the list where the source has one; the Next block. No new CSS: it uses the document grid, the rail, the callout and the document list styles.

### 7.22 Checklist page

A document page whose sections are groups of questions (cleanlanguage.ai's `/checklist/`): the kicker, `h1`, the rail listing the groups, a meta line, a source callout, then per group a section label (`Section N`), a numbered clause heading (`h2.clause` with its number) and the questions as a numbered clause list (`ol.clauses`, items `N.1`, `N.2` and so on), so that a question can be cited by number; the Next block. No new CSS: it reuses the numbered clauses of section 7.8.

### 7.23 Demo page

A page that shows the result and gives the reader a prompt to try (cleanlanguage.ai's `/demo/`): the kicker, `h1`, a rail of its 3 parts, a meta line; "Watch it work": 1 before-and-after pair (section 7.18) under a block label naming the pattern, and a link to the full set of examples; "Try it on your own writing": the explanatory paragraphs, the prompt in a copy block (`pre.hascopy` with `copy.js`; a `.code-note` line says what to do without JavaScript) and the fallback for an assistant that cannot browse; "Make it your default": the install link in the Next block. No new CSS.

### 7.24 About this project (landing page)

Markup, directly after the value paragraph's region: `<div class="region"><p class="lbl">About this project</p><div><p class="about"><a href="/disclosure/">Disclosure</a><span class="dot" aria-hidden="true">&middot;</span> <a href="/license/">Licence</a><span class="dot" aria-hidden="true">&middot;</span> <a href="https://posluns.ca/privacy/" target="_blank" rel="noopener noreferrer">Privacy<span class="vh"> (opens in a new tab)</span></a><span class="dot" aria-hidden="true">&middot;</span> <a href="https://posluns.ca/security/" target="_blank" rel="noopener noreferrer">Security<span class="vh"> (opens in a new tab)</span></a></p></div></div>`. 1 line in the label voice (Public Sans 500 0.8rem, tracked uppercase, `--text-muted`), the 4 links separated by middle dots.

From 1.7.0 there is 1 ordinary space after each dot span and none before it, so the line breaks only after a dot and a dot never starts a line. The dot's right padding is `calc(0.5rem - 0.36em)`: it gives up the space's width (4.59 px at the line's 12.8 px, the space and its 0.12em tracking in Public Sans), so the gaps either side of the dot are equal; measured from the link's last glyph to the dot and from the dot to the next link's first glyph, 8.00 and 7.98 px at 1280 and 390 px (8.00 and 12.59 px before the trim). On a page still on the older markup, with no space, the gap after each dot is about 3.4 px, so a site moves to the new markup. Until 1.6.1 the links and dots touched, so the line could not break: at 300 to 360 px the page was 379 px wide (reported from aiqt.ai and opfiles.ai, reproduced by the template owner). Measured on `reference/landing.html` 1.7.0 in Chromium 153 at 300, 320, 340, 360 and 390 px, light and dark: no horizontal overflow, 2 lines, no line starting with a dot, no link split across lines (10 of 10 cases; at 1280 px, 1 line). For pages still on the older markup, `.region .about` carries `overflow-wrap: anywhere`, which splits a word only when nothing else fits: measured with the 1.6.1 page on the 1.7.0 stylesheet, the page no longer overflows at any of those widths, but the line breaks inside a word at 320 to 360 px and a dot starts the 2nd line at 300 px, so a site moves to the new markup. The template's overflow checks include 320 px. It is the place that links the Disclosure and Licence pages (the value paragraph's own licence link may stay); the inner pages and the footer do not link them.

### 7.25 Inline accent

Markup: `<span class="accent">28</span>` around a number or key term in running text, a table cell included. It sets `color: var(--accent)` and lining numerals, with no change of size, weight or face, so each site shows its own text-safe accent (4.5:1 or more on every surface, per `contrast-report.txt`). The colour is emphasis only: the sentence must read the same without it, so no meaning depends on colour (WCAG 2.2, success criterion 1.4.1). Use it sparingly: a count or a key term, never a whole sentence. `--brand` is never used for text (section 6).

From 1.11.2 the warm variant: `<span class="warm">28</span>` sets `color: var(--ink-warm)` and lining numerals, exactly as `span.accent` does. `--ink-warm` is a component token (section 6.4) defaulting to `var(--accent)`, so until a site overrides it in its `site-palette.css` the warm span shows the site's accent and every page renders as before; `tools/check_palette.py` holds the token at 4.5:1 as text on the 5 surfaces, the 2 row tints, `--band-tint-bg`, `--pair-after-bg` and `--box-bg`, and blocks on failure. The same constraints apply: emphasis only, sparingly, a count or a key term, never a whole sentence, and no meaning on colour. Any value that passes the gate is lawful, with 1 exception: `--brand`, or its value, is never used for text (section 6), whether or not it would pass the gate (it also misses the 4.5:1 text floor on 6 of the 7 sites in light mode on these grounds). A palette's accent or a value chosen for text is the expected override.

### 7.26 Compact figures

Markup: `<div class="figures compact">` with the usual `.fig` children (`.num`, `.cap`), up to 6. The figures share the text column's width equally on 1 line, with 2.2rem numerals and tighter caption tracking; at 900 px and below they set in rows of 3. The rule above each figure stays `--brand` and the numerals `--accent`. Use the standard `.figures` row for 2 to 4 figures that need the larger numerals.

### 7.27 Figure beside text

Markup: `<div class="fig-aside"><div class="fig">...</div><p>...</p></div>`: 1 figure and a short paragraph side by side, the paragraph aligned with the figure's top; stacked at 600 px and below. For a total and its parts, the parts stated in the paragraph.

### 7.28 Brand marker

Markup: `<mark class="marker">labelled and evidenced</mark>`. A band of the site's `--brand` behind the lower part of the line (38 % strength where the browser supports `color-mix()`, otherwise a thin solid band); the text keeps `--text` and the paragraph's type. Underlining is not used, because links are underlined. Emphasis only, sparingly, as for 7.25. The class is `marker`, never `brand`, which is the top bar's wordmark.

### 7.29 Search and facets

Section 7.13 gives the form and the list; this section gives the options of `search.js` from 1.5.0, each opt-in by markup. They are a separate section because they add a contract of their own (attributes, address parameters, status wording), and 7.13 stays the index-list component. Reference pages: `search.html` (results mode with facets) and `facets.html` (default mode with facets).

- **Default mode, unchanged from 1.4.1.** An empty query shows every row and leaves the status empty; a query shows the rows whose `data-search` text (or, without it, the row's text) holds every term, and the status reads "N of M documents match.". On a page without facets this wording stays. A page on 1.4.1 markup behaves as before: measured on grclibrary.ai's mockup search page (36 rows) for 12 queries, the hidden rows, the row order and the status text are identical under the 1.4.1 and 1.5.0 scripts.
- **Results mode.** `data-search-mode="results"` on the scope: with an empty query and no active facet no row shows, and the status shows a prompt, `data-search-prompt` on the scope or "Type to search the documents."; a query, or an active facet, shows only the matching rows.
- **Title ranking.** On in results mode, and in default mode with `data-search-rank="title"` on the scope: among the matching rows, those whose title holds every term come first, and each group keeps page order. The title is the row's `data-title`, or else the text of its first `a` without any `.vh` text. Clearing the query restores page order. Ranking moves rows within their own list only.
- **Facets.** A chip group is any element with `data-filter-group="<group>"` (lowercase letters, digits and hyphens; never `q`), usually `<ul class="chips">`, holding links or buttons with `data-filter="<value>"`; `data-filter=""` is the group's All chip. Each row carries `data-<group>`, a space-separated list of values (for example `data-fw="iso27001 csf"`). 1 value per group is active; a second click on the active chip, or a click on All, clears the group. A row shows when every active facet's value is among its tokens and it matches the query. Values compare in lowercase. The chosen chip carries `aria-pressed="true"` and the others `"false"` (All is pressed when its group is clear). Because `aria-pressed` belongs to buttons, the script gives a link chip `role="button"` and makes the space key activate it, as a toggle; a modified click (a new tab or window) is left to the browser, so the link still works, and without script every chip is a plain link to a real page. Rows are hidden with the `hidden` attribute. On a page with facets the status reads "Showing N of M documents.".
- **The address.** On load, `?q=` fills the input and `?<group>=<value>` sets a facet (a value with no chip in that group is ignored). As the reader types or clicks, the script writes the state back with `history.replaceState`, removing a parameter whose value is empty and keeping every other parameter and the hash; no history entry is added. This applies in both modes: in default mode it changes no row and no status for a given query, and a page on 1.4.1 markup had no use for a `q` parameter, so it is not opt-in.
- **Without script** every row shows, every chip is a plain link and the input does nothing, as in 1.4.1.
- **Chip lists start at the text edge (from 1.8.0).** `ul.chips` has no list indent, also inside `.doc`, whose lists are indented 1.2rem; until 1.7.0 a chip list in a document started about 19 px right of the text edge.
- **Chips that would show nothing (from 1.7.0, grclibrary.ai's request, owner, 2026-11-04).** Opt-in per group, on the group's element: `data-filter-empty="disable"` and, optionally, `data-filter-count="live"`; any other value counts as absent, and a page with neither on any group runs as in 1.6.1 (measured: the DOM and the address after the same 7 actions are identical under the 1.6.1 and 1.7.0 scripts on `search.html` and on `facets.html` with the attributes removed, 32 of 32 snapshots). The served HTML is unchanged; only the script sets the new attributes.
  - *Would-show count.* Recomputed on load, on every input and on every chip choice: for each chip of an opted-in group, the rows that hold every query term, the chosen value of every other group and the chip's value (the chip's own group's choice is ignored, because choosing replaces it); for All, without this group's condition. A row with several values in a group counts toward each. At load the script prepares each row's lowercased text and group tokens once; each update is 1 pass over the rows, finding which conditions a row fails: none, and it counts toward its values in every group and every All; only 1 group's, and it counts toward that group's values and All; the query, or 2 or more groups, and it counts nowhere. The DOM changes only where a chip's state or count changes.
  - *Unavailable chip.* With `data-filter-empty="disable"`, a chip whose count is 0 takes `aria-disabled="true"` (never the `disabled` attribute), loses it when the count rises, and stays in the tab order. All is never unavailable, nor is a chosen chip, even when the result is empty, so a click can still clear it; the script never drops a choice by itself. Activating an unavailable chip (a click, Enter on a link, Space through the chip's key handler) changes no choice, row or address, and the status reads "<Chip label> would show no documents with the current choices." until the next change; focus stays on the chip. A modified or middle click is left to the browser, as in 1.6.1. Each unavailable chip points through `aria-describedby` to 1 note per page, `<p id="filter-empty-note" hidden>Would show no documents with the current choices</p>`, created by the script; the note carries `hidden` so that it is never read as page text on its own, while a description is still computed from it. There is no live announcement per chip; the result stays in the status, "Showing N of M documents.".
  - *Look.* `.chips [aria-disabled="true"]`: a dashed border in place of the solid one, `cursor: not-allowed`, and no hover change, with no fading and no transition. From 1.9.1 an available chip reads in `--chip-fg` (`--text`) with a `--chip-border` (`--text-muted`) border, and an unavailable chip in `--chip-unavailable-fg` (`--text-muted`) with a dashed `--chip-unavailable-border` (`--rule-strong`), and an unavailable chip is greyed out: its word struck through (the count, `display: inline-block`, is not), `--chip-unavailable-fg` (`--text-muted`) on `--chip-unavailable-bg` (light `--rule`, dark `--bg`) with a dashed `--chip-unavailable-border` (light `--rule`, so no visible border; dark `--rule-strong`). Until 1.9.0 both states read in `--text-muted` with a `--rule-strong` border. The 1px dashed border in `--rule-strong` measures at least 3.05:1 on the page background on all 6 sites, in the 6 states of section 6.1 (device light or dark; no `data-scheme`, `light` or `dark`), so no 2px border is needed.
  - *Live counts.* With `data-filter-count="live"`, each chip's `.n` text becomes its would-show count (an unavailable chip reads 0, a second cue in text); without it `.n` keeps its served count. A page whose text says the counts never change must say otherwise.
  - *Results mode.* Counts ignore the rule that no row shows until the reader types or picks a chip, so on an idle results page a chip is unavailable only when no row holds its value; ranking does not affect counts.
  - *The address.* Read and written as in 1.5.0; a refused value is never written. A link that opens on an empty combination keeps its values: the chosen chips stay selected and available, the list is empty, and the status reads "Showing 0 of M documents.".
  - Reference page: `facets.html`, whose Domain group is opted in, Type group opted in with live counts, and Framework group left as in 1.6.1. Measured in Chromium 153 (headless, Playwright): 51 checks, 0 failing, 0 console errors (the request's checks 1 to 5; 26 page states compared chip by chip against counts computed independently from the rows; results mode; the look; tab order). Not tested: screen readers (NVDA, JAWS, VoiceOver), the request's check 6. Speed on a page of 3,000 rows with 3 groups of 20 chips, all opted in with live counts, headless Chromium on the build machine, the median of 14 typed updates: 1.8 ms for the script and 7.6 ms with style and layout; with the CPU throttled 4 times through the DevTools protocol, 10.5 ms and 40.4 ms (the same page without the attributes: 12.5 ms and 36.7 ms). These are headless measurements, not a phone.

### 7.30 Matrix with presets

Markup: presets as links (recommended, so that each leads to a page that resolves without script) or buttons, anywhere on the page, each with `data-preset="<key> <key> ..."` (the column keys it shows) and `data-preset-id="<id>"` (its name in the address); the All preset has `data-preset=""` and shows every column. `<p class="matrix-status" role="status" aria-live="polite" data-matrix-status></p>` above the table. The table: `<div class="tablewrap bounded" tabindex="0"><table class="matrix sticky sticky-col" data-matrix><caption class="vh">...</caption><thead><tr><th scope="col">Domain</th><th scope="col" data-col="<key>">Name<span class="org">Organization</span></th>...</tr></thead><tbody><tr><th scope="row">Row</th><td data-col="<key>"><code>id</code><a href="...">count<span class="vh"> documents</span></a></td>...</tr></tbody></table></div>`. 1 matrix per page.

`matrix.js` shows the chosen preset's columns and hides the others with the `hidden` attribute on their `th` and `td`; the row-header column carries no `data-col` and always shows. The status names the preset and the columns shown ("View: Security programmes. Showing 4 of 8 columns."). The chosen preset carries `aria-pressed="true"` when it is a button (the others `"false"`) and `aria-current="true"` when it is a link (the others none): a preset choice is a choice of view among links, so a link keeps its link role and is marked as the current item of its set, as the install selector does (section 7.12); `aria-pressed` is not valid on a link. `?view=<id>` chooses a preset on load (an unknown id is ignored, and the All preset is chosen); a choice is written back with `history.replaceState`, and All removes the parameter. A modified click is left to the browser. Without script every column shows and each preset is a plain link (grclibrary.ai points them at its per-preset static pages). The table has a single header row by design, so the script has no `colspan` handling: a matrix with grouped or spanning headers is out of contract. Cells hold short identifiers and count links only, never third-party control text. Reference page: `crosswalk.html`.

From 1.7.0 the script has 2 more options, each opt-in by markup; a page without them behaves exactly as above (the 1.5 and 1.6 matrix checks, 54 of 54, pass unchanged on `crosswalk.html` 1.7.0).

- **Column toggles.** `<div class="matrix-toggles" data-col-toggles hidden>` holding 1 `<button type="button" data-col="<key>" aria-pressed="true">` per column, optionally in groups, each a `<p class="lbl" id="...">` naming the group and a `<div class="chips" role="group" aria-labelledby="...">` of buttons. Without script the container keeps `hidden` and every column shows. The script reveals it. A press shows or hides that column, setting `hidden` on its `th`, its `td` and any `col` with the same `data-col` (in a fixed-layout table a column's width comes from its `col`, so a hidden column hides its `col` too); `aria-pressed` follows; focus stays on the pressed button; nothing animates. The status reads "Showing N of M columns.", or, when the columns shown are exactly a preset's, that preset is chosen and the status keeps the preset wording. Presets and toggles stay in sync both ways. The pressed state is shown by `aria-pressed` and the chips' 2px accent border, not by colour alone.
- **The address.** A set of columns that is a preset's is written as `?view=<id>`; any other set as `?cols=<key>+<key>` (space-separated keys, so `+` in the address) with `?view=` removed; all columns removes both, with `history.replaceState`, as for presets. `?cols=` is read on load only on a page with toggles; unknown keys are ignored, and with no known key `?view=` applies. This keeps 1 parameter per state and the preset form for any set a preset names.
- **Column highlight.** `data-col-highlight` on the table: while the pointer is over a cell, or keyboard focus is inside one, every cell sharing its `data-col` (the header cell included) takes the class `colhl`, drawn in `--row-hover`, and loses it on leaving. Focus is never moved, and the row-header column has no `data-col` and is never highlighted. A heat cell keeps its step. Reference page: `matrix.html`.

### 7.31 Sticky scroll box

A sticky header can stick only to its nearest scroll container, and `.tablewrap` is one, so until 1.5 the header of a `.sticky` table stuck to an unbounded wrapper and left the screen with the page. Markup from 1.5.0: `<div class="tablewrap bounded" tabindex="0"><table class="sticky [sticky-col]">`. `.tablewrap.bounded` is a box at most 75 % of the viewport height (`75svh` where supported, `75vh` otherwise) that scrolls in both directions, with `overscroll-behavior: contain`; inside it the header row stays at the box's top. `table.sticky-col` keeps the first column at the box's left edge. From 1.7.0, opt-in, `table.sticky-col.pin-cap` gives the first column a capped width, `clamp(8rem, 30vw, 15rem)`, and lets its text wrap, so a long row label cannot take most of a phone screen; the first column's `th` holds the row's name, not a paragraph. `--pin-col` overrides the width, set by a class on the table: `pin-r6` and `pin-r8` (6rem and 8rem), and `pin-r10` to `pin-r20` in steps of 2 (`clamp(8rem, 30vw, Nrem)`, so they change only the cap). In a fixed-layout table, leave the first `col` without a width class, so that the cap sets the width. `table.sticky-col` without `pin-cap` renders exactly as in 1.6.1 (owner, 2026-11-04): measured on `crosswalk.html`, which does not use the class, the pinned column is 109, 109, 117 and 143 px at 320, 390, 1280 and 1512 px in both versions, with the same row heights. Sticky cells take the page background (`--bg`), or from 1.7.0 the row's opaque tint (section 7.34), so scrolled content never shows through, and the stacking order is the first column 1, the header row 2 and the top-left cell 3, so the corner sits above both. A sticky table uses separated borders with no spacing, so the header hairline moves with the header. Markup built on 1.4.1 (`.tablewrap` holding `table.sticky`, without `.bounded`) is bounded through `.tablewrap:has(> table.sticky)`, in a rule of its own so that a browser without `:has()` still applies the `.bounded` rule and renders the 1.4.1 markup as before. Measured in Chromium 153 (headless, Playwright) at 390, 820, 1180 and 1512 px in light and dark: after a page scroll that puts the box's top 100 px below the window's top and an inner scroll of 500 px down and 300 px across, the header row's top equals the box's top, the first column's left edge equals the box's left edge, the top-left cell is the topmost element at its position, and the page does not scroll sideways. Support in other browsers was not tested. The box is bounded by design: when the page is scrolled past the box's top, the header leaves the screen with it. 75 % leaves a quarter of the window outside the box at every height, so the reader can always scroll the page past it. From 1.5.0 `.tablewrap` is also `position: relative`, so visually hidden text inside a cell stays inside the box and cannot widen the page.

### 7.32 Left-rail page

Markup: `<div class="doc left-rail">` holding, in order, the kicker, the `h1`, the side list `<div class="lrail"><img class="rail-logo" src="logo.svg" alt="..."><nav aria-label="..."><p class="lbl">Label</p><ul><li><a href="...">Item</a></li>...</ul></nav></div>`, then the page's blocks. The page has no right rail and no label column: the grid is the side list at the rail's width (`--railw`) on the left and the main column, and labels sit above their content, as at 1000 px and below on other pages. The main column gains the label column's width and 1 column gap. The logo appears once, above the side list, as it sits above the rail on other inner pages. The list stays in view while the page scrolls (`position: sticky` at 1.5rem from the top, as the right rail, in the rail's type, with an inner scroll if it is taller than the window). At 1000 px and below the page is 1 column and the side list sits under the `h1`, not sticky, with the logo capped at 7rem and the list in 2 columns (1 at 600 px and below), as the right rail does. In the source the list follows the `h1`, so keyboard focus reaches it before the page's content. It works without script; with `matrix.js` its links can be the matrix presets (section 7.30). `rail.js` reads only `nav.rail` and so leaves the side list alone; a left-rail page has no `nav.rail`, so the script does nothing there (checked on `crosswalk.html` with `rail.js` added: no error and no `aria-current="location"` mark). Measured on the reference pages at 1512 px: the main column is 1168 px on the left-rail page and 979 px on a standard document page.

### 7.33 Light and dark switch

From 1.6.0, on every page, the 404 page included. Markup, the last child of `header.mast`, after `nav.nav`:

`<button class="scheme" type="button" data-scheme-toggle aria-pressed="false" hidden><svg class="moon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M20.5 14.2A8.5 8.5 0 0 1 9.8 3.5a8.5 8.5 0 1 0 10.7 10.7z"/></svg><svg class="sun" ...><circle cx="12" cy="12" r="4.2"/><path d="M12 2.5v2.2M12 19.3v2.2M2.5 12h2.2M19.3 12h2.2M5.3 5.3l1.6 1.6M17.1 17.1l1.6 1.6M5.3 18.7l1.6-1.6M17.1 6.9l1.6-1.6"/></svg><span class="vh">Dark mode</span></button>`

with `<script data-cfasync="false" src=".../scheme.js"></script>` in `<head>`, before the stylesheets, with no `defer` or `async` (the reference pages carry the full markup of both glyphs). The 2 glyphs are drawn for the package in inline SVG with presentation attributes only, so the CSP needs no change and no icon set is loaded.

- **Place.** The 3rd column of the top bar, after the links and GitHub, outside the scrolling `nav`, so it never scrolls away. At 1000 px and below it sits at the right of the wordmark's row, above the scrolling links. Its right margin of -0.625rem lines the glyph up with the page gutter; its block margins of -0.25rem keep the bar at its 1.5.1 height. The gap after the wordmark is the wordmark's margin (0 at 1000 px and below, where the wordmark has its own column), so the 3rd column takes no space when the button is absent or hidden: the 1.5.1 pages on the 1.6.0 stylesheets and the 1.6.0 pages without script lay out the bar exactly as 1.5.1 (measured on the 14 reference pages at 6 widths from 390 to 2000 px).
- **Look.** A 24 px glyph in a 44 by 44 px target (larger than the 24 px that section 10 requires outside the footer), `--text-muted` at rest and `--text` on hover, the standard focus ring. The glyph shows the scheme a press leads to: a moon while light shows, a sun while dark shows; CSS chooses it from `prefers-color-scheme` and `data-scheme`, so it is right before the script has run.
- **Name and state.** The accessible name is "Dark mode" (visually hidden text); `aria-pressed` is `"true"` while dark shows and `"false"` while light shows, whether the scheme comes from the device or from a press. This is the toggle button of the WAI-ARIA Authoring Practices button pattern (https://www.w3.org/WAI/ARIA/apg/patterns/button/, read 2026-11-03), which says that "it is critical the label on a toggle does not change when its state changes"; the glyph changes with the state and the name stays the same.
- **Behaviour.** Until the reader presses it, the page follows the device setting and the button follows device changes. A press sets `data-scheme="light"` or `"dark"` on `<html>` (the attribute `palettes.css` and `baseline.css` already honour; `color-scheme` follows it, so scrollbars and form controls match) and stores the same value in `localStorage` under the key `scheme`. On a later page `scheme.js` reads the stored value and sets the attribute while `<head>` is parsed, before the first paint, so the page never shows the other scheme first; the same file wires the button once the DOM is ready (1 file: the part that must run early is 2 lines, and a second request would buy nothing). Every storage access is wrapped in `try`, so blocked storage leaves the switch working for the current page. A stored choice that equals the device setting stays stored, because the reader chose it. There is no 3rd "automatic" state: a reader who never presses the switch keeps the automatic behaviour, and offering a way back to it would need a 3-state control in place of a 2-state button, which the owner did not ask for; clearing the site's storage restores it.
- **Without script** the button keeps its `hidden` attribute and is never shown, so no reader sees a switch that does nothing; the page follows the device setting as in 1.5.1.
- **Cloudflare.** `data-cfasync="false"`, placed before `src`, keeps Rocket Loader from deferring the file (Cloudflare's documentation, https://developers.cloudflare.com/speed/optimization/content/rocket-loader/ignore-javascripts/, read 2026-11-03: the attribute "must be added before the `src` attribute"). Not tested: if Rocket Loader deferred the file, the stored scheme would apply only after the page had painted in the device scheme. A site gate that allows only certain attributes on an external `<script>` must admit `data-cfasync="false"` on the `scheme.js` element, placed before `src`. A `data-*` attribute does not change whether or how a browser runs the script; only Cloudflare reads it.

Measured in Chromium (headless, Playwright) on the 14 reference pages, device light and dark: 588 checks, 0 failing (button shown at 44 by 44 with a 24 px glyph; a press toggles `data-scheme`, `aria-pressed`, `color-scheme` and the colours; after a reload the attribute is set before `<body>` is parsed and `--bg` already has the stored scheme's value at the first animation frame and at `DOMContentLoaded`; device changes followed while nothing is stored; with `localStorage` replaced by a getter that throws, no error and the switch works; without script the button has no box; Tab reaches it after the top bar's links (8 or 9 presses from the top of the page), and Enter and Space toggle it; at 390 px it stays in place while the links scroll). Other browsers were not tested.

### 7.34 Large matrix tables

From 1.7.0 (owner, 2026-11-04, the table request for crosswalk pages, parts 1 and 2). Every capability is opt-in by a class or an attribute. Reference page: `matrix.html`, 10 domains by 20 instruments with every capability below; its instrument names and organizations are real, and its groups, identifiers, counts and shading are illustrative sample data.

**The combined form.** This is the accessibility contract for a large matrix:

```html
<p class="matrix-status" role="status" aria-live="polite" data-matrix-status></p>
<div class="tablewrap bounded" tabindex="0" role="region" aria-label="Sample matrix: 10 domains by 20 instruments">
<table class="matrix fixed sticky sticky-col pin-cap cells-wrap zebra rowhover" data-matrix data-col-highlight>
<caption class="vh">Sample matrix: 10 domains by 20 instruments, in 4 groups</caption>
<colgroup><col><col class="r12" data-col="iso27001"> ... <col class="r6" data-col="dora"></colgroup>
<thead><tr><th scope="col">Domain</th>
<th scope="col" data-col="iso27001" data-grp="1">27001<span class="org">ISO/IEC</span><span class="grp">Security</span></th> ...
<th scope="col" class="rot" data-col="dora" data-grp="4">DORA<span class="org">EU</span><span class="grp">Resilience</span></th></tr></thead>
<tbody><tr><th scope="row">Governance</th>
<td data-col="iso27001"><code>EX.1.1</code> <code>EX.1.1a</code><a href="...">3<span class="vh"> documents</span></a></td>
<td class="na" data-col="csf">none</td> ...
<td class="nowrap" data-col="dora" data-heat="2"><a href="...">4<span class="vh"> documents</span></a></td></tr> ...
</tbody></table></div>
```

The wrapper is focusable (`tabindex="0"`), a named region (`role="region"` with an `aria-label` naming the table), so keyboard users can reach the box and scroll it with the arrow keys, and the standard `:focus-visible` ring shows where they are. Column headers carry `scope="col"` and row headers `scope="row"`. A `caption` (visible or `.vh`) or a heading before the table names the matrix. The top-left cell sits above both pinned axes (stacking order: first column 1, header row 2, corner 3) on an opaque background. Sticky positioning never changes the DOM order, so screen readers read the table in source order.

- **Column widths in rem (1.1).** In a `table.fixed`, `col.r6` to `col.r24` in steps of 2 set a per-column custom property, `--colw`, and 1 rule, `.fixed col { width: var(--colw); }`, applies it; a `col` without one of these classes leaves `--colw` unset, so its width stays `auto`, and the `col.w<N>` percentages keep their higher-specificity rules. The choice: inline `style` is forbidden by the CSP (`style-src 'self'`) and by section 12, so a per-column value must come from markup that the stylesheet maps; classes on `col` are already the pattern of `col.w<N>`, and a `data-colw` attribute would need the same 10 mapping rules. In fixed layout the table's width is the larger of its `width` and the sum of its columns, so a matrix whose columns add up to more than the page grows past it and scrolls inside `.tablewrap`. Use rem classes or percentages in 1 table, not both. Measured: 96, 128, 160, 192, 224, 256, 288, 320, 352 and 384 px for `r6` to `r24` at the 16 px default; on `matrix.html` the table is 3312 px wide in an 866 px box at 1180 px, with no page overflow at 320, 390, 820, 1180, 1512 or 2000 px.
- **Wrapping cells (1.2).** `table.matrix.cells-wrap`: data cells wrap within their column (`white-space: normal`, identifiers in `code` included); headers keep their rules. `td.nowrap` keeps a cell on 1 line, for example a count-only column, and `td.id` keeps its own rule. Measured on a test table with 10rem columns: a wrapping cell of 4 identifiers and a count runs to 2 lines (68 px), a `td.nowrap` cell stays on 1 line (44 px).
- **The pinned column (1.3).** Section 7.31: `table.sticky-col.pin-cap`, `clamp(8rem, 30vw, 15rem)`, `--pin-col` through the `pin-r<N>` classes, labels wrap. The 8rem minimum keeps at least half of the window for data on phones (owner, 2026-11-04: half of the window, not of the box). Measured on `matrix.html` as the data visible in the window, from the pinned column's right edge to the box's right edge: at 320 px the pinned column is 128 px (right edge at 144 px) and the data area 160 px, 50.0 % of the window; at 360 px 200 px, 55.6 %; at 390 px 230 px, 59.0 %.
- **Row guides (1.4).** `table.zebra` tints even body rows with `--row-tint`; `table.rowhover` tints the row under the pointer with `--row-hover`. The tint applies to every cell of the row, the sticky `th` and `td` included, and the tints are opaque, so a pinned cell keeps the row's colour and scrolled content never shows through. No transition. Hover is a reading aid only and does nothing on touch, which loses nothing.
- **Empty cells (1.5).** `<td class="na">none</td>`, with the site's own word: muted (`--text-muted`), centred, the word kept in the cell so that screen readers and copied text get the same answer as sighted readers. It passes 4.5:1 on every surface and row tint (section 6.1).
- **Heat shading (2.2).** `td[data-heat="0"]` to `"4"` in a `table.matrix`, mapped to `--heat-0` to `--heat-4`, 1 ramp per site and scheme. The count stays printed in the cell, so colour is never the only carrier; a heat cell's own text is `--text` and its links keep `--link`, the colours each step is checked against. A heat cell keeps its step under the row and column tints. From 1.8.0 the focus ring inside a heat cell (on its count link, or on the cell) is drawn in `--text` in place of `--focus`: every step holds `--text` at 4.5:1 or more, so the ring keeps 3:1 against any step, and the light ramp is no longer limited by `--focus` (section 6.1). The mapping from a count to a step is site logic. The legend: `<ul class="chips heat-legend" aria-label="..."><li><span class="tag" data-heat="0">1 document</span></li> ... </ul>`, each step named in words on its own shading. Heat drops to white in print.
- **Rotated headers (2.4).** `th.rot` sets the header text vertically, read from the bottom up, with `writing-mode: sideways-lr` where the browser supports it and `vertical-rl` (read from the top down) where it does not; it is capped at 11rem and wraps to a 2nd line beyond that. There is no transform: rotating the cell would also move its header hairline to the top. The text is unchanged, so screen readers read it as written. For a short visible form, put the full name in a `.vh` span in the same `th`, for example `CCM<span class="vh"> (Cloud Controls Matrix)</span>`, so every reader gets the full name; never a `title` attribute as the only carrier.
- **Column groups (2.6).** `data-grp="1"` to `"6"` on `thead th` draws a 3px accent under the header cell in `--grp-1` to `--grp-6` (the family's 6 hues, each text-safe on the site's surfaces), and `<span class="grp">Group</span>` in the header cell names the group in the same colour. The group is named in text (the tag line, and the toggle group's label where there are toggles); the colour reinforces it. From 1.8.0, while every visible column belongs to 1 group, `matrix.js` sets `data-one-grp` on the table and the tag lines are not shown: they would repeat 1 name on every column and only add width. The accent under each header stays, and the tags return as soon as a second group is visible. A static page with no `matrix.js` controls keeps its tags.
- **Print (2.5).** In `@media print`, from 1.8.0 for every table (in 1.7.0 for `table.matrix` only): every `.tablewrap` loses its height cap and its scrolling; sticky cells are static; `thead` repeats on each page and rows do not split where they fit; each table takes the page's width, a `table.fixed` drops its 56rem minimum and its rem column widths and is laid out by its content (a `table.matrix` keeps its fixed layout; `col.w<N>` percentages stay); every cell wraps, a long token breaking anywhere, so no column runs past the paper's edge; and a rotated header runs its organization and group lines on after its name, so it stays 1 line wide. For `table.matrix`, the column toggles and the presets are hidden, and every cell, and the links, identifiers and tag lines in it, is black (`--print-ink`) on white (`--print-paper`), with no tint or heat. A matrix with many columns becomes narrow per column on paper (the 20-column sample: about 23 px per column at A4 width): print a preset view, which keeps only its columns. Measured in Chromium's print layout at A4 and Letter printable widths (718 and 739 px): on `reference.html`, `crosswalk.html`, `matrix.html` and `matrix.html?view=ai`, no table box clips and no table or cell content runs past the page; in 1.7.0 the register printed 675 px of its 1219 px table, and the crosswalk ran 194 px past the page at A4 width. In an A4 PDF of `reference.html` the register's last column is present on all 17 rows (in 1.7.0 on none).
- **Accessibility.** The focus ring keeps 3:1 on every surface it is drawn on, including the heat steps (in `--text` there, from 1.8.0). Every colour comes from palette tokens, with text at 4.5:1 in both schemes (section 6.1); nothing depends on colour alone (counts, words and group names are text); no rule in this section has a transition or an animation, so there is no motion to reduce (a future one must respect `prefers-reduced-motion`); hover and the column highlight are reading aids, and nothing exists only on hover.

Measured in Chromium 153 (headless, Playwright) on `matrix.html` and a test table: 90 checks, 0 failing, 0 console errors (widths at 7 window widths in both schemes; stripes and hover on sticky cells; contrast read from the rendered page for `td.na`, heat cells and legend, text on the row tints and the group tags, on all 6 sites in both schemes, lowest 4.62:1 (heat cells 4.91:1); rotated headers; print emulation; toggles, the address and the highlight; no script). Print emulation renders 1 long page, so page breaks and the header row repeating on paper were not observed; the stylesheet sets `thead` to `table-header-group` and `break-inside: avoid` on rows. Other browsers were not tested.

### 7.35 Link-preview cards (from 1.9.0)

`tools/build_card.js` builds a site's link-preview card from 1 data file, `site-card.toml` (the fields are in `tools/site-card.example.toml`, the template the site writes its own file from and does not copy; section 4, item 2), and 1 owner-approved illustration SVG. It renders in headless Chromium (from 1.11.2 the full Chromium build: the builder resolves `chromium.executablePath()` once, launches both browsers with exactly that executable, and refuses Playwright's default `chrome-headless-shell` build, whose text edges differ, exiting 1 with the message `the resolved browser build is <name>, a headless shell, not full Chromium; install the full build with "npx playwright install chromium" and rerun`) with the bundled Inter 4.000 (`tools/card-fonts/`, Regular, Bold and ExtraBold, SIL OFL 1.1, never served), refuses every network request, and writes:

- `<site>-card.png`: 1200 by 630 px, reduced to 256 colours (median cut, Floyd-Steinberg; from 1.9.2 the error passed on is capped at 24 levels per channel, and a pixel whose error-steered colour lies more than 24 levels from its own takes the palette colour nearest its own instead, which can itself lie further than 24 levels away where the palette holds no close colour; the background check holds the card outside the safe area within 32 levels of the full-colour render; until 1.11.4 this sentence said a dithered pixel stays within 24 levels), served at `/images/<site>-card.png`;
- `<site>-card-safe.png`: the same render with the layout's safe area outlined in red dots and, from 1.10.0, the Teams crop in cyan dashes, for review, never served;
- `<site>-card-teams.png` (from 1.10.0): x 208 to 992 of the card at full height, 784 by 630, the part Microsoft Teams shows, for review, never served;
- `<site>-touch.png`, when the data file names a logo: 180 by 180, the logo at 144 px centred on the site's light `--bg`, opaque, as 1.1.0 made the icons in `logos/`; from 1.12.2 a PNG logo (at most 180 by 180 px) is placed unscaled at the centre instead, its transparent pixels blended over the background (vendorreplies.ai: `vendorreplies.png` on `touch_bg = "#0b121c"`);
- `<site>-favicon.ico` (from 1.12.2; owner, 2026-11-09, D-080), when the data file names a `favicon`: 3 frames, 16, 32 and 48 px, 32-bit RGBA stored as PNG, served by the site at `/favicon.ico` (section 5, item 1). From the site's `<site>-favicon.svg` each frame is rendered at its own size on a transparent page in the same Chromium as the card, so no frame is a downscale of a larger one. From a PNG, for a site whose logo is photographic artwork (vendorreplies.ai), the visible pixels are centred on a transparent square and each frame is downscaled from it with Pillow's Lanczos filter, reimplemented in the builder, and the 32 and 16 px frames are also written as `<site>-favicon-32.png` and `<site>-favicon-16.png`;
- `card-tags.html`: the Open Graph and Twitter tags (type, site name, URL, title, description, image, its type, width, height and alt text) for the page head; the page keeps its own `canonical` link;
- `cardpreview.html`: the before and after review page: the card served today (given with `--before`, copied as `<site>-card-before.png`) above the new card, then, from 1.10.0, both as Teams crops them (`<site>-card-before-teams.png`, the centre of the before image's width in the same proportion), each with its size and SHA-256, and the measurements; `noindex, nofollow`, no inline style or script, and no link to it from the site;
- `card-report.txt`: every measured size, clearance and contrast ratio, the Chromium version and (from 1.11.2) the launched build beside it (the executable's name: `chrome` on Linux, `chrome.exe` on Windows, `Google Chrome for Testing` on macOS; a headless-shell build is refused before any report is written, so its name appears only in the refusal message), and the SHA-256 of every input and every file written.

**Layout (round 2, owner, 2026-10-31).** The card fills a 960 by 480 px safe area at x 120, y 75. The illustration is 430 px high, right-aligned in a 340 px box, with a 28 px gap, so the text column is 592 px wide. The wordmark is fitted to the column in whole pixels (cap 112, floor 60), weight 800, letter-spacing -0.03em, the name in `word_colour` and the TLD in gold `#fbbf24`, over a 6 px gold rule as wide as the wordmark. Both statement lines share 1 size: each is fitted to the column (cap 48, floor 20), weight 700, and the smaller applies. The subline is 27 px, weight 400, white at 88 %, with balanced wrapping; with `sub_nowrap` it stays on 1 line, fitted to the column (cap 30, floor 18). An optional formula row, `{gold}` and `[blue]` (`#5eb0ef`) spans in plain text, runs below, fitted to 960 px (cap 40, floor 20). The background is the site's `background` (CSS gradients and colours only).

**Centred layout (from 1.10.0, owner, 2026-11-05).** With `layout = "centre"` in the data file the card is stacked and centred in a 704 by 530 px safe area at x 248, y 50, which lies 40 px inside the Teams crop on each side: the illustration, square, at most 250 px and at least 140 px high, sized to the height the text leaves; an 18 px gap; the wordmark fitted to 704 px (cap 88, floor 60) over its gold rule; both statement lines at 1 shared size, each fitted to 704 px (cap 34, floor 20); the subline at 27 px with balanced wrapping, centred (with `sub_nowrap` on 1 line fitted to 704 px, cap 30, floor 18); and the optional formula row fitted to 704 px (cap 40, floor 20). Colours, weights, letter-spacing and the background are those of the side layout. The gap and the clearances are measured from the glyph boxes, so the builder widens the gap and the space below the text by the amount the glyph boxes reach past their elements. A text that leaves the illustration under 140 px is a failure: shorten the text or keep the side layout. The vertical safe margin is 50 px, against 75 px in the side layout, so that the illustration keeps its size: Teams keeps the full height, and a platform that crops a 1.91:1 image to 2:1 removes about 15 px at the top and bottom.

**A background for the centred layout (from 1.11.5; owner, 2026-11-07, D-054; variant chosen on 2026-11-08, D-057).** The contrast check takes the brightest background pixel anywhere in a text's glyph box, the text hidden. In the centred layout a subline of 3 lines reaches the bottom of the safe area and spans most of its width, so a background drawn for a text band in the middle of the card leaves the lower corners of the subline's box on the bright site hue. The posluns.ca series background (`scripts/drafts/cards/build_cards.py`, `card_v4`: 2 darkening ellipses, 600 by 330 and 760 by 430 px centred at x 600, y 315, under 2 colour splotches at the bottom corners) is such a background: in the centred layout auditthis.ai's series card fails at `contrast of subline is 2.73:1 on #b0901c (needs 4.5:1)` and opfiles.ai's at 4.27:1 on `#ac590a`. The rule, its threshold and the measured box are unchanged. A background for the centred layout darkens the whole 704 by 530 safe area and paints the darkening above any colour splotch. The centred variant of the series background lists its 2 ellipses first in `background` (CSS paints the first layer on top) at 640 by 480 and 800 by 580 px, with the same centre, colours and stops; on all 6 series cards every check passes, the subline at 6.41:1 (auditthis.ai) to 11:1 (aiqt.ai) (measured on 2026-11-08 in Chromium 153.0.8010.12; the release check `t_v1115.js` builds both backgrounds for the 6 sites). `tools/site-card.example.toml` carries auditthis.ai's centred variant as a commented line. The background stays the site's own data: the builder cannot require the variant, and the contrast check remains the guard.

**Metal wordmark (from 1.11.5; owner, 2026-11-07, D-054).** 1 optional field, off by default. With it off the card page, and so every output, is the page of 1.11.4: a control rebuild of every release-check data file in both layouts, with and without `--before`, gives byte-identical images and preview pages, and tags and reports that differ only in the version label, the builder's folder in the input paths and the tags' digest. `word_style = "metal"` (default `"flat"`), in the centred layout only (otherwise `word_style = "metal" needs layout = "centre"`), sets the wordmark's name in a platinum gradient and its TLD and rule in a gold gradient, from the posluns.ca series v6 cards; `word_colour` stays required. A gradient fill is measured at its worst stop: the lowest ratio of its stops against the brightest background pixel, and 1:1 when that pixel's luminance lies within the stops' own range.

**Teams crop (from 1.10.0).** For either layout the report gives the clearance of the content inside x 208 to 992 (negative when content runs past it) and names what Teams would cut; for the side layout this is a finding for the owner's review and never a failure, and for the centred layout the safe area keeps the clearance at 40 px or more. The crop was measured from 1 Teams preview on 2026-11-05; other Teams clients, and other platforms that crop to a near-square thumbnail, may crop differently. The crop copies the centre of the width at the image's full height and scales nothing; from 1.11.3 (owner, 2026-11-07) the card's crop takes only the 1200 by 630 card, and a `--before` image may have any size in the exact proportion 1200:630, tested on the IHDR width and height before the image is decoded, in integer arithmetic as w and h greater than 0 and `w * 630 === h * 1200` (600 by 315 and 2400 by 1260 pass, 1200 by 628 and 0 by 0 fail). Any other `--before` image, an undecodable or a huge PNG with such an IHDR included, exits 1, before the image is decoded, a browser is launched or a file is written, with the message `--before <path> is <w> by <h> px, not in the proportion 1200:630 (w and h greater than 0 and w * 630 must equal h * 1200)`. Before the IHDR width and height are read, the builder checks that the `--before` file starts with the full 8-byte PNG signature, is at least 24 bytes long and names `IHDR` at bytes 12 to 15, each check failing the same way with 1 clear message of its own, so a truncated file or one without an IHDR chunk fails with that message rather than a read error; from 1.11.4 the IHDR length field (bytes 8 to 11) must equal 13, and any other value fails the same way with the message `--before <path> is not a valid PNG (its IHDR chunk declares a length of <n> bytes, not 13)`. A card of any other size, 0 by 0 included, fails with the message `the card is <w> by <h> px; the Teams crop takes only a 1200 by 630 px image`.

**Checks.** Any failure exits 1 and writes no card: every required field present and well formed (from 1.11.5 `word_style` is `flat` or `metal`, and `word_style = "metal"` needs the centred layout); the illustration and logo hold no script, event attribute, `javascript:` URL, external reference, `<image>`, inline style, `foreignObject` or DOCTYPE; every character is in the character map of each bundled font; every text fits at or above its floor (a text that cannot is a failure, never a smaller font); the glyph boxes stay inside the layout's safe area (side: clearance 120 px left and right, 75 px top and bottom; centre: 248 px and 50 px); side: at least 28 px between the text column and the illustration; centre: the illustration at least 140 px high and at least 18 px above the text; contrast against the brightest pixel of the background under each text box, the text hidden, with the text colour composited over it (statement lines and subline 4.5:1; wordmark name, TLD and formula row 3:1, as large text); the render is exactly 1200 by 630; 2 renders of the same inputs give the same pixels; and, from 1.9.2, outside the layout's safe area no pixel of the 256-colour card differs from the full-colour render by more than 32 levels in any channel (the background check; the report gives the largest change); `layout` is `side` or `centre`. The report's first line and the comment in `card-tags.html` name the builder's version and the layout. Exit 0 pass, 1 fail, 2 usage error.

**Repeatability.** The same inputs and the same Chromium build give the same SHA-256. Another Chromium build may draw text edges differently, so from 1.11.2 the builder launches both browsers with the executable `chromium.executablePath()` resolves (Playwright's full Chromium build, never its headless shell, which the builder refuses), the report records the launched build next to the version, and the release checks fail on a headless-shell build, or on a 1.11.2-or-later report without a build, before comparing a metric. The recorded values were reproduced with full Chromium 153.0.8010.12 (Chrome for Testing, Playwright 1.63.0). The review compares measurements, not bytes.

**Icons (from 1.12.2; owner, 2026-11-09, D-080).** 1 optional key, `favicon`, and 1 option, `--icons-only`. The favicon SVG is checked as the logo is; a PNG logo or favicon must be a well-formed 8-bit RGBA or RGB PNG, not interlaced and without a `tRNS` chunk (a transparent colour, which the builder does not apply), a PNG logo at most 180 by 180 px (it is never scaled) and a PNG favicon's visible pixels at least 48 px across (no frame is enlarged); each frame must be exactly its size with at least 1 visible pixel. `--icons-only` writes only the icons and `card-report.txt`, reads only `site`, `logo`, `favicon` and `touch_bg` from the data file (the card fields may stay in it), and launches no browser when no input is an SVG, so a site without a card, or one that keeps its own card (vendorreplies.ai), builds its icons with the same tool. The icon PNGs are encoded as Pillow 12.1.1 encodes them, the encoder of 1.12.1's `tools/build_icons.py`: per row the filter (none, up, sub, Paeth; average too for a PNG logo's touch icon) with the lowest sum of absolute signed bytes, and zlib level 6 (9 for that touch icon), memLevel 9, the filtered strategy. So the builder reproduces the 11 reference files of 1.12.1 in `logos/` byte for byte: measured on 2026-11-09 with Chromium 153.0.8010.12 (Playwright's build, the one the builder launches), Node.js 22.22.1 and zlib 1.3.1, for the 7 sites with an SVG favicon and, from `vendorreplies.png`, for vendorreplies.ai's `.ico`, PNG favicons and touch icon (release check `t_v1122.js`); Google Chrome 157.0.8103.38, with which 1.12.1 made the files, renders the same frames pixel for pixel. Another Chromium build may round edge pixels differently, and a Node.js build with another zlib may compress the same pixels to other bytes, so the report records the Node.js and zlib versions in an `Icons:` block and lists the favicon under its inputs. A touch icon from an SVG logo is encoded as before; it is pixel-identical to the package's `logos/<site>-touch.png`. With no `favicon` key and an SVG logo every output is as in 1.12.1.

**Approval before a card goes live (owner, 2026-11-05).** A site builds its card with `--before` set to the card it serves today and does not replace the live card until the owner approves the before and after preview. On a preview branch, the site publishes `cardpreview.html` and its images (the before and after cards and, from 1.10.0, their Teams crops) at the site root of `https://demo.<project>.pages.dev/` and gives the owner that URL with both SHA-256 values; auditthis.ai and posluns.ca show the images inline in the owner's Claude session instead. After approval the site commits the card and the tags, deploys, and checks the live image's SHA-256 against the approved one; the preview page, the before image and the Teams crops are removed.

Measured on 2026-11-05 in Chromium 153.0.8010.12 (Playwright 1.63.0): from 6 data files written from the round-2 parameter table and the 6 round-2 illustrations, every wordmark size and width, line size, subline size and line count, formula row, clearance, gap and contrast ratio equals round 2's record to its last reported digit. The images are not byte-identical to round 2: the 256-colour step is the builder's own (round 2 used Pillow's), and the mean channel difference per pixel is 0.6 to 1.3 levels of 255. The 5 touch icons the builder makes from `logos/<site>.svg` are pixel-identical to the package's `logos/<site>-touch.png`. A made-up 7th site gets every output from a data file and 2 SVGs alone, and 9 failing inputs (a statement line and a wordmark too long at their floors, low contrast, an SVG with a script, an external reference or inline style, a missing field, a background with `url()`, a character Inter lacks) each exit 1 with no card written. Not tested: how each platform crops or scales the image.

Measured on 2026-11-05 for 1.10.0, in the same Chromium: a side-layout card is byte-identical to 1.9.2's for all 6 round-2 data files, and every one runs past the Teams crop by 88 px on each side (its wordmark, rule, lines, subline and illustration are cut). With `layout = "centre"` all 6 pass: illustrations 173 to 250 px, wordmarks 82 to 88 px, statement lines 31 to 34 px, the content 40.2 to 125.9 px inside the Teams crop, background checks 8 to 23 levels. Not tested: Teams clients other than the one the crop was measured on.

### 7.36 Banner (from 1.11.0)

`<header class="banner" data-text="start|centre|end"><picture>...<img src="..." alt="..." width="..." height="..."></picture><div class="banner-text"><h1>Title</h1><p class="banner-tagline">Tagline</p></div></header>`, between the masthead and `<main>`. The picture is framed by a band in `--banner-from` and `--banner-to` (default `--brand` and `--accent`) and set at most 34rem tall; the text sits over it in the third that `data-text` names (start by default), in white with a dark shadow, so the site chooses a picture dark enough to carry it and checks that in its review. `.banner-fill` on the banner fills the title with the frame's gradient. Below 52rem the text moves under the picture onto a `--surface-raised` panel in `--text`, a filled title included (a light gradient end can fall under 3:1 there). A `<picture>` may give a wide image from 52rem and a portrait one below. `reference/banner.html` shows vendorreplies.ai's page with a neutral placeholder picture (its own artwork is not in the package).

### 7.37 Bands (from 1.11.0)

`<section class="band">`, `.band.band-tint` or `.band.band-dark`, each a direct child of `<main>` holding its own `.wrap`, give a page an alternating rhythm at full width. A tinted band sits on `--band-tint-bg`; a dark band inverts the colours (light: `--text` as the ground, `--bg` as the text, `--glow` headings and links; dark: below the page on `--surface-sunken` in the page's own colours) and holds headings, paragraphs, lists and links only. The colours are component tokens the gate checks (section 6.4). Each band's top and bottom padding is `clamp(2.2rem, 5vw, 4rem)`; from 1.11.1 a site may set its own in `site.css` with `--band-pad` (for example `.site-<name> { --band-pad: clamp(1.1rem, 2.5vw, 2rem); }`, half the default), with no change to the template (requested by vendorreplies.ai, owner's choice of a template token over a local override, 2026-11-06). From 1.11.3 the `h2` and `h3` headings of a plain or tinted band read the component token `--heading-fg`, default `var(--text)`, the colour they inherited until 1.11.2, so with the token unset nothing changes; an `h3` inside a card-grid box in such a band follows it too (until 1.11.2 it took the same `var(--text)` from the card grid's own rule). A dark band's headings keep `--band-dark-heading`. A site sets `--heading-fg` in its `site-palette.css`, in both forms of each scheme, and the gate holds it at 4.5:1 as text on `--bg` and `--band-tint-bg` (section 6.4; requested by vendorreplies.ai, owner, 2026-11-07).

### 7.38 Card grid (from 1.11.0)

`<ul class="boxes">`, or `<ol class="boxes boxes-numbered">` for numbered steps: short boxes side by side, as many as fit at 15rem or wider (3 in a desktop text column), stacking on phones; each box is on `--box-bg` with a `--box-border` border and may open with an `h3`. In a document it takes no list indent. From 1.11.3 a box's `h3` inside a plain or tinted band reads `--heading-fg` (section 7.37; default `var(--text)`, its colour until 1.11.2); elsewhere it stays `--text`.

## 8. Scripts

- Same-origin files only, loaded with `defer`, except `scheme.js`, which loads in `<head>` without `defer` so that a stored scheme applies before the first paint (section 7.33); no inline script, no `on*` attributes, no dependencies, no network requests. No storage, except `scheme.js`, which keeps the reader's scheme in `localStorage` under the key `scheme` (the value `light` or `dark`) and survives blocked storage. ES2020, strict mode, 1 IIFE per file, feature detection before enhancing.
- Each script is opt-in per page and enhances markup that already works without it: `copy.js` (every page with a copy block; 1.3 KB), `select.js` (install pages; 3.0 KB), `search.js` (search and index pages; 10.6 KB from 1.7.0, 6.4 KB from 1.5.0 to 1.6.1, 1.6 KB before), `rail.js` (every page whose rail lists in-page sections; 3.2 KB from 1.13.0, 2.8 KB from 1.3.0 to 1.12.4), `matrix.js` (a matrix page; 8.6 KB from 1.7.0, with the column toggles and highlight; 4.0 KB from 1.5.0 to 1.6.1), `scheme.js` (every page; 2.7 KB, from 1.6.0). Sizes are rounded and include each file's header comment.
- The 6 scripts are the whole set. A new script is a minor version of the baseline.
- `search.js` and `matrix.js` write the page state into the address with `history.replaceState` and read it with `URLSearchParams`; where either is missing they skip the address and still filter. From 1.7.0 `matrix.js` also writes `?cols=` (section 7.30); a same-document address change makes no request and needs no CSP change.

## 9. Security headers baseline

Decided by the owner on 2026-11-03 for all 6 sites; the values come from Cloudflare's documentation as fetched that day.

From 2026-11-07 the owner's direction extends this baseline to all 7 sites, vendorreplies.ai included: every site has consistent security headers and configuration. The 2026-11-03 decision and the values below stand as written.

From 1.12.0 NodeKeeper (nodekeeper.ai) uses the template; from its release the same baseline applies to it.

**Who sends what.** Cloudflare sends only the headers it generates automatically: the Pages defaults `X-Content-Type-Options: nosniff`, `Referrer-Policy: strict-origin-when-cross-origin` and `Access-Control-Allow-Origin: *`, `Strict-Transport-Security` from the zone's HSTS setting, and `Speculation-Rules` from Speed Brain. No custom Cloudflare rule of any kind: no Response Header Transform Rule and no managed "Add security headers" transform. Each repository's `_headers` sends, on `/*`: `Content-Security-Policy` (the site's live policy), `X-Frame-Options: DENY`, `Permissions-Policy` (below) and `Cross-Origin-Opener-Policy: same-origin`; and on `/draft/*`: `! Content-Security-Policy` (removing the live policy), `Content-Security-Policy` with the draft policy, and `X-Robots-Tag: noindex`. Never set `X-Content-Type-Options`, `Referrer-Policy` or `Strict-Transport-Security` in `_headers`; remove any such line. After each deploy, check on the live domain that every header appears exactly once on a page, a file and the 404 response, and that the browser console shows 0 CSP violations.

**The shared policy** for draft pages now and every page after promotion, the same on every site:

```
default-src 'none'; script-src 'self' 'unsafe-inline' https://ajax.cloudflare.com https://static.cloudflareinsights.com; connect-src 'self' https://cloudflareinsights.com; style-src 'self'; font-src 'self'; img-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'
```

Live pages until promotion carry the same policy widened only where a site's live pages need it (for example `style-src 'self' 'unsafe-inline'` for inline style blocks), from an inventory of what each live page loads; each widening is recorded and removed at promotion. The template itself needs only `'self'` sources: the search form never submits (`search.js` prevents it; without script it does nothing), so `form-action 'none'` holds. `history.replaceState`, which `search.js` and `matrix.js` use from 1.5.0 to write the query into the address, is a same-document change: it makes no request and loads nothing, so it needs no CSP change. The light and dark switch of 1.6.0 needs no CSP change either: `scheme.js` is a same-origin file under `script-src 'self'`, the page carries no inline script for it, its glyphs are inline SVG markup with presentation attributes (no `style` attribute and no image request), and `localStorage` is not governed by CSP. `site-palette.css` is a same-origin stylesheet under `style-src 'self'`.

**The 4 Cloudflare features**, allowed on every site, and what each needs: Rocket Loader, `script-src https://ajax.cloudflare.com`; Web Analytics, `script-src https://static.cloudflareinsights.com` and `connect-src https://cloudflareinsights.com`; JavaScript Detections, `'unsafe-inline'` for its inline loader and `'self'` for `/cdn-cgi/challenge-platform/`; Speed Brain, no CSP change (it sends a `Speculation-Rules` header), and it does not work with a nonce or `strict-dynamic` policy. A hash or a nonce in `script-src` makes browsers ignore `'unsafe-inline'`, so a site that pins inline code by hash cannot run JavaScript Detections; secureconfig.ai's live pages drop their hashes for `'unsafe-inline'` until promotion. Cloudflare states that Speed Brain "is not compatible with websites that use or rely on pages.dev": check it on the custom domain, never on the pages.dev preview. JavaScript Detections strips ETags from the HTML responses it injects into.

**Permissions-Policy**, 1 value for every site and posluns.ca, confirmed by the owner on 2026-11-03 (the union of the sites' current proposal and posluns.ca's current value):

```
accelerometer=(), autoplay=(), bluetooth=(), browsing-topics=(), camera=(), display-capture=(), fullscreen=(), geolocation=(), gyroscope=(), hid=(), magnetometer=(), microphone=(), midi=(), payment=(), serial=(), usb=()
```

**security.txt.** Each repository serves `/.well-known/security.txt` with the W3 fields, pointing at `https://posluns.ca/security/`, checked by a gate; Cloudflare's security.txt feature stays off.

## 10. Accessibility

Content hidden with the `hidden` attribute is never displayed: from 1.5.1 a global `[hidden] { display: none !important; }` rule outranks any component's display rule (section 2, "Hidden content"). A script that hides content sets `hidden`, never an inline style.

WCAG 2.2 AA. Skip link; landmarks (`header`, `nav aria-label="Site"`, `main`, `nav aria-label="On this page"`, `footer`); 1 `h1` per page and no skipped levels; visible `:focus-visible` (3 px, 2 px offset, `--focus`); touch targets at least 44 px tall in the footer and 24 px elsewhere; body text 20 px on desktop and 18 px on phones, resizable to 200 % without loss; `prefers-reduced-motion` turns off the hero fade and the arrow nudge, the only motion; tables and prompt blocks scroll inside focusable wrappers; external links announce "(opens in a new tab)"; icon-only tiles carry their names and the signature its full text as an accessible name; hidden project names in the footer's narrow stages remain accessible; the install selector, the copy button, the search and facet filter and the matrix presets and column toggles report through live regions; a facet chip that would show nothing (section 7.29) stays focusable with `aria-disabled="true"`, a dashed border and a description, and refusing it is reported in the status; large matrix tables (section 7.34) are focusable named regions with scoped headers, their tints, heat steps and group tags keep text at 4.5:1 in both schemes, counts and words carry every value, and their row hover and column highlight are reading aids with no motion; the light and dark switch (section 7.33) is a native `button` named "Dark mode" whose `aria-pressed` gives its state, reached by Tab after the top bar's links, operated by Enter and Space, with a 44 by 44 px target and the standard focus ring, and hidden without script; every palette a site overrides passes the same contrast thresholds through `tools/check_palette.py` (section 4, item 4); every page works with JavaScript off.

## 11. Per-site impact

| Site | Pages built in the mockups | Gates or contracts that constrain the template | Gates that must change | Must keep |
|---|---|---|---|---|
| secureconfig.ai | Home, Guides, nginx guide (1 example), For AI assistants, For humans, Checklist, Disclosure (new page), Licence, 404 | CSP gate allows hash-pinned inline blocks only and a 28-element allowlist (refuses `ul`, `table`, `details`, `img`, `strong`, `em`, `dl`); menu gates require guide links as GitHub blob URLs and category headings matching the README | Decided by the owner: the site moves to `'self'` files; the CSP gate must allow the same-origin stylesheet and scripts and keep refusing inline code; the element allowlist must accept the elements the template emits on this site (the mockup uses `ul`, `ol`, `table` with `caption`, `thead`, `tbody`, `tr`, `th` and `td`, `details`, `summary`, `img`, `strong`, `em`, `small`, `code`, `pre`, `button`, `section` and `nav`; it uses no `dl`, `figure`, `form` or `colgroup`); the menu gates stay (the guide index still links to GitHub blob URLs and uses the README's category headings) | `/llms.txt`, `/llms-full.txt` |
| opfiles.ai | Home, The model, The standard, Core files, Adopt, Disclosure, Licence, 404 | Logo block byte-identical per site; 1 `/disclosure` link in a `nav`; pinned sentences on 5 pages; site files listed in `.aiqt/manifest.toml` | The logo-block gate must accept the template's `a.brand` wordmark (no logo image in the top bar; the logo appears in the hero and the rail); the top bar's Disclosure item (decided for all 6 sites, section 2) satisfies the `/disclosure` gate once the gate accepts `/disclosure/`; the pinned sentences must be re-pinned to the new copy; the manifest must list the baseline files | 20 short links in `_redirects`, `/llms.txt`, `/adopt.md` |
| auditthis.ai | Home, Planning prompt, Audit prompt, Method, References, For AI assistants, Disclosure (new page), Licence, 404 | No inline style or script; element allowlist (refuses `article`, `dl`, `figure`, `aside`); `pre id="plan-text"` and `pre id="audit-text"`; generated pages must match a fresh build; 23 fixed public paths | The element allowlist must accept `dl` (the mockup uses it for a record; `article`, `figure` and `aside` are not used and stay refused) and whichever of `button`, `section`, `details`, `summary` and `caption` it does not already allow; the prompt `pre` ids are kept by the copy-block markup; `/license/` joins the fixed public paths | `/llms.txt`, `/llms-full.txt`, `/VERSION`, `/prompts/*.md`, `/references.yml`, `/METHOD.md` |
| cleanlanguage.ai | Home, The standard, Examples, Patterns, Install, Verify, Instructions, Disclosure (new page), Licence, 404 | Install page contract (5 family sections, picker links, reset button, versioned asset tokens); instructions page embeds the portable text exactly; release tools rewrite install and verify pages; metadata and sitemap gates | The install contract is met by the selector markup (section 7.12; the mockup shows 5 families); the release tools must write the template's download block; the sitemap must list `/license/` | `/install/?ai=...`, `/downloads/*`, `/download`, `/download/checksum`, `/sitemap.xml` |
| aiqt.ai | Home, Install, The standard, The 5 rules (with `/rule1` to `/rule5` redirected to its sections), Mappings, Disclosure, Licence, 404 | Same logo, disclosure, overclaim and version-currency gates as opfiles.ai; install page ids; `/mappings` meta lines; manifest coverage | As opfiles.ai for the logo block, the disclosure link and the manifest; the version-currency gate must read the control strip's version; the install ids are kept by the selector markup | `/rule1` to `/rule5` (the existing redirects in `site/_redirects` point to `/rules#rule1` to `#rule5`, and the rules page keeps those anchors), `/downloads/*`, deep links such as `/install#pick` |
| grclibrary.ai | Home, Browse, Governance (1 domain example), A document (1 example), Search, Disclosure (new page), Licence, 404 | `{{BASE}}` prefix on internal links; no hard-coded versions in templates; exact markup of the standards and For-AI lists checked against the citations register | The index-list markup (section 7.13) must be accepted by the list gates, or the lists generated into it from the register; `{{BASE}}` applies to the baseline asset paths | v2 and v3 staging trees until promoted |
| vendorreplies.ai (from 1.11.0) | Not built in the mockups; `reference/banner.html` shows its home page in the template: banner, bands, card grid, the stacked and framed pair, `no-rail` and `measure-full` | None recorded yet; its own instance reports them on adoption | To be reported | Its CONAN banner artwork and its own link-preview card (`og-card.jpg`) |
| nodekeeper.ai (from 1.12.0) | Not built in the mockups; no reference page | None recorded; it is not yet published | To be reported on adoption | Its own content |

The paragraphs below list, for each version, the files a site replaces from each starting version. `tools/site-card.example.toml` is the template of the card builder's data file: a site writes its own `site-card.toml` from it, the builder reads only that file, and a site does not copy the template (section 4, item 2). Where a list names `tools/site-card.example.toml`, a site that kept a copy from an earlier version replaces it, or deletes it and its pin entry; a site without a copy does not add one (from 1.12.3; finding SPEC-S11-EXAMPLE-TOML, 2026-11-10).

From 1.11.1 every site replaces `baseline.css` for the footer's compact bar, the names from a 1280 px window and the current tile's gold line at every width; no markup changes.

From 1.11.2 a site that adopts the warm ink replaces `baseline.css` and `tools/check_palette.py`; no markup changes; with `--ink-warm` unset every page renders as in 1.11.1.

From 1.11.3 no runtime behaviour changes while `--heading-fg` is unset: the stylesheet and palette headers, the gate's hints and the builder's version label change, and the `h2` and `h3` headings of plain and tinted bands read the new optional `--heading-fg` (default `var(--text)`, their colour until 1.11.2). A site replaces `baseline.css`, `palettes.css`, `tools/check_palette.py` and, if pinned, `tools/build_card.js` at its next update; a site that sets `--heading-fg` replaces `baseline.css` and `tools/check_palette.py` together. A rollout notice states the files to replace from each supported starting version, not only the delta from the previous version.

From 1.11.4 nothing renders differently and no token changes: the card builder's comments, report inputs and `--before` checks, the heading-ink selector's nesting guard and the documents change. A site replaces `baseline.css` and, if pinned, `tools/build_card.js` at its next update; `palettes.css` and `tools/check_palette.py` are byte-identical to 1.11.3.

From 1.11.5 a rollout notice lists the files to replace for each supported starting version itself, and never refers a starting version to an earlier notice (1.11.4 QA, F9: the 1.11.4 notice, as sent, referred sites on 1.11.2 or earlier to the 1.11.3 notice). 1.11.5 changes no markup and no token, and with the new card field unset no card changes. A site moving to 1.11.5 replaces, from 1.11.4: `baseline.css` (its header comment only, which names the version), `fonts.css` and `NOTICE.md`; adds `fonts/dancing-script-wght-normal.ttf` and deletes `fonts/dancing-script-latin-wght-normal.woff2` (the served Dancing Script, now the upstream file: 133,636 bytes in place of 42,708, loaded where a page renders the footer's signature; `font-src 'self'` covers it, so no header changes); and, if it pins the builder, replaces `tools/build_card.js` (a site that kept a copy of `tools/site-card.example.toml` from an earlier version replaces it, or deletes it and its pin entry; a site does not copy that template). From 1.11.3: the same files (`baseline.css` then also carries the 1.11.4 selector change). From 1.11.2, 1.11.1 or 1.11.0: also `palettes.css` and `tools/check_palette.py`. From 1.10.0, 1.9.x or 1.8.x: also `logos/vendorreplies.png`, and the 1.11.0 footer markup on every page (the seventh card `li.slice-vr`, the list labelled "The 7 project sites"); from 1.8.x the card builder and its fonts are new (`tools/build_card.js` and `tools/card-fonts/`; `tools/site-card.example.toml`, also new, is the template of the data file, which a site does not copy). Each list was computed by comparing the copied files of each starting version with 1.11.5.

From 1.12.0 no site needs to change: the release adds NodeKeeper's palette and logos and changes no markup, no class, no existing token value and no rendering. A site that moves to 1.12.0 replaces, from 1.11.5: `palettes.css` (it gains the 4 `.site-nodekeeper` blocks and the count-free header; every other line is unchanged), `baseline.css` (its header comment only), `NOTICE.md` (item 3 names NodeKeeper's logos) and, if it pins the builder, `tools/build_card.js` (its version label only); `logos/nodekeeper*` are new and needed only on nodekeeper.ai. From 1.11.4 or earlier: also the files the 1.11.5 paragraph above lists for that starting version (`tools/check_palette.py`, `fonts.css` and every other copied file are byte-identical between 1.11.5 and 1.12.0, so those lists stand). NodeKeeper adopts from its own release (D-064). Each list was computed by comparing the copied files of 1.11.5 with 1.12.0.

From 1.12.1 every site serves `/favicon.ico`, a copy of its `logos/<site>-favicon.ico`, and vendorreplies.ai links the PNG favicons `vendorreplies-favicon-32.png` and `vendorreplies-favicon-16.png` 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 the new `vendorreplies-touch.png`; until 1.12.1 it linked `vendorreplies.png` for both (section 5, item 1); no CSS rule, token, class or other markup changes, and no page renders differently. A site that moves to 1.12.1 replaces, from 1.12.0: `baseline.css` (its header comment only), `NOTICE.md` (item 3 names the icon files) and, if it pins the builder, `tools/build_card.js` (its version label only); adds the 11 new files in `logos/` (`<site>-favicon.ico` for the 8 sites, `vendorreplies-touch.png`, `vendorreplies-favicon-32.png`, `vendorreplies-favicon-16.png`); and copies its own `<site>-favicon.ico` to `/favicon.ico`. From 1.11.5: also `palettes.css` and `logos/nodekeeper*` (the 1.12.0 paragraph above). From 1.11.4 or earlier: also the files the 1.11.5 paragraph lists for that starting version. Each list was computed by comparing the copied files of 1.12.0 and 1.11.5 with 1.12.1.

From 1.12.2 each site builds its `favicon.ico` with `tools/build_card.js` and copies from `logos/` only the files its pages load (section 4, item 2; D-080); no CSS rule, token, class, logo file or markup changes, and no page renders differently. A site that moves to 1.12.2 replaces, from 1.12.1: `baseline.css` (its header comment only) and `tools/build_card.js` (a site that has not copied the builder copies it to `.baseline/` and pins it, with `tools/card-fonts/`, which it copies to `.baseline/card-fonts/`; section 4, item 3), and a site that kept a copy of `tools/site-card.example.toml` from an earlier version replaces it, or deletes it and its pin entry (a site does not copy it: the template documents the new `favicon` key, and the builder reads the site's own `site-card.toml`); adds the `favicon` key to its `site-card.toml` (vendorreplies.ai: `logo` and `favicon` naming `vendorreplies.png`, and `touch_bg = "#0b121c"`), builds, and serves the built `<site>-favicon.ico` at `/favicon.ico` in place of the copy of `logos/<site>-favicon.ico`, which on the recorded Chromium and zlib is byte-identical, so the served bytes do not change; and deletes from its copy folder, and from its pin, every `logos/` file its pages do not load and its builder does not read, the 11 reference files of 1.12.1 among them. vendorreplies.ai also serves its built `vendorreplies-favicon-32.png`, `vendorreplies-favicon-16.png` and `vendorreplies-touch.png` from a folder of its own and points its 3 icon links there. From 1.12.0: also `NOTICE.md` (item 3 names the icon files), and the site starts serving `/favicon.ico` (the 1.12.1 paragraph above; vendorreplies.ai also changes its icon links), with no `logos/` file to add. From 1.11.5: also `palettes.css` (the 1.12.0 paragraph above). From 1.11.4 or earlier: also the files the 1.11.5 paragraph lists for that starting version. Each list was computed by comparing the copied files of 1.12.1, 1.12.0 and 1.11.5 with 1.12.2.

1.12.3 is documentation only (finding SPEC-S11-EXAMPLE-TOML, reported by auditthis.ai on 2026-11-10): sections 3, 4, 7.35 and 11 say that `tools/site-card.example.toml` is a template a site does not copy, and that a site that kept a copy from an earlier version replaces it, or deletes it and its pin entry. No CSS rule, token, class, logo file, markup or builder behaviour changes, and every page, card and icon is as in 1.12.2. No site needs to move. A site that moves to 1.12.3 replaces, from 1.12.2: `baseline.css` (its header comment only) and, if it pins the builder, `tools/build_card.js` (its version label only); a site that kept a copy of `tools/site-card.example.toml` from an earlier version replaces it, or deletes it and its pin entry (the file is byte-identical to 1.12.2's). From 1.12.1 or earlier: also the files and steps the 1.12.2 paragraph above lists for that starting version. Each list was computed by comparing the copied files of 1.12.2 with 1.12.3. This paragraph is kept as written for 1.12.3: its "if it pins the builder" predates the 1.12.4 paragraph below, which says that every site holds the builder from 1.12.2 (section 4, item 2; D-080).

1.12.4 is documentation only. It carries the owner's decision of 2026-11-12 on cleanlanguage.ai's request (D-094), the owner's ruling of the same day (D-096), and the vote panel's rulings of the same day (D-095 and D-098). The NOTICE entry of README adoption step 7 names a third exception: the project names and the projects' domain names, which are the projects' own names and marks and which CC0 1.0 does not license (its section 4(a)). `NOTICE.md` item 3 says the same of the names and domain names, in place of the 1.12.3 wording. The entry's font exception also names, where present, the card builder's Inter fonts, which `NOTICE.md` item 4 already licenses under the SIL Open Font License 1.1. The entry names no path, so the same entry is true in every repository. Line 1 of `tools/site-card.example.toml` says that the file is a template a site does not copy, and line 2 that paths are relative to the site's `site-card.toml`. No CSS rule, token, class, logo file, markup or builder behaviour changes, and every page, card and icon is as in 1.12.3. No site needs to move. A site that moves to 1.12.4 replaces, from 1.12.3: `baseline.css` (its header comment only), `NOTICE.md` (item 3) and `tools/build_card.js` (its version label only), which every site holds from 1.12.2, when each site builds its `favicon.ico` with it (section 4, item 2; D-080); and it replaces the site-template entry in its repository's NOTICE with the entry of adoption step 7 (section 4, item 8). A site that kept a copy of `tools/site-card.example.toml` from an earlier version replaces it, or deletes it and its pin entry. From 1.12.2: the same files and the same NOTICE entry. From 1.12.1 or earlier: also the files and steps the 1.12.2 paragraph above lists for that starting version. Each list was computed by comparing the copied files of 1.12.3 and 1.12.2 with 1.12.4.

From 1.13.0 the last line of a download block in a document ends flush with the block (D-097), so the space under a download block in a document is 1.6rem (25.6 px) in place of up to 43.2 px, and the content below it moves up; no other gap changes, no gap grows, and no markup, class, palette token, logo file or builder behaviour changes. A site may also set `--para-gap`, `--download-after` and `--download-line-gap` in its `site.css`, decrease only; each is clamped between 0 and its default, so a larger value has no effect and a negative value gives 0 (section 4, item 4); each takes a length, such as `0px`, because a unitless `0` or another value that is not a length makes the declarations that read it invalid, which gives 0 margins (section 7.11). `rail.js` no longer throws on an address fragment or a rail link it cannot decode (section 7.6). A site copies `tools/card-fonts/` only where it builds a card (D-103): a site that runs the builder only with `--icons-only` may delete `.baseline/card-fonts/` and its pin entries, and its NOTICE entry needs no change, since it names the Inter fonts "where present" (section 4, item 8). A site that moves to 1.13.0 replaces, from 1.12.4: `baseline.css` (the D-097 rules and its header comment), `rail.js` where it copies it (section 4, item 2) and `tools/build_card.js` (its version label only). From 1.12.3 or 1.12.2: also `NOTICE.md` (item 3) and the site-template entry in its repository's NOTICE (the 1.12.4 paragraph above). From 1.12.1 or earlier: also the files and steps the 1.12.2 paragraph above lists for that starting version, with `tools/card-fonts/` only where the site builds a card. A site that kept a copy of `tools/site-card.example.toml` from an earlier version replaces it, or deletes it and its pin entry. A site that sets a token, or has a local workaround for the download gap, whether it keeps or removes it, checks its pages before and after (section 4, item 4, the QA for a change). The flush last line (`.doc .download > :last-child`, specificity (0,3,0)) wins over a site rule of lower specificity that set the bottom margin of a block's last line: such a rule stops applying there, which lowers the gap, unless the rule set a negative margin, which then no longer pulls the next block up, so the gap grows. The file line's token sets no margin of its own, so a site rule that set the file line's margin in 1.12.4 still does. Each list was computed by comparing the copied files of 1.12.4, 1.12.3 and 1.12.2 with 1.13.0.

Every site's Disclosure page carries the privacy-policy link beside the vulnerability-disclosure line (section 7.20); cleanlanguage.ai's `/privacy/` redirects to `https://posluns.ca/privacy/` at promotion. The Disclosure and Licence pages are linked from the landing page's "About this project" line (section 7.24), not from the top bar or the footer.

## 12. Code style

- HTML: lowercase elements and attributes, double quotes, root-relative asset paths, semantic elements before `div`, `lang="en"`, no inline style or script, no `on*` attributes; every external link carries `target="_blank" rel="noopener noreferrer"` and a visually hidden "(opens in a new tab)".
- CSS: colour from tokens only (no literal colours outside `palettes.css` and a site's own `site-palette.css`, except the family tokens in section 6.2 and `--band-grey` until it moves); rem, ch, cqw and percentage units; no `!important` except the `.vh` utility; comments in plain English without dashes.
- JavaScript: ES2020, strict mode, 1 IIFE per file, no globals, no dependencies, feature-detect before enhancing; storage only in `scheme.js` (section 8).
- Generators: Python standard library only; generated files committed where the site already commits them; every generator gains a `--check` mode; a generator refuses to write a page that contains an en or em dash, an inline style or script, an `on*` attribute, or "what this is" framing.
- Writing: the Clean Language skill applied to every page; Oxford English with -ize; no en or em dashes; numerals for quantities; "ensure that"; never claim that a site, prompt, or audit makes a project compliant, secure, or complete.

## 13. Open decisions for the owner

None. The Permissions-Policy value was confirmed by the owner on 2026-11-03 (section 2).

### Implementation choices (proceeding as stated unless the owner objects)

- A palette override in `site-palette.css` is written in both forms of its scheme, mirroring `palettes.css` (section 4, item 4), so a change to 1 dark value is the same value on 2 lines of 1 file, and the palette gate fails if the 2 differ. grclibrary.ai's standard asked for 1 value on 1 line. A form with 1 line per value is possible (each value declared once as a site variable and the 4 selector forms pointing at it), at the cost of a second layer of names in the site file and in the gate; the package ships the mirrored form, which the owner's brief named.
- The footer grey (`#4a4a4a` light, `#5c5c5c` dark) becomes a neutral token in `palettes.css`, so `baseline.css` keeps to the tokens-only rule.
- The footer's first and last cards start and end on their own hue, as built, so the bar's ends and the grey tail take a clean colour.

The footer's phone stage (2 lines below 600 px of content width, the grey band as separator) was decided by the owner on 2026-11-02 and is recorded in section 2. The footer that a site outside it (from 1.12.0, NodeKeeper) shows on its own pages, the shared family footer as is, was decided by the owner on 2026-11-08 (D-075) and is recorded in sections 2 and 7.19.
