diff --git a/.okf/build/hugo-stats-th-classes.md b/.okf/build/hugo-stats-th-classes.md
new file mode 100644
index 000000000..b1595b986
--- /dev/null
+++ b/.okf/build/hugo-stats-th-classes.md
@@ -0,0 +1,75 @@
+---
+type: Gotcha
+title: Classes used only on
get purged from production CSS
+description: Hugo 0.165 writeStats records no class attributes on
, so any selector whose class appears only on a th is stripped by PurgeCSS in production - and the visual gate cannot catch it because baseline and candidate are both built purged.
+tags: [css, purgecss, hugo, false-green, layouts]
+status: stable
+generated: { by: claude/opus-5, at: 2026-08-21T18:12:00Z }
+verified:
+ - { by: claude/opus-5, at: 2026-08-21T18:12:00Z }
+timestamp: 2026-08-21T18:12:00Z
+---
+
+# The trap
+
+PurgeCSS keeps a selector only when the class appears in `hugo_stats.json`,
+which Hugo writes from the rendered HTML. On Hugo 0.165 that file records the
+`th` **tag** but not class attributes on `th` elements. A class used only on a
+`
` is therefore invisible to PurgeCSS and its rule is deleted from the
+production bundle.
+
+Probe that established it (2026-08-21, `layouts/next/landing.html`):
+
+| Class | Element it sits on | In hugo_stats.json |
+|---|---|---|
+| `rr-td-muted` | `
` | **no** |
+
+Development builds hide it completely: `postcss.config.js` sets
+`isDevelopment` from `HUGO_ENVIRONMENT === "development"` and skips PurgeCSS
+entirely, so `bin/hugo-build` renders the styling correctly while production
+ships it unstyled.
+
+# Why no gate catches it
+
+The visual regression suite compares a baseline against a candidate that were
+**both** built the same way, so a rule that is purged in both is invisible to
+it. `css_orphan_guard_test.rb` does not help either - it asks whether a FILE
+is reachable from a template, never whether a SELECTOR survived the purge.
+This class of defect reaches production green.
+
+# What to do instead
+
+Style the column by position, or move the class onto a child element:
+
+```css
+/* works: the selector matches on tag + position, no class lookup */
+.rr-table thead th:nth-child(2) { color: var(--rr-ruby); }
+```
+
+```html
+
+
Fractional CTO
+```
+
+Verify on a PRODUCTION build, never a dev one: grep the fingerprinted bundle
+under the production destination for the selector, and grep the rendered HTML
+to confirm no orphan class attribute remains.
+
+# The same class of defect elsewhere
+
+Two more ways a /next/ rail change goes green while being wrong:
+
+- **`page/site-scripts` in a landing baseof.** Its only payload is
+ `navigation.js`, which unconditionally binds to `.js-mobile-menu-opener`
+ from the site header partial. A layout that renders its own chrome and
+ drops that partial throws `Cannot read properties of null` on every page
+ load - and no test asserts a clean console, so nothing fails.
+- **Computed `resources.Get` paths.** `css_orphan_guard_test.rb` scans for
+ the literal `resources.Get "css/…"` string. A `printf`-built path silently
+ removes the file from the guard's reachable set, so the guard keeps passing
+ while no longer guarding anything. Keep one literal `resources.Get` per
+ file, selected through a dict, when a template serves several skins.
diff --git a/.okf/build/index.md b/.okf/build/index.md
index bf9942ced..a885217bf 100644
--- a/.okf/build/index.md
+++ b/.okf/build/index.md
@@ -4,3 +4,4 @@
* [Test gates](test-gates.md) - the local suites, when each is a commit blocker, the 0.0001 default tolerance, the SECTION_CONFIGS shield, why a green run never refreshes a baseline, and why below-fold content is invisible at any tolerance, bin/record-baselines for accepting only the baselines you meant to move, and why a deleted source file still serves from every local _dest/ tree, plus the NULL CHANGE - a diff that passes every gate and alters nothing - and what `okf_validate` actually guards (shape, not truth; error-only conformance) with the two-spec trap
* [CI gates](ci-gates.md) - what GitHub Actions enforces: build, unit, path-scoped link check (visual regression is report-only), and what gates a PR never sees
* [Template PDFs](pdf-templates.md) - regenerating the downloadable course PDFs
+* [Classes only on <th> get purged](hugo-stats-th-classes.md) - Hugo 0.165 writeStats records no class attrs on `
`, so PurgeCSS deletes those rules from the production bundle while dev builds look fine and the visual gate stays green; also two sibling false-greens on the /next/ rail (site-scripts in a landing baseof, computed `resources.Get` paths defeating the orphan guard)
diff --git a/.okf/log.md b/.okf/log.md
index e85249bfe..7def78e8a 100644
--- a/.okf/log.md
+++ b/.okf/log.md
@@ -145,6 +145,50 @@ any truthy value, `test/support/setup_snap_diff.rb:28` enters record mode only
on the literal `"true"`. `=1` on qtest therefore gets the worst of both: the
suite still compares and fails, and the cleanup is skipped. That is the
mechanism behind the older "the flag appears to be ignored on qtest" note.
+## 2026-08-21 - register pilot A, and three ways a /next/ change ships green and wrong
+
+Pilot A ("Rescue Room") landed on the rail at
+`/next/pilots/rescue-room/fractional-cto/`: Paul's approved Claude Design
+artifact translated onto `layouts/next/landing.html`, all copy in stub
+frontmatter so pilots B and C reuse the template, self-hosted Poppins (a
+fonts.googleapis.com link would put a third-party fetch inside the screenshot
+runs). New concept `build/hugo-stats-th-classes.md` records the finding that
+adversarial review surfaced and the two siblings it belongs with.
+
+The finding: **Hugo 0.165 writeStats records no class attributes on `
`**,
+so `.rr-th-ours` / `.rr-th-theirs` were absent from `hugo_stats.json` and
+PurgeCSS deleted them from the production bundle - while `rr-td-muted` on a
+`
` in the same table was recorded fine. Dev builds skip PurgeCSS entirely,
+so the page looked correct locally and shipped unstyled. **No gate catches
+this**: the visual suite compares a baseline and a candidate that were both
+built purged, and `css_orphan_guard_test` asks whether a FILE is reachable,
+never whether a SELECTOR survived. Fix is positional (`th:nth-child(2)`) or a
+class on a child span. Two siblings, same shape: `page/site-scripts` in a
+landing baseof throws on every load (navigation.js binds unconditionally to
+header nodes the layout dropped, and nothing asserts a clean console), and a
+`printf`-computed `resources.Get` path silently drops a file from the orphan
+guard's reachable set so the guard passes while guarding nothing.
+
+Two corrections to how the evidence was gathered, both worth keeping.
+`FORCE_SCREENSHOT_UPDATE=1` does NOT force a re-record - in `bin/qtest` its
+only effect is skipping the post-run `git checkout` restore - so a
+byte-identical PNG under that flag proves nothing. And the system tests serve
+`_dest/public-test-local` while qtest's build step refreshes
+`_dest/public-test`; a probe verified in the wrong tree returns a meaningless
+pass. Related, still open: a marker placed in an in-frame table cell did not
+fail the desktop screenshot test even with the served tree confirmed to
+contain it, so the capture path is under separate diagnosis and pilots B/C are
+on hold until it is understood.
+
+Also: `bin/qtest` now maps `themes/beaver/static/(css|fonts)/` to `:all` - a
+webfont change alters text rendering on every page but mapped to nil, the same
+false-green class the untracked-file guard was added for. And a real person's
+words are not copy to tighten: the design blueprint had smoothed the Wozniak
+Clutch quote ("Their team was also detailed and precise, helping us to find
+problems..." became "They were detailed and precise, helping us find
+problems...") and it was carried through with a "verbatim" claim that nobody
+had checked. The pilot now asserts the rendered blockquote against
+`data/testimonials.yaml`.
## 2026-08-21 - anatomy settled, register under test: the pilots flow
diff --git a/bin/qtest b/bin/qtest
index 74e2f284f..e856d5d92 100755
--- a/bin/qtest
+++ b/bin/qtest
@@ -47,6 +47,8 @@ PAGE_TESTS = {
"friday-report" => "friday_report",
# v2 clean-slate rail (ADR-0006): layouts/next/* + pages/next-pilot.css
"next-pilot" => "next_pilot",
+ # Design-register pilots on the same rail: pages/next-.css
+ "next-rescue-room" => "next_rescue_room",
"simple-page" => "privacy_policy",
# critical/privacy-policy-critical.css maps by basename; alias it to the
# simple-page tests so a touch there doesn't abort as an unknown key
@@ -76,6 +78,12 @@ def pages_for(file)
when %r{^themes/beaver/assets/css/critical/([^/]+)-critical\.css$} then [Regexp.last_match(1)]
when %r{^themes/beaver/layouts/blog/}, %r{^content/blog/} then %w[blog-list blog-single]
when %r{^layouts/next/}, %r{^content/next/} then PAGE_TESTS.keys.grep(/^next-/) # every v2 key; shared v2 templates affect them all
+ # Self-hosted webfonts and the @font-face files that declare them change how
+ # EVERY page renders text, but they live outside the asset CSS tree, so the
+ # rules below never saw them - a fonts-only diff mapped to nil and qtest
+ # exited 0 having tested nothing. Same false-green class the untracked-file
+ # guard above was added for.
+ when %r{^themes/beaver/static/(css|fonts)/} then :all
# Terminal: root layouts/ had NO case at all, so a root-layouts-only diff
# mapped to nil and qtest exited 0 having tested nothing (false green).
when %r{^themes/beaver/assets/css/}, %r{^themes/beaver/layouts/}, %r{^layouts/} then :all
diff --git a/content/next/pilots/rescue-room/fractional-cto.md b/content/next/pilots/rescue-room/fractional-cto.md
new file mode 100644
index 000000000..8adb6fda2
--- /dev/null
+++ b/content/next/pilots/rescue-room/fractional-cto.md
@@ -0,0 +1,170 @@
+---
+title: Fractional CTO (Rescue Room pilot)
+description: Design-register pilot A - warm humanist "Rescue Room" skin over the settled fractional-CTO landing anatomy.
+layout: landing
+type: next
+# Drives both the CSS file and the bundle name in layouts/next/landing.html,
+# so pilots B and C reuse this template by changing one line.
+register: rescue-room
+# Self-hosted display face for this register (themes/beaver/static/). Another
+# register omits this key and ships no webfont at all.
+font:
+ css: /css/fonts-poppins.css
+ preload: /fonts/poppins-latin-700.woff2
+
+nav:
+ links:
+ - name: Services
+ url: /services/
+ - name: Use Cases
+ url: /use-cases/
+ - name: Blog
+ url: /blog/
+ - name: Course
+ url: /course/tech-for-non-technical-founders-2026/
+ - name: Clients
+ url: /clients/
+ cta:
+ name: Book a free consultation
+ url: /free-consultation/
+
+hero:
+ eyebrow: Fractional CTO
+ headline: A technical partner in days, not months of hiring
+ subhead: Your dev shop isn't working. Your investors want a technical update. You need someone who can audit the code, fix the team, and give you a straight answer.
+ primary:
+ name: Book a free consultation
+ url: /free-consultation/
+ secondary:
+ name: Book a 30-min call
+ url: /contact-us/
+ derisk: You keep the written assessment whether or not we work together.
+ # Markdown, rendered inline - the bold carries the emphasis the blueprint
+ # gave . Clutch rating is linked to the profile (claims-canon).
+ stats:
+ - Starts in **days**
+ - No equity, no lock-in
+ - "[**4.8**/5 on Clutch](https://clutch.co/profile/jetthoughts)"
+
+monthOne:
+ headline: What a fractional CTO does in month one
+ items:
+ - Reads the codebase and tells you what state it's actually in
+ - Sits in on your standups and tells you which ones are theatre
+ - Writes the technical section of your investor update
+ - Decides what gets rebuilt and what you can live with
+ - Sends you the Friday report from week one
+
+comparison:
+ eyebrow: The maths
+ headline: What you're comparing it against
+ fractionalLabel: Fractional CTO
+ fulltimeLabel: Full-time CTO
+ rows:
+ - label: Cost
+ fractional: A fraction of a full-time hire
+ fulltime: Full salary, benefits, and equity
+ - label: Equity
+ fractional: None
+ fulltime: Typically required
+ - label: Time to start
+ fractional: Days
+ fulltime: Months of hiring
+ - label: If it isn't working
+ fractional: You stop. No severance, no re-hire.
+ fulltime: Severance and a re-hire
+ - label: Best when
+ fractional: Pre-Series A, or stabilising after a bad build
+ fulltime: Post-Series A, with a team to own
+
+steps:
+ eyebrow: How it starts
+ headline: Four weeks from first email to a plan you can act on
+ intro: You don't sign a retainer to find out what's wrong. The assessment comes first.
+ list:
+ - title: Code review first
+ body: "You send access. You get a written assessment: what's solid, what's fragile, what's going to cost you."
+ - title: A 30-minute call
+ body: We walk you through the assessment in plain English. Most founders take the document to their board whether or not they hire us.
+ - title: Week one on the ground
+ body: Your fractional CTO joins standups, reads the backlog, and sends the first Friday report.
+ - title: Month one plan
+ body: What gets rebuilt, what gets left alone, what it costs, in what order. You own the code and the plan.
+
+clients:
+ eyebrow: Clients + case studies
+ headline: Clients stay for years, not quarters.
+ stats:
+ # `derived: tenure` is what makes the template compute the value from
+ # site.Params.foundingYear. Keyed on its own field, not on the label -
+ # a label-keyed branch falls back silently when someone rewords it.
+ - derived: tenure
+ label: Years of industry experience
+ - value: "5"
+ label: Years of average client relationship
+ - value: 8+
+ label: Years of average developer experience
+ cases:
+ - name: Agent Inbox
+ body: Hired us to build their real-estate communication platform, get it market-ready, and grow the team around it.
+ tech: [Rails, React, Node.js]
+ url: /clients/agent-inbox/
+ - name: Open Apply
+ body: Hired us to debug their school-admissions product, optimize the user experience, and streamline internal project management.
+ tech: [Rails, Hotwire, Tailwind]
+ url: /clients/open-apply/
+ - name: Mobile Coach
+ body: Hired us to support and extend the products around their enterprise chatbot platform, and to scale their R&D and engineering teams.
+ tech: [Rails, AWS, PostgreSQL]
+ url: /clients/mobile-coach/
+
+testimonial:
+ eyebrow: From a client
+ # VERBATIM tail of the canon description in data/testimonials.yaml (Bruno
+ # Wozniak). The design blueprint had smoothed it to "They were detailed and
+ # precise, helping us find problems..." - a real person's words are not copy
+ # to tighten. Asserted by test/unit/next_rail_test.rb against the yaml.
+ quote: Their team was also detailed and precise, helping us to find problems before they appear. We knew exactly what was happening and where we were going the whole time.
+ attribution: Bruno Wozniak · Director of Engineering, PubNative
+ sourceName: verified Clutch review
+ sourceUrl: https://clutch.co/profile/jetthoughts
+ proof:
+ - "[**4.8**/5.0 on Clutch](https://clutch.co/profile/jetthoughts)"
+ - "**5-year** average client relationship"
+
+closing:
+ headline: Still deciding?
+ body: The consultation is free. The assessment is yours either way.
+ cta:
+ name: Book a free consultation
+ url: /free-consultation/
+
+footer:
+ columns:
+ - heading: Services
+ links:
+ - name: Fractional CTO
+ url: /services/fractional-cto/
+ - name: Fractional Product Management
+ url: /services/fractional-product-management/
+ - name: App/Web Development
+ url: /services/app-web-development/
+ - name: Talent Recruiting & Training
+ url: /services/talent-recruiting-training/
+ - heading: Proof
+ links:
+ - name: Clients
+ url: /clients/
+ - name: Use Cases
+ url: /use-cases/
+ - name: Friday report
+ url: /friday-report/
+ - heading: Learn
+ links:
+ - name: Blog
+ url: /blog/
+ - name: Course
+ url: /course/tech-for-non-technical-founders-2026/
+ - name: About us
+ url: /about-us/
+---
diff --git a/docs/projects/2509-css-migration/css-bundle-ownership-map.md b/docs/projects/2509-css-migration/css-bundle-ownership-map.md
index 204d13759..28eadc2a6 100644
--- a/docs/projects/2509-css-migration/css-bundle-ownership-map.md
+++ b/docs/projects/2509-css-migration/css-bundle-ownership-map.md
@@ -38,6 +38,7 @@ zero FL-Builder export files remain in any slice.
| friday-report | `page/friday-report.html` | pages/friday-report.css | new 2026-08-20 | born semantic (no FL modules; one `#fl-main-content` specificity override against legacy-theme-skin) |
| pagination | `list.html` (second bundle) | — (pagination.css only) | 0.2K → 0.2K | no-FL |
| next-pilot | `layouts/next/single.html` (repo root, NOT in theme) | pages/next-pilot.css | new 2026-08-21 | v2 clean-slate rail (ADR-0006): css-variables + navigation.css + footer.css only — no style.css/586/base-4/legacy-theme-skin. Serves `/next/*` staging pages |
+| next-rescue-room | `layouts/next/landing.html` (repo root, NOT in theme) | pages/next-rescue-room.css | new 2026-08-21 | Design-register pilot A (10.01). The register skin is the bundle's ONLY file — not even css-variables — because the pilot renders its own nav and footer and must be judged on its own surface. Serves `/next/pilots/rescue-room/*`. A new register adds a row here, a skin file, and one line to the `$skins` dict |
¹ careers was a byte-identical verbatim move (3086-layout2 had zero dead
nodes); the small growth vs 07-12 is upstream content drift (course v2 merge
diff --git a/layouts/next/landing-baseof.html b/layouts/next/landing-baseof.html
new file mode 100644
index 000000000..5fb495674
--- /dev/null
+++ b/layouts/next/landing-baseof.html
@@ -0,0 +1,88 @@
+{{/* Landing baseof for the /next/ design-register pilots. Hugo picks
+ -baseof.html, so any page with `layout: landing` gets this
+ instead of layouts/next/baseof.html.
+
+ Forked from layouts/next/baseof.html. The is kept VERBATIM -
+ enhanced-meta-tags emits the noindex that makes /next/ safe to stage on
+ the production host, and the schema partials, favicons and the analytics
+ gate all ride with it. The skip link, and the
+ service-worker block are kept for the same reason.
+
+ DROPPED vs the sibling baseof: the site header and footer partials and
+ the mermaid block. A register pilot renders its OWN nav and footer from
+ the blueprint - reusing the site chrome would put the register being
+ tested next to the register being replaced, which is the one comparison
+ the demo must not make. No /next/ pilot carries mermaid. */}}
+
+
+
+
+
+
+
+ {{ partial "seo/enhanced-meta-tags.html" . }}
+
+ {{ block "header" . }}{{ end }}
+
+ {{ partialCached "page/favicons.html" . "favicons" }}
+ {{/* Analytics (GA4 + Clarity) load ONLY on the real production host -
+ local dev, test builds and any other baseURL never inject tracking. */}}
+ {{/* analytics stays OFF for noindexed staging pages so /next/ testing
+ never pollutes the engagement read (ADR-0004 gate). */}}
+ {{ if and (strings.Contains site.BaseURL "jetthoughts.com") (not .Params.noindex) }}
+ {{ partial "page/analytics.html" . }}
+ {{ end }}
+ {{ $favicon := resources.Get "/img/favicon/favicon.ico" | fingerprint "md5" }}
+
+
+ {{ $browserConfig := resources.Get "img/favicon/browserconfig.xml" | resources.ExecuteAsTemplate "browserconfig.xml" . | fingerprint "md5" }}
+
+
+
+ {{/* Enhanced SEO Schema Markup */}}
+ {{ partial "seo/enhanced-organization-schema.html" . }}
+ {{ partial "seo/comprehensive-service-schema.html" . }}
+ {{ partial "seo/article-schema.html" . }}
+ {{ partial "seo/breadcrumb-schema.html" . }}
+ {{ partial "seo/faq-schema.html" . }}
+
+
+
+ Skip to main content
+
+ {{/* The pilot's own nav and footer sit OUTSIDE - inside it, the
+ skip link would land the reader back on top of the navigation it
+ exists to skip. */}}
+ {{ block "nav" . }}{{ end }}
+
+
+ {{ block "main" . }}{{ end }}
+
+
+ {{ block "footer" . }}{{ end }}
+
+ {{/* page/site-scripts is deliberately absent: its only payload is
+ navigation.js, which unconditionally binds to .js-mobile-menu-opener
+ from the site header partial this layout drops - including it threw
+ "Cannot read properties of null" on every pilot page load. A pilot
+ that grows real interactive chrome adds its own script. */}}
+
+
+
diff --git a/layouts/next/landing.html b/layouts/next/landing.html
new file mode 100644
index 000000000..55d734c34
--- /dev/null
+++ b/layouts/next/landing.html
@@ -0,0 +1,238 @@
+{{/* Design-register pilot landing page (/next/pilots//…).
+
+ One template, one anatomy - hero, comparison table, timeline, clients,
+ testimonial + closing CTA, footer - wearing whichever register the stub
+ names. `register` picks the CSS file AND the bundle name (Concat races on
+ a duplicate bundleName, so it must stay unique repo-wide); every word on
+ screen comes from the stub's frontmatter. Pilots B and C reuse this file
+ by changing those two things, which is the whole point of piloting the
+ REGISTER rather than the anatomy (10.01 research). */}}
+
+{{ define "header" }}
+ {{- $register := .Params.register -}}
+ {{/* Deliberately the ONLY stylesheet: no css-variables, no navigation.css,
+ no footer.css. The register defines its own surface from scratch and
+ renders its own chrome, so nothing from the legacy cascade can leak in
+ and make the comparison unfair. */}}
+ {{/* Literal resources.Get calls, one per register - a printf'd path is
+ invisible to test/unit/css_orphan_guard_test.rb, which is the only
+ thing standing between this rail and a graveyard of dead skins. A new
+ register adds one line here. */}}
+ {{- $skins := dict
+ "rescue-room" (resources.Get "css/pages/next-rescue-room.css")
+ -}}
+ {{- $resources := slice (index $skins $register) -}}
+ {{ partial "assets/css-processor.html" (dict "resources" $resources "bundleName" (printf "next-%s" $register)) }}
+ {{- with .Params.font }}
+
+
+ {{- end }}
+{{ end }}
+
+{{ define "nav" }}
+ {{- with .Params.nav }}
+
+ {{- end }}
+{{ end }}
+
+{{ define "main" }}
+ {{- $p := .Params }}
+
+
+
+ {{- with $p.monthOne }}
+
+ {{- end }}
+
+
+ {{- with $p.comparison }}
+
+
+
{{ .eyebrow }}
+
{{ .headline }}
+
+
+
+
+ {{/* No class attributes on these th: Hugo 0.165 writeStats does
+ not record class attrs on
, so PurgeCSS deletes any
+ selector that only ever matches one. Column colour comes
+ from nth-child in the stylesheet. */}}
+
What is being compared
+
{{ .fractionalLabel }}
+
{{ .fulltimeLabel }}
+
+
+
+ {{- range .rows }}
+
+
{{ .label }}
+
{{ .fractional }}
+
{{ .fulltime }}
+
+ {{- end }}
+
+
+
+
+
+ {{- end }}
+
+ {{- with $p.steps }}
+
+
+
+
{{ .eyebrow }}
+
{{ .headline }}
+
{{ .intro }}
+
+
+ {{- range $i, $step := .list }}
+
+ {{ add $i 1 }}
+
{{ $step.title }}
+
{{ $step.body }}
+
+ {{- end }}
+
+
+
+ {{- end }}
+
+ {{- with $p.clients }}
+
+
+
+
+
{{ .eyebrow }}
+
{{ .headline }}
+
+
+ {{- range .stats }}
+ {{/* Company tenure DERIVES from foundingYear - a frozen number in
+ frontmatter is wrong every January (claims-canon). */}}
+ {{- $value := .value }}
+ {{- if eq .derived "tenure" }}
+ {{- $value = printf "%d+" (sub now.Year (int site.Params.foundingYear)) }}
+ {{- end }}
+
+ {{ $value }}
+ {{ .label }}
+
+ {{- end }}
+
+
+
+ {{- range .cases }}
+
+
{{ .name }}
+
{{ .body }}
+
+ {{- range .tech }}
+
{{ . }}
+ {{- end }}
+
+ {{/* Three identical link texts on one screen give a screen-reader
+ user nothing to choose between (WCAG 2.4.4) - the client name
+ rides along, hidden. */}}
+ Read the case study: {{ .name }}→
+
+ {{- end }}
+
+
+
+ {{- end }}
+
+ {{/* Guarded like every other band: a register that omits both keys should
+ render no band at all, not an empty dark stripe. */}}
+ {{- if or $p.testimonial $p.closing }}
+
+