Patterns that travel
Featured · curated highlights
Read everythingNew surface · the patterns, applied
brand.gf.cx · methods, tools, wisdom
A surface born from the patterns. Scaffolded with gfcx_subdomain_new.py and shipped with the full belts-and-braces baseline — smart-404, social card, data-tooltips, inline-link promotion, git-deploy, and a declarative site.config.json. First content: a cinematography lighting set-up, ingested + transcribed and footed with the built-with provenance footer. The clearest live proof these patterns travel — a whole subdomain assembled from them in one session.
Presentation · visual language
slide-deck-greys
A dark-mode pitch-deck visual language extracted from the YouExec Hardware Innovation deck. Three type sizes, six layouts, one accent, five narrative moves. Includes 18 reference animations and a copy-ready brief for Claude Design or any slide generator — the pattern travels anywhere a deck has to satisfy both the technical reviewer and the partner writing the check.
Time-zone scheduling · live surface
Timeline overlap · zone-card
A solid-green scheduling hero card paired with a UTC-anchored three-row timeline (NYC · London · Asia) and clickable zone overlays. Answers "when in time should this happen?" by showing the action window, the live now-tick with pulse + traffic-light zone colour, and tooltip pills for both ends of the wire. Built on status.gf.cx/transfer-windows/ in 25 mins of iterative refinement, 2026-06-04.
Deployment mechanics
BASE_URL one-line swap
Workers + tag-previews + dashboards all reference a single exported BASE_URL constant — when the custom domain binds, one-line edit + redeploy flips everything. Avoids the dozen-find-replace migration that always misses one.
Vendor integration
Sandbox-first discipline
Vendor APIs (Pwinty, Resend, Twilio, Stripe) get a self-contained surface — sandbox.gf.cx/<vendor>/ — for end-to-end proof before promoting to production tag-previews and order paths. Contains the blast-radius of sandbox creds + experimental wiring.
Cloudflare gotchas
Workers-with-Assets · run_worker_first
CF Pages projects with _worker.ts + assets binding intercept asset paths BEFORE the worker for browser-shaped requests. Symptom: works in curl, 404s in browser. Fix: run_worker_first: true. Cost us 90 minutes on dare.co.uk/ip before the cf-cache-status HIT signal broke it open.
Deploy verification
Visual verification · external probe
"Looks good from here" is not deploy verification. WhereGoes / urltoscreenshot.com / RapidAPI-screenshot are the independent witnesses that catch the curl-200-but-browser-404 class of bug before real users do. Bake into deploy hygiene.
Routing design
Tag-router · numeric + slug peers
QR labels encode short numeric IDs (denser scan, re-assignable, opaque). Slug URLs stay as friendly aliases for humans. Both resolve to the same record — first-class peers in the worker. Class-prefixed numbering (1xxx vehicles · 2xxx tools · etc.) gives the IDs structure without leaking semantics.
Portfolio scaffolding
gfcx_subdomain_new · scaffold
Don't hand-roll a new gf.cx subdomain. The generator at ~/bin/gfcx_subdomain_new.py encodes prior friction — fonts, primitives, CF Pages config, deploy command, custom-domain dashboard path. Each new subdomain adds learnings back to the scaffold itself.
UX primitives · accessibility
Jargon tooltip · data-tip pattern
Wrap acronyms server-side in <abbr aria-label="…" data-tip="…"> + pull in assets.gf.cx/tooltip/tooltip.css. Dark-card, caret-arrow, viewport-clamped. Key trap: use aria-label not title — the browser double-fires its own gray box from title=. Validated on dash.gf.cx perf reports 2026-05-31.
UX primitives · interactivity
Interactive tooltip · data-tip-href
When a tooltip needs a clickable "Learn more ↗" link, CSS ::after can't deliver it — pointer-events:none kills clicks inside. Add data-tip-href, suppress the CSS card, and let tooltip-interactive.js inject a real DOM card with a 120ms hover-zone guard so the cursor can travel from trigger to link without it closing.
Easter egg · micro-gesture
Pulse on icons · staggered wink
A row of glyphs is a row of opportunities. Stagger an animation-delay across them with nth-child and they ripple sequentially — barely-noticed at a glance, delightful once you catch it. Subtle scale + opacity nudge for ~10% of the cycle, at rest for the other 90%. Validated on data.gf.cx 2026-05-29: "I keep wanting pulses on these icons too, like an easter egg."
Dashboard composition
Context within the field · inline origin chips
A diagnostic row that shows what (path + count + statuses) earns another row when it also shows where from — country codes + scanner UAs as chips, attached to the row they qualify. No separate panel, no drill-down: the origin is in the field with the field. Validated on dare.co.uk "Diagnostic probes" 2026-06-04 — Dan: "very nice touch · helpful · context-specific, within the field."
Meta-pattern · documentation primitive
Pattern preview · live demo + tabbed code
A reusable CodePen-style block — iframe-sandboxed live demo on one side, tabbed HTML/CSS/JS source with copy button on the other. Authors drop a <div class="pattern-preview"> with <template> children; the JS assembles the srcdoc and renders the tabs. Lets readers feel a pattern before they read the why. Now backing the tooltip + pulse + meta examples on this page.
Operating dashboards · seed
Read-me · status of the stack
R/Y/G priority tiers + collapsible category panels make a system's "what's done / what's open / what's silent-failure risk" visible at a glance. Doubles as documentation + running TODO. Dan named the pattern 2026-05-27 after 5 items flipped ✅ in one session on home.gf.cx/service-records.
Provenance · footer primitive
Built with · provenance footer
A collapsible <details> footer that records which scripts, models and tools built a page — compact when closed (count + ingest <time>), full ledger on click. The payoff: each listed tool links back to its governing pattern or kb memory note, so the reader goes from "what built this" to "why it's built this way" in one click. First live on happiness.gf.cx 2026-06-18.
Editorial · article composition
Editorial article · micro-decision grouping
Don't write an advice piece as flowing prose — compose it as a thesis plus a grouping of 3–4 self-contained "moves," each an imperative subhead with 1–3 tight sentences, that "rolls up nicely" into one takeaway. Skimmable by heading alone; lowers the bar to write and to read. happiness.gf.cx is the authoritative editorial pattern.
Observability · probe quarantine
Diagnostic probes · quarantine panel
"Where am I?" probes — /ip · /cf · /trace — are analytics noise: scanners and bots, not readers. Don't delete them, quarantine them in a collapsed panel that shows who's poking the perimeter and from where, with a country + scanner-UA breakdown. Extracted from dare_cf_analytics.py into a dependency-free composable script, diagnostic_probes_panel.py. 2026-06-19.
UX · long-page affordance
Back-to-top · jump icon
A neat ↑ top link that appears only on long pages, from one shared script. The conditional logic is runtime, not a static page list — it reveals itself when the document runs past ~2.2 screens. Auto-added to new surfaces via the scaffold; retrofit across the corpus by gfcx_backtop.py (with a manifest-backed undo). Sweep hand-authored pages; wire the generator for generated ones. 2026-06-25.
Primitive · report masthead
Report header · quiet by default
A report masthead should sound like the document beneath it. The dare.co.uk report header had drifted into an oversized serif-italic headline fighting a restrained, sans-bodied report. Three live header specimens on the shared page-header primitive — sans, upright serif, canonical italic — to compare and graduate. Specimen B (upright serif) shipped corpus-wide; the title voice + colour chip landed in page-header v0.2.0. 2026-06-25.
Media · third-party embed
Karaoke video player · Player.js sync
A vendor-hosted HLS embed wrapped with word-by-word karaoke captions, a CC toggle, and a click-to-seek transcript — synced to true player position over the Player.js postMessage protocol. Ships as a degradation ladder: live sync → wall-clock button → static transcript, with the fallback demoting itself to a status light when the protocol connects. 2026-07-05.
Cloudflare · DNS / TLS
Scoped per-subdomain CAA
A subdomain fronted by a non-Cloudflare CA (AWS ACM → Amplify, CloudFront) can't get a cert — the apex serves Cloudflare's auto-managed CAA set and resolution walks up to it. Give the subdomain its own CAA record: lookup stops at the first name with records, so the walk never reaches the apex. Zero blast radius. Tool: gfcx_scoped_caa.py. 2026-07-07.
UX · media / lightbox
Thumbnail → modal enlargement
A thumbnail that earns a click opens one shared enlargement modal — image or inline <svg> — with caption, closable by ×/backdrop/Escape. The overlay injects itself on demand; mark any element data-modal and it's keyboard-native. One CDN css+js, themed off local vars. Lifted from srvc.gf.cx/transit. 2026-07-09.
UX · maps / location
Map-dossier — locating a facility
Place a facility with a three-beat sequence — aerial → close-up hover → street-view. Two duality axes: for maps, reverse the hover (start close-up, zoom out); and street-view either deferred (.svfacade, click-to-load) or always-on (.embed3d). The .rollup hover image must be the exact pixel size of the base. One CDN css+js. Lifted from srvc.gf.cx/transit. 2026-07-13.
UX · time / readability
Human-friendly relative time
Show elapsed time the way people read it — 3d ago, not a raw ISO stamp — and hang the exact instant (18 Jul 2026, 07:12:49 -0400) on a data-tip hover. Human-scale by default, precise on demand. fmt_age + fmt_exact over one shared parse_ts. Trap: a colon-less %z offset (-0400) the parser rejects → one raw row in a column of tidy "Xh ago" cells. 2026-07-18.
UX · status / readability
Plain-language status faces
Turn a machine's status cards into human copy — a plain title + one calm verdict-keyed sentence, the raw summary kept behind the detail link. Keyed by canonical label, rendered through one _face() helper called from every path (main loop, pending, empty-window, hover title) — else 24h reads clean while 7d/30d/90d leak jargon. Traps: key on the raw verdict not the CSS class; {n} strips a trailing %. From dash.dare.co.uk. 2026-07-24.
Ops · security / audit
Security-header audit
Don't read the headers — count them. curl -sSI the live URL and tally each: 0 = missing, >1 = two emitters, and the pattern of which doubled fingerprints the culprit before you open a panel. Then enforce one owner per zone — set not add. Traps: sample 15-20× (a ruleset edit propagates POP-by-POP), the second owner often hides in a dashboard toggle no grep finds. From dare.co.uk's doubled X-Content-Type-Options. 2026-07-25.
Read-me · status of the stack
"Can't tell you how important a pattern like 'Read-me · status of the stack' is to the growth. It's super logical and shows foresight and helps frame the interdependencies between entities and records/bindings and such like."
"This is a GOLD standard set of operating principles."
The problem
A system grows by accretion: new endpoints, new bindings, new vendor accounts, half-done refactors, parked sketches. Without a self-state-tracking surface, two things rot:
- The operator's mental model diverges from reality. Did I bind that domain? Is Access on yet? Did I ever fix that typo'd entry? Cost = re-discovery on every return.
- The interdependencies become invisible. A 🟢 nice-to-have can quietly turn into a 🔴 blocker the moment a related decision lands. Without a frame for "this depends on that," velocity tax compounds.
The shape
An H2 "Read-me · status of the stack" near the bottom of an operator dashboard or category landing. Below it: a stack of collapsible <details> panels — one per priority tier, with:
- A colored dot 🔴 🟡 🟢 carrying the priority semantics (production-blocking / lifecycle-rot / silent-failure-risk)
- A category name in the operator's language — not P1/P2/P3 abstractions
- A count summary in the panel header ("4 items" → "2 of 3 done · 1 open") so scope + velocity are visible without opening
- Done items kept in the list with ✅ and a date — they don't disappear; the page becomes session-log + TODO simultaneously
- Each open item has a how-to-fix sub-line below its title — actionable, not just descriptive
Why R/Y/G works (and a flat checklist doesn't)
| Choice | Why |
|---|---|
| Color via dot, not text-color | Dot is at-a-glance even at peripheral vision. The dot does the work before any reading happens. |
| Categories named in operator's language | "Tag-physical workflow" instead of "Hardware" — names the operator already uses internally. Eye finds the right bucket without translation. |
| Collapsible per-category | 23 items in a flat list is intimidating + scroll-heavy. 7 collapsed panels fit one phone screen; the eye picks where to dive in. |
| Count in summary | Shows scope without opening. After completions: "2 of 3 done · 1 open" makes velocity visible. |
| ✅ stays + dates | Velocity is visible. Future-you knows what was resolved + when + by which session. |
| How-to-fix sub-line per open item | Re-entering work next session = one-line briefing. The dashboard is actionable, not just descriptive. |
| Lede summarizes velocity | "5 of 23 resolved this session" primes the reader to see the page as alive + moving, not a static gripe list. |
Where it applies
Strong candidates — surfaces where the page is both documentation AND a TODO:
- Operator dashboards (
home.gf.cx/service-records/is the canonical exemplar) - Agent / watcher hubs (
agent.gf.cx/— module lifecycle state) - Marketing / SEO sites with running performance debts (PSI, GSC indexing gaps, broken-link punch lists)
- Category landings on
gf.cx/that grow ≥ 5 interdependent records - Vendor decision surfaces (refuse re-quote, HVAC shortlist) with status across candidates
Weak candidates — pages whose audience is external:
- Public-facing detail pages (
io.gf.cx/foundfinder pages, individualsvc.gf.cxbrand pages) - Sales pages, blog posts, content marketing
The portable shape
<h2 class="section-heading">Read-me · <em>status of the stack</em></h2>
<p class="lede">[snapshot date · N of M resolved · brief tier framing]</p>
<details class="gap">
<summary>
<span class="gap__pri">🔴</span>
Category name
<span class="gap__count">2 of 3 done · 1 open</span>
</summary>
<ul>
<li><span class="gap__title">✅ item title</span> (done YYYY-MM-DD). Brief.</li>
<li><span class="gap__title">item title.</span> Why it matters.
<span class="gap__sub">How to close it · effort · who/what blocks.</span></li>
</ul>
</details>
When it breaks
- Stale fast if not maintained. Two sessions of stale state = the page becomes a source of wrong belief about system state. Worse than no dashboard.
- R/Y/G can paper over judgment. A 🟢 with a 6-month-rotting dependency is still rotting. Mitigation: in the 🟢 lede, frame it as "silent-failure risk" not "nice-to-have."
- Counts drift from reality. Hand-maintained counts go stale when items get added without count update. Mitigation: render via JS when this matters; skip for personal-tool scale.
Trigger to add it to a page
When the page starts having tracked-but-unresolved decisions, bindings, or dependencies that span ≥ 3 categories. Below that, a single TODO list or a Cross-references block is enough. Above that, R/Y/G + collapsible structure starts paying off because the eye needs help finding what matters.
Reference implementation
Canonical exemplar as of 2026-05-27: home.gf.cx/service-records/ → Read-me · status of the stack. The dashboard tracks the cross-Worker service-records stack (svc.gf.cx + io.gf.cx/found). After 5 items flipped ✅ in one session, Dan named the underlying pattern — this patterns.gf.cx entry codifies it.
BASE_URL · one-line swap
The problem
A subdomain spins up on a Pages preview URL (foo-gf-cx.pages.dev) and its custom domain (foo.gf.cx) binds days later. In between, Worker code, tag-previews, OG images, analytics, README copy — every surface composing an absolute URL — references the wrong place. Hand-find-replace at swap time always misses one.
The shape
Export one canonical BASE_URL constant from one shared module. Every other surface imports it. Binding day = one-line edit + redeploy flips everything.
// src/config.ts
export const BASE_URL = "https://foo.gf.cx";
// everywhere else:
import { BASE_URL } from "./config";
const previewUrl = `${BASE_URL}/preview/${id}`;
When it breaks
- URLs in static HTML/CSS don't import — they need a build-time substitution (esbuild
define, jinja templating). Cover that surface separately or it'll be the one that slipped through. - Cross-repo references (subdomain A linking subdomain B) — each repo keeps its own
BASE_URL; cross-refs go through a redirect or a shared catalog, not a direct import across repos.
Reference
The found.gf.cx → io.gf.cx/found migration (2026-05-28) used sed across 4 external files for retroactive cleanup of hardcoded URLs. Codified into this pattern so the next subdomain starts correct rather than getting fixed at swap time.
Sandbox-first · discipline
The problem
Wiring production vendor credentials into a customer-facing surface before the integration is proven is how secrets leak, callback paths misfire, and the integration's true edge-cases ambush the live order path. The opposing risk — building a sandbox surface that no one ever wires up — wastes the dry-run.
The shape
Every vendor integration (Prodigi / Pwinty, Resend, Twilio, Stripe, Canva Connect) gets a self-contained surface at sandbox.gf.cx/<vendor>/ first. That surface holds the sandbox credential, does the full round-trip (OAuth, API call, callback, response inspection), and proves the path end-to-end. Only then does the same code path get promoted to the production surface with the production credential swapped in.
Why a dedicated subdomain
- Credential blast radius: sandbox creds live in
sandbox.gf.cx's Worker secrets — they never enter the production surface's env. - Experimental wiring is contained: half-built endpoints, debug routes, raw API response dumps — none of it bleeds into the surface a customer sees.
- Multi-account ready: the same sandbox can be reused when adding a second account (Dogwood / audreyinc / xlab-co) without rebuilding the dry-run substrate.
Reference implementations
- sandbox.gf.cx/print — Prodigi sandbox cockpit. Validated the print-on-demand pipeline (quote → order → fulfillment callback) before print.gf.cx wired production creds. Shipped MVP 2026-05-27.
- sandbox.gf.cx/canva — Canva Connect OAuth round-trip cockpit.
print.gf.cx's "Design in Canva ↗" button calls into this surface for the design-create → editor → return-to-app → PNG-export flow. Shipped 2026-05-28.
Workers-with-Assets · run_worker_first
"Default routes browser navigations through the assets-layer FIRST, bypassing the Worker. curl works, browser 404s. Should bake into gfcx_subdomain_new.py template."
The problem
A Cloudflare Pages project with a _worker.ts + assets binding looks like it should serve dynamic routes from the Worker and static files from the asset layer. In practice, browser navigations (requests with Sec-Fetch-Mode: navigate) route through the asset layer first, returning a 404 for routes the Worker would have handled. The symptom: curl returns 200, the browser returns 404. Easy to misdiagnose as DNS, cache, or routing.
The fix
# wrangler.toml — every new gf.cx subdomain serving both static + API
[assets]
directory = "./dist"
binding = "ASSETS"
run_worker_first = true # ← Worker gets first crack at every request
Where it applies
- Every gf.cx subdomain that serves both static pages and Worker-handled routes (auth, OAuth callbacks, JSON API, dynamic HTML)
- Pages projects migrating from a pure-static
pages_build_output_dirsetup to Workers-with-Assets (sandbox.gf.cx's migration on 2026-05-27 surfaced this exact trap) - Default to bake into
~/bin/gfcx_subdomain_new.pyso new subdomains start correct
How to recognise
If curl https://name.gf.cx/dynamic-route returns 200 and the same URL in a browser returns 404, with the response showing cf-cache-status: MISS or no Worker invocation log: this is the bug.
Reference
Cost 90 minutes on dare.co.uk/ip before the cf-cache-status HIT/MISS signal broke the diagnosis open. Captured 2026-05-28 after the same trap reappeared on sandbox.gf.cx's Workers-with-Assets migration — proof the pattern travels.
Visual verification · external probe
The problem
"Looks good from here" is not deploy verification. The local browser cache, the local NextDNS resolver, the CF edge cache for your IP, the SSL cert that resolved an hour ago — all of these can show a working site while a real user on a different network sees a 404, a bad cert, or an asset that hasn't propagated. The class of bug this misses: curl works, browser fails (see run_worker_first). An independent witness is the only reliable check.
The shape
After any non-trivial deploy of a public surface, hit it from an external visual probe — a screenshot service that renders the page from a third-party network, with no shared cache and no DNS overlap. The probe answers two questions in one shot:
- Does it resolve at all from outside? (DNS, cert, routing, propagation)
- Does the rendered page look right? (assets loaded, fonts loaded, JS executed, no Rocket-Loader-broke-the-boot regressions)
Useful probes
- WhereGoes — redirect chain + final landing screenshot. Free, fast, no account.
- urltoscreenshot.com — clean visual capture, scriptable variants.
- RapidAPI screenshot endpoints — for baking into CI / cron / deploy hooks.
Where it applies
- Custom-domain binding day on any new subdomain (curl alone can't catch SSL cert mismatch + CF Universal SSL propagation gaps)
- Migration from one project to another (Pages → Workers-with-Assets is a classic case)
- Any deploy touching inline JS (Rocket Loader can break execution even when the static HTML renders fine — see the related zone-level gotcha)
If the local browser says "looks good" but no external probe has confirmed it, the deploy is unverified.
Tag-router · numeric + slug peers
The problem
QR-encoded asset tags need to be dense (a phone camera reads /1042 faster than /sennheiser-hd-560s-headphones) and re-assignable (the physical tag number outlives the asset description). But humans also need friendly URLs they can type, share, and remember. Choosing one over the other forces a compromise on the side you didn't pick.
The shape
Both numeric and slug forms are first-class peers in the Worker — neither is a redirect to the other. Both resolve directly to the same record:
// io.gf.cx/found Worker
GET /1042 → render record #1042
GET /sennheiser-hd560s → render record (slug-looked-up) #1042
GET /1042.json → JSON projection of same record
The numeric ID carries class-prefixed structure for cheap human grouping (1xxx = vehicles, 2xxx = tools, 5xxx = electronics) without leaking the actual category into URL semantics — the prefix is a hint, not a contract.
Why both peers, not redirect
- QR scans hit numeric direct. No redirect = one fewer round-trip, no cache-pollution surprises.
- Shared links hit slug direct. A friendly URL in a message stays friendly; canonicalisation happens at render-time (
rel="canonical"), not at routing-time. - Re-assignment doesn't break either. If a physical tag (#1042) gets moved to a new asset, numeric continues to resolve to whatever record currently owns it; the old slug 404s cleanly.
Reference implementations
- io.gf.cx/found — gf.cx asset tags + lost-property recovery (numeric + slug peers, KV-backed)
- svc.gf.cx — service-record QR labels with brand-keyed slug + numeric ID
gfcx_subdomain_new · scaffold
The problem
Each new <name>.gf.cx subdomain shares the same first hour of setup work: Pages project create, custom-domain binding, fonts, primitives (cards.css, media.css), declarative landing, 404 page, wrangler.toml, _redirects, deploy command, the dashboard URL for the custom-domain pane. Hand-rolling each one rediscovers the same friction every time — and forgets the small things (Workers-with-Assets run_worker_first, Rocket Loader data-cfasync, BASE_URL constant) the previous one taught.
The shape
One script — ~/bin/gfcx_subdomain_new.py — encodes the current state of "what a fresh gf.cx subdomain looks like." Run it with a name; it generates the repo, the Pages project, the deploy command line, and prints the dashboard URL where the custom-domain CNAME needs to bind. Every new subdomain starts from the latest learnings, not a blank slate.
The discipline
Each new subdomain that surfaces a friction (a missing field, a forgotten CF setting, a default that was wrong) feeds back into the scaffold itself before the next subdomain spins up. This is what makes the scaffold compound: the third subdomain inherits the second's edge cases, the tenth inherits all nine.
What the scaffold bakes in today (v0.3.0)
A fresh subdomain is no longer just a landing page — the generator stamps the full belts-and-braces baseline, so a new surface ships finished, not minimal. A surface missing any of these is under-finished:
- Declarative
site.config.json— a belts-and-braces manifest: the single source of truth for which baseline layers are wired. You audit a surface against it before calling it done, rather than eyeballing the HTML. (v0.3.0, 2026-06-19) - CF Access secure-by-default —
site.config.jsonnow declaresaccess: cf-access onfor every new surface (email-OTP for the owner, home-IP on the bypass whitelist). Public surfaces are the explicit opt-out (enabled:false) — because Access breaksog:imagecapture + social crawlers, anything shareable opts out deliberately. - Three-layer UX baseline — inline-link promotion, rollover thumbnails, and an auto-captured social card (
post_deploy.sh→media.gf.cx/<surface>/og/card.png), wired in rather than bolted on. - Diagnostic probe endpoints —
functions/ip.ts · cf.ts · trace.tsgive every surface/ip · /cf · /traceout of the box (companion to the probe-quarantine panel). - Smart, context-aware 404, a git-deploy workflow (
.github/workflows/deploy.yml), and surfaces.json snapshot registration at scaffold time. - Workers-with-Assets
run_worker_first = trueand Rocket-Loaderdata-cfasync="false"— the earlier gotchas, still carried forward.
The principle
Don't hand-roll a new gf.cx subdomain. The wrappers encode prior friction.
The same principle applies to the wider ~/bin/ portfolio: wrangler-deploy > raw npx wrangler, verify-cf-token > raw curl, gfcx_status_hub_render > hand-templated dashboard. Reach for the wrapper first — it has already paid the cost of every gotcha you haven't hit yet.
Pulse on icons · staggered wink
"I keep wanting pulses on these icons too, like an easter egg."
Validated on data.gf.cx after the same session locked in the active-dot infinite pulse (the "you are here" marker) and the on-hover pulse on the gate-detail icons. The third move — a permanent, gentle pulse across the source-type glyphs on the active gate card — gave the surface a quiet pulse-of-life that reads more as character than as chrome.
The problem
A row of icons next to a label reads as static decoration. The eye logs them once and moves on. Without motion, you lose two things:
- The personality cue. Static iconography is institutional; subtle motion suggests "this surface is alive, somebody's at the keyboard."
- The serial relationship. Icons in a row mean the same thing: they're peers. Synchronous motion treats them as one block. Staggered motion treats them as a sequence — which more truthfully maps the underlying data (different sources, processed in order).
The shape
Each glyph wraps in a span that carries a class. The animation runs on every glyph, but nth-child selectors offset the phase. The result is a ripple — left to right, or whatever sequence the DOM order encodes.
<div class="icon-row">
<span class="icon-row__glyph"><svg>...</svg></span>
<span class="icon-row__glyph"><svg>...</svg></span>
<span class="icon-row__glyph"><svg>...</svg></span>
<span class="icon-row__glyph"><svg>...</svg></span>
</div>
.icon-row__glyph {
transform-origin: center;
animation: icon-wink 5.2s ease-in-out infinite;
}
.icon-row__glyph:nth-child(2) { animation-delay: 0.9s; }
.icon-row__glyph:nth-child(3) { animation-delay: 1.8s; }
.icon-row__glyph:nth-child(4) { animation-delay: 2.7s; }
@keyframes icon-wink {
0%, 88%, 100% { transform: scale(1); opacity: 0.82; }
4%, 12% { transform: scale(1.18); opacity: 1; }
}
Live demo
Four glyphs, ~5.2s cycle, 0.9s phase offset between siblings — a quiet ripple left-to-right. Sit with it for 10 seconds to catch the full cycle.
The tuning
- Cycle length 5.2s. Long enough that the surface doesn't feel restless. Short enough that the casual visitor catches it within a few seconds of landing.
- Wink occupies ~12% of the cycle (frames 0–12% in the keyframes). The other 88% the glyph is at rest. That's the "easter egg" tax — most of the time it looks static.
- Phase offset = cycle / count. Four glyphs over 5.2s means a 1.3s wave period; the 0.9s stagger creates an overlapping ripple rather than a strict sequence. Pick offsets that feel like a sentence, not a metronome.
- Scale 1 → 1.18. Big enough to register at small icon sizes (18-24px), small enough that it doesn't shift surrounding layout.
transform-origin: centerensures the wink emanates from the icon's middle rather than the corner. - Opacity 0.82 → 1. The brightness shift carries most of the visual signal; the scale just amplifies it.
Where it belongs
This is a character animation, not a status indicator. Reach for it when:
- The icons are peers in a sequence (sources, steps, stages) — not when they're competing for attention.
- The page is read more than scanned. Easter eggs assume the user will sit with the surface.
- The motion doesn't compete with an existing always-on signal nearby (a pulsing active dot, a spinner, a loading bar). One ambient motion per region.
It does NOT belong on form fields, navigation, primary CTAs, error states, or anywhere accessibility-critical. Pair with @media (prefers-reduced-motion: reduce) { .icon-row__glyph { animation: none; } } to respect user settings.
The principle
Static icons are decoration. Staggered icons are alive.
This pattern is the third in a family of pulse-as-affordance moves: infinite pulse = "you are here" (active marker, always running), hover pulse = "you can engage with this" (one-shot on rollover), and now staggered ambient pulse = "this surface has a heartbeat" (decorative, low-energy, sequential). Same CSS technique (scale + opacity keyframes), three semantic registers. Reach for whichever matches the meaning you're encoding.
Jargon tooltip · data-tip pattern
"Nit-picking — can you add the tooltip pattern from assets.gf.cx to the metrics? FCP are human-readable. Audit/apply across all reports — anytime we use jargon-heavy, put its explainable tooltip in."
The problem
Technical reports and dashboards accumulate acronyms — FCP, LCP, CLS, INP, TTFB, GSC, CrUX. Readers who know them are fine; readers who don't can't act on the data. Adding a glossary page adds friction. Inline prose definitions bloat the table. The browser's native title="" tooltip solves it but looks awful: grey system font, 500ms hover delay, no viewport-clamping, no theme cohesion.
The shape
Two pieces: a CSS primitive from assets.gf.cx and a server-side term-annotator.
| Layer | What it does |
|---|---|
assets.gf.cx/tooltip/tooltip.css | Pure-CSS [data-tip] selector — dark card, caret arrow, viewport-clamped max-width: min(320px, calc(100vw - 32px)). No JS. |
_annotate_jargon(html) in seo_render_html.py | Regex pass over rendered HTML — wraps known GLOSSARY terms in <abbr> with data-tip. Skips <code>, <pre>, <a>, <abbr>, <svg> blocks to avoid rewriting paths or SVG text nodes. |
The trap — title= fires twice
The instinct is to set both title="…" (accessibility) and data-tip="…" (styled tooltip). Don't. The browser fires its own native gray box from title= ~500ms after hover — you get two overlapping tooltips simultaneously.
Fix: use aria-label="…" instead of title="…". Screen readers read it; the browser does not show a native tooltip.
<!-- ❌ Double tooltip — browser shows gray box + styled card -->
<abbr title="First Contentful Paint" data-tip="First Contentful Paint — …">FCP</abbr>
<!-- ✅ Single styled tooltip — aria-label for screen readers only -->
<abbr aria-label="First Contentful Paint — …" data-tip="First Contentful Paint — …">FCP</abbr>
Import
<link rel="stylesheet" href="https://assets.gf.cx/tooltip/tooltip.css">
Live demo — CSS-only variant
Hover the dotted term. Pure CSS, no JS — note that aria-label is used in place of title to suppress the browser's native gray box.
Time to first byte (TTFB) measures network + server latency. Largest contentful paint (LCP) measures perceived loading speed.
Server-side annotation
GLOSSARY = {
"FCP": "First Contentful Paint — time until the first text or image is painted on screen (good ≤ 1,800 ms).",
"LCP": "Largest Contentful Paint — time until the largest image or text block is visible (good ≤ 2,500 ms). A Core Web Vital.",
# ... more terms
}
def _annotate_jargon(html: str) -> str:
# stash <code>, <pre>, <a>, <abbr>, <svg> blocks first
for term, expansion in GLOSSARY.items():
safe = expansion.replace('"', """)
html = re.sub(
r"\b(" + re.escape(term) + r")\b",
rf'<abbr aria-label="{safe}" data-tip="{safe}">\1</abbr>',
html
)
# restore stashed blocks
return html
Interactive variant — data-tip-href
When a tooltip needs a clickable "Learn more ↗" link, the CSS-only card can't deliver it (content: attr() is text-only, pointer-events: none). The interactive variant replaces the CSS card with a real DOM element for those terms.
| Attribute | Role |
|---|---|
data-tip="…" | Tooltip body text (same as base pattern) |
data-tip-href="https://…" | Triggers JS card instead of CSS card; "Learn more ↗" links here |
aria-label="…" | Screen-reader text (same value as data-tip) |
CSS suppresses the ::after/::before pseudo-elements for these elements; the JS card takes over with pointer-events: auto. A 120ms hide delay lets the cursor travel from the trigger across the gap into the card without it closing.
<!-- CSS-only (no link) -->
<abbr aria-label="First Contentful Paint — …" data-tip="First Contentful Paint — …">FCP</abbr>
<!-- Interactive (with Learn more link) -->
<abbr aria-label="First Contentful Paint — …"
data-tip="First Contentful Paint — …"
data-tip-href="https://web.dev/articles/fcp">FCP</abbr>
<!-- Import both files; JS self-injects its card CSS -->
<link rel="stylesheet" href="https://assets.gf.cx/tooltip/tooltip.css">
<script src="https://assets.gf.cx/tooltip/tooltip-interactive.js" defer></script>
Limitations
- Above only by default. Near-top-of-viewport elements need
data-tip-pos="below". Near-right-edge elements needdata-tip-align="right". The CSS has both variants. - SVG elements can't host pseudo-elements. Overlay an invisible HTML hit-target on the SVG datum instead — see
assets.gf.cx/tooltip/README.md.
Where it applies
- Report tables with metric acronyms (FCP, LCP, CLS, GSC, CrUX)
- Dashboard kicker labels, status-bar legends, sortable column headers
- Any surface where the primary audience is the operator, not a general public
Interactive tooltip · data-tip-href
"Lets A/B test — for selector-area tooltips that have an embedded link that lights up red. This involved smart demarcation of the zone, to allow the user to 'click it' without it disappearing."
The problem
The CSS-only data-tip tooltip is text-only. content: attr(data-tip) renders as a plain string — no HTML, no links, no interactive elements inside the card. The pseudo-element also carries pointer-events: none, so even if you could inject a link, the cursor would fall straight through it. For glossary terms that have canonical reference pages (FCP → web.dev/articles/fcp), a "Learn more ↗" link inside the tooltip is genuinely useful — but CSS can't deliver it.
The shape
| Piece | What it does |
|---|---|
data-tip-href="https://…" | Signals that this element needs the JS card — the CSS card is suppressed for these elements via an explicit override rule in tooltip.css |
tooltip-interactive.js | Self-contained IIFE. Queries all [data-tip][data-tip-href] elements, injects a single shared DOM card (.tip-interactive) into document.body, positions it above each trigger on mouseenter |
| 120ms hide delay | The guard that makes the pattern work. On mouseleave a timer schedules hide rather than hiding immediately — the cursor can travel from the trigger across the gap into the card. mouseenter on the card cancels the timer |
| Red "Learn more ↗" link | .tip-interactive__link — color: #c8364c, bold, opens in new tab with rel="noopener" |
The trap — hover-zone gap
If you hide the card on mouseleave of the trigger immediately, there is a pixel gap between the trigger element and the card (the caret arrow space). The cursor momentarily has no target — the card vanishes before the user reaches the link. The 120ms delay is the fix: small enough to feel instant, large enough for a deliberate move from trigger to card.
Live demo
Hover the dotted term below. When the card appears, slide your cursor across the gap to click the red "Learn more" link — the 120ms delay keeps the card open while you travel.
The browser first paints meaningful content at FCP. Both attributes data-tip + data-tip-href are required for the JS card to activate.
CSS suppression rule
Already baked into tooltip.css v0.2.0. Prevents the CSS card from doubling up with the JS card:
abbr[data-tip][data-tip-href]:hover::after,
abbr[data-tip][data-tip-href]:hover::before { display: none !important; }
Where it applies
- Glossary terms in technical reports that have a canonical reference URL
- Any tooltip where a secondary action (read more, open docs, copy value) adds genuine utility
- Not needed when the tooltip is self-contained — the pure-CSS pattern is lighter and sufficient for most cases
Context within the field · inline origin chips
"Not sure how this was encoded into the json, BUT it's a worthy pattern — you added where the traffic is originating. Very nice touch — helpful, and it's context-specific, within the field."
The problem
A diagnostic table row that shows what happened (path · count · status codes) leaves the next obvious question — where from? — to be answered by a click, a panel switch, or a fresh query. By the time the reader has navigated to a separate "by country" or "by user-agent" view, they've lost the row that prompted the question. The cost is small per row and large across a dashboard that has dozens of rows.
The shape
Earn the row another row. Under each diagnostic record, render a thin sub-row of origin chips — country codes + non-browser UA labels — scoped to that record's data. No new section, no drill-down: the qualifier sits in the field with the field it qualifies.
/ip 44 ████████ 200 301
┌── chips sub-row ────────────────────────────────────┐
│ CN 25 US 9 SG 6 HK 5 · headless-chrome 1 nginx-ssl 1 │
└─────────────────────────────────────────────────────┘
Encoding · the three layers
| Layer | What it does |
|---|---|
| Fetch · scoped group-by | One GraphQL call groups by clientCountryName + userAgent, filtered to the probe paths you care about (/ip, /cf, /trace). Keeps the payload small and the chips per-row. |
| Classify · UA → short label | A small dict (_SCANNER_UA_PATTERNS) maps noisy UAs to dense labels: HeadlessChrome → headless-chrome, python-requests → python-requests, curl/ → curl. Browsers (no match) drop out — chips show only the interesting non-browser callers. |
| Render · attached sub-row | A second <tr> with colspan="4" immediately follows each parent row. Country chips first, then a · separator, then scanner chips in a different chip class. Empty origins → no sub-row (graceful degradation). |
Why the chips belong in the field
- Reading flow uninterrupted. The eye that asks "why 44 hits on /ip?" gets the answer in the next saccade — not the next click.
- Top-N truncation per row.
top 8 countries · top 4 scannerskeeps the chip strip from becoming a second table. Long-tail callers don't push the parent row off-screen. - Top-level breakdowns still exist. Per-row chips don't replace the page-wide "by country" table — they answer "for this row specifically" while the page-wide view answers "for the zone overall."
- Scoping prevents noise. The fetch is filtered to the small set of paths where origin actually matters (probes, login routes). Don't run it for every row in a 200-row table — pick the rows where origin earns its space.
Where it applies
- Probe / diagnostic surfaces where the question "who's poking this?" follows immediately from the count (
/ip,/cf,/trace, healthchecks, sandbox endpoints) - WAF / firewall rows where the country + bot signature is the actionable context
- Email + webhook delivery dashboards — failed-delivery row + recipient-domain + retry-source chip
- Any table where the parent row is a category and the chips name the actors inside it
Reference implementation
- dash.dare.co.uk "Diagnostic probes" section — chips on
/ip//cf//trace ~/Code/dare-pipeline/scripts/dare_cf_analytics.py—fetch_probe_origins()(fetch + classify),_SCANNER_UA_PATTERNS(UA dictionary),probe_origins_row()(sub-row render)
Don't break out to a panel what fits in a row. The qualifier belongs in the field with the field it qualifies.
Pattern preview · live demo + tabbed code
"How could we put in real-code examples of the elements — so that we can see it, like how CodePen does it? Visual + code to the side. It's a preview pattern, for pattern library."
The problem
A pattern entry that documents interactive behaviour — a tooltip, a pulse, a hover-zone guard — earns one <pre><code> block in the prose and asks the reader to imagine what it looks like. The reader can either trust the description or open a new browser tab and rebuild it themselves. Either way, the learning loop is broken: the page tells you about the behaviour without letting you feel it.
The shape
A reusable block that pairs an isolated live demo with the source that produced it. Demo runs in an iframe sandbox so its styles never leak into the host page. Source is shown in a tabbed dark panel (HTML / CSS / JS) with a Copy button. Side-by-side at desktop, stacked at mobile.
| Piece | What it does |
|---|---|
<template data-tab="html|css|js"> | Author writes raw source inside <template> elements. data-tab names the language for the tab label + iframe assembly. |
data-external-css / data-external-js | Comma-separated URLs of external assets to load inside the iframe head (e.g. assets.gf.cx/tooltip/tooltip.css). Lets a demo depend on the portfolio's primitive CSS without redefining it. |
pattern-preview.js assembles srcdoc | Reads each template, dedents the source, builds a single <iframe srcdoc="…"> with external assets in <head> + inline code in <body>. Iframe is sandbox="allow-scripts allow-popups". |
| Tabs + Copy button | Click a tab to switch the source pane (HTML default). Copy button copies the currently-shown source to clipboard with a 1.6s "Copied ✓" confirmation. |
Live demo — meta
This block is itself a pattern-preview, demoing the simplest possible interactive case: a button that increments a counter. Inspect the three tabs to see how html / css / js compose into the live iframe on the left.
The trap — host-page style leak
The naive version inlines the demo HTML directly into the patterns.gf.cx body. The host page's serif fonts, link styles, code styles, and CSS variables apply to the demo — the demo no longer looks like the surface where it'll actually run, and any aggressive demo CSS (e.g. body { background: black }) leaks back into the host page. The iframe sandbox is non-negotiable: each demo gets its own document scope, its own root element, its own stylesheet origin.
Why srcdoc over a separate file
- Single source of truth. The same code text feeds the tabbed view AND the iframe — no risk of the displayed source drifting from the running demo.
- No HTTP round-trip per pattern. Demos are inline in the page; no
/demos/<slug>.htmlfile to maintain. Adding a new pattern is one<div>. - External assets still load. Absolute URLs in
data-external-css/data-external-jsresolve normally inside the srcdoc. Relative URLs do not — that's the one constraint.
Where it applies
- Any pattern entry that describes interactive or visual behaviour the reader can't predict from the code alone (tooltips, hover states, animations, focus management, micro-gestures)
- Onboarding docs that need a "see it work first, then read the why" rhythm
- NOT for patterns that are purely conceptual (deploy rituals, naming conventions) — the demo would be performative noise
Reference implementation
/assets/pattern-preview.css— layout grid, tab styling, mobile stack/assets/pattern-preview.js— template reading, srcdoc assembly, tab + copy behaviour. ~120 lines, no dependencies.- In use: this section · Jargon tooltip · Pulse on icons · Interactive tooltip.
A pattern library that only describes its patterns is a glossary. A pattern library that lets you feel them is a workbook.
Built with · provenance footer
"'Built with' can be added to patterns as a footer behavior … can we reveal/hide it, especially when the script list is long — do any of the scripts have references back to pattern/memory that can be linked programmatically?"
The problem
A composed page — a voiced article, an ingested clip, a generated dashboard — is the visible output of a pipeline the reader never sees: which script built it, which model transcribed it, when it ran. That provenance is real and worth keeping, but pasted raw at the foot of every page it's clutter, and it goes stale silently. Once the build list grows past three or four tools it dominates the footer it was meant to quietly annotate.
The shape
A single collapsible block: a <details> whose <summary> stays compact — label, tool count, an ingest <time> stamp visible while collapsed — and reveals the full per-tool list on click. Provenance-at-a-glance closed; full ledger open. No JS — <details> carries the reveal/hide for free; the disclosure triangle is a CSS ::before on the summary that rotates 90° on [open], with the native marker hidden.
<details class="provenance">
<summary><span class="t">Built with</span><span class="meta">4 tools · ingested
<time datetime="2026-06-18T12:50:15Z">18 Jun 2026</time></span></summary>
<ul>
<li><a href="https://kb.gf.cx/<slug>/"><code>~/bin/happiness_video_ingest.py</code></a> — ffprobe · poster · encode · R2</li>
<li><code>gemini-2.5-flash</code> — verbatim transcription</li>
</ul>
</details>
.provenance > summary::before { content: "\25B8"; transition: transform .15s; }
.provenance[open] > summary::before { transform: rotate(90deg); }
.provenance > summary::-webkit-details-marker { display: none; }
Linking back · the registry convention
The payoff is the second half of the question: when a listed tool has a canonical home — its governing pattern on this site, or the memory note that documents it (published at kb.gf.cx/<slug>/) — its row links there. The reader goes from what built this to why it's built this way in one click. The link targets already exist; what's missing is a mapping, not new infrastructure.
Keep the map in one place — the footer / OG helper that emits the block — rather than hand-authoring anchors per page:
BUILT_WITH = {
"happiness_video_ingest.py": {
"label": "~/bin/happiness_video_ingest.py",
"href": "https://kb.gf.cx/feedback_video_ingest_composable_transcripts_2026-06-18/",
},
# external tools: link to their own home, or omit href
"ffmpeg": {"label": "ffmpeg / ffprobe"},
}
A script that wants to own its own provenance can instead declare it in its docstring — a # pattern: <slug> / # kb: <slug> tag the builder harvests — but the central registry is the floor: it works today with no edits to the tools themselves.
When it breaks
- Default-open on short lists. One or two tools don't need hiding — the disclosure is friction. Collapse by default only when the list earns it (≥3–4 rows); a short list can ship
open. - Hand-typed timestamps. The
<time>is provenance — it must be stamped by the build step, not typed, or it lies. Emit it where the page is generated. - Dead registry links. A linked kb/pattern slug that 404s is worse than no link. The same external-probe discipline applies: a link ships only once its target resolves.
Reference
First live on happiness.gf.cx/behaviour/control-is-not-regulation/ (2026-06-18) — a three-clip ingest whose footer lists the ingest script, ffmpeg, the gemini-2.5-flash transcription step and wrangler, with the ingest script linking back to its composable-transcript directive on kb.gf.cx. Generalises toward the shared footer / ~/bin/og_capture.py helper so new surfaces inherit linkable provenance rather than a static list — same scaffold-it-as-the-floor logic as the scaffold baseline.
Editorial article · micro-decision grouping
"a grouping of 3-4 micro-decisions that rolls up nicely."
The problem
An advice or how-to article written as flowing prose buries its useful parts. The reader has to read the whole thing to extract the moves, the writer has to find a single narrative thread to thread them on, and nothing in the piece is independently quotable or skimmable. The structure fights both the writing and the reading.
The shape
Compose the article as a thesis plus a small grouping of self-contained moves. Each move is a micro-decision: an imperative sub-heading — a single concrete instruction — followed by one to three tight sentences. Ideally three or four moves (the set is small enough to hold in the head); list pieces can scale to about a dozen. The grouping "rolls up" into one takeaway stated under the title.
- Title + a one-line thesis — the rollup; what it all amounts to.
- 3–4 labeled moves. Each is an imperative subhead + 1–3 sentences. Real subheads from the live exemplars: "Slow down by about twenty percent." · "Ask the second question." · "Match the greeting to the situation" · "Take control of the greeting."
- Each move is self-contained and skimmable — the set rolls up under the thesis, but any one move stands alone.
- Optional tail — crisis/footer lines, or the built-with provenance footer.
Why it works
- Skimmable. Each move's subhead is independently useful — a reader gets value scanning the headings alone, before reading a single sentence of body.
- Lowers the bar to write. Decide the three or four moves first, then fill each. The hard part — structure — is done before any prose is written; the easy part is one move at a time.
- The grouping creates a gestalt. The small set composes into the article's single thesis. It "rolls up nicely": modular but coherent, each move standing alone yet reinforcing the whole.
When it breaks
- Too many moves. Past a dozen the rollup dissolves — the reader can't hold the set, and it reads as a listicle, not a thesis. The canonical count is 3–4; treat the list-scale variant as the ceiling, not the target.
- Soft subheads. A subhead that describes rather than instructs ("On greetings") loses the skim value. Each must be a concrete imperative the reader could act on without reading the body.
- No rollup. Moves that don't compose into one takeaway are just a list. The thesis under the title is what makes the grouping an article.
Reference
The authoritative editorial pattern for articles is happiness.gf.cx. Live exemplars, from the ideal count outward:
- happiness.gf.cx/behaviour/the-four-second-read/ — 5 moves, closest to the ideal 3–4.
- happiness.gf.cx/behaviour/on-being-heard/ — ~8 moves.
- happiness.gf.cx/body/eleven-things-self-growth/ — 11 moves, the list-scale variant.
Diagnostic probes · quarantine panel
"This diagnostic pattern should be preserved … it's a good one. It should be a composable script that can be used easily."
The problem
Every gf.cx surface ships "where am I?" diagnostic endpoints — /ip, /cf, /trace (the functions/ip.ts · cf.ts · trace.ts that the scaffold stamps into each subdomain). Almost nobody hitting them is a reader: they're pinged by uptime checks, port scanners, headless bots, and your own probes. Left in the analytics, that traffic inflates the top-paths table and buries real reader behaviour. But just deleting it throws away a genuinely useful signal — who is poking the perimeter, and from where.
The shape
Don't delete the noise — quarantine it. Filter the probe paths out of the reader-facing tables, and surface them in their own collapsed <details> panel that answers the different question: per-path request count + bar + status codes, a country-of-origin breakdown, and a scanner-UA classification (headless-chrome, nginx-ssl, bastion, python-requests…). Collapsed by default — it's infrastructure noise, one click away, out of the reader's path — and rendered in an olive/brown that quietly says "not reader traffic." No JS: <details> carries the reveal for free.
Diagnostic probes — /ip · /cf · /trace (excluded from reader view)
| /ip | 70 | 200301 | |
| CN 24SG 18HK 18MX 12US 12·headless-chrome 2nginx-ssl 1bastion 1 | |||
diagnostic_probes_panel.py --demo. Click the summary to collapse.Composable · two calls
The whole pattern is extracted into one dependency-free module — ~/bin/diagnostic_probes_panel.py — so any surface drops the panel in with aggregate() then render_panel(), or a single call if you have a Cloudflare token:
from diagnostic_probes_panel import aggregate, render_panel
# from your own Cloudflare httpRequestsAdaptiveGroups rows:
panel = render_panel(aggregate(rows)) # → self-contained HTML, own CSS, no JS
# or end-to-end (needs an Analytics:Read token):
panel = render_panel(fetch(token, zone_id))
# or just look: diagnostic_probes_panel.py --demo
The shape passed between the two halves is a stable contract — aggregate the noise once, render it anywhere:
{ "/ip": { "requests": 70, "statuses": [200, 301],
"countries": [["CN", 24], ["SG", 18], ...],
"scanners": [["headless-chrome", 2], ["nginx-ssl", 1], ...] } }
When it breaks
- Probe-path drift. The filter set is the pattern — rename
/traceon a surface and forget to updatePROBE_PATHS, and the noise leaks back into the reader view (or the panel goes silently empty). Keep the path list in one place. - The UA map is an allow-list. Unknown scanners fall through as plain country counts — fail-open by design — so "no scanners" means "none I recognised," not "no bots." Add patterns as you spot them; don't read absence as proof.
- Don't serve it to readers. The panel is for you — a back-of-house instrument, not content. Keep it excluded from reader view and out of OG/SEO. The same external-probe discipline applies in reverse: some things are measured, not shown.
- It must degrade, not crash. Country/scanner data comes from Cloudflare's GraphQL — on a token failure
fetch()returns{}and the panel renders empty rather than taking the page down with it.
Reference
Extracted from dare_cf_analytics.py's probe section — the dare.co.uk dashboard, where the panel first earned its keep filtering /ip · /cf · /trace out of the reader tables — and generalised into ~/bin/diagnostic_probes_panel.py on 2026-06-19. The probe endpoints themselves are part of the scaffold baseline (every surface gets /ip · /cf · /trace); this panel is their natural back-of-house companion — same scaffold-it-as-the-floor logic.
The footnote pattern
"let this be the 'footnote pattern' as it also highlights in red, and it picks out highlight bold for particular words or phrases."
The problem
A long-read accretes reader questions and replies in the margins. Dumped inline they swamp the spine of the piece; given long descriptive titles they turn the footer into a wall of competing headlines. You want the appendix dense and scannable when closed, and fully self-announcing when opened — without the collapsed list shouting over the article above it.
The shape
Each footnote is a collapsible <details> whose closed row is a single word, and whose open body leads with the full title carried inside. Two type signatures do the emphasis: a red highlight for the key term, and a bold pick-out for the particular word or phrase under examination.
- Summary = one word. The collapsed row is just a muted
footnotelabel and a single italic keyword, right-aligned into a clean column. Every keyword on the page is unique — it is the footnote's handle. - Title carried into the body. The full descriptive title moves to the first line of the opened body (
.fn-headline) and ends on that same emphasized keyword — so closed stays a one-word column, open announces itself. - Two signatures. Red
<em>(the accent colour) carries the running emphasis and the keyword; bold<strong>picks out the one word or phrase being examined. Nest<em><strong>…</strong></em>for bold-red. - Body convention. The prompt — the reader's own words — opens as a single red-italic paragraph; the reply follows, picking out terms in red and bold as it lands on them.
<details class="context-note">
<summary><span class="fn-kind">footnote</span>
<em style="color:inherit;text-transform:none;letter-spacing:0;">glance</em></summary>
<div class="context-note__body">
<p class="fn-headline">Silence over complaint, the choice that was yours — the backward <em>glance</em></p>
<p><em>…the prompt, in the reader's own words…</em></p>
<p>…the reply, picking out a <em><strong>word</strong></em> where it lands…</p>
</div>
</details>
The collapsed row right-aligns the keyword to a column, and the carried-in headline sits at the top of the opened body:
.footnotes .context-note summary em { margin-left: auto; }
.fn-headline { font-family: var(--serif); font-size: 19px; line-height: 1.3;
color: var(--ink); font-weight: 600; margin: 0 0 16px;
padding-bottom: 13px; border-bottom: 1px solid var(--line); }
.fn-headline em { font-style: italic; color: var(--accent-aches); }
Why it works
- Dense when closed. A column of single words is an index, not a second article — the appendix never competes with the spine above it.
- Self-announcing when open. The carried-in headline means a note explains itself the instant it's expanded; the one-word handle never costs the reader context.
- The eye has two registers. Red for the through-line of emphasis, bold for the single term under the lens — so a long reply still has one obvious place the argument turns.
- The keyword is a memory hook. One unique word per note is easier to recall, and to link back to, than a half-sentence title.
When it breaks
- Two-word creep. The discipline is one word — let it grow to a phrase and the collapsed column stops being an index. A tight two-word term (clean exits) is the ceiling, not the target.
- Bold everywhere. If every other word is picked out, nothing is — the
<strong>pick-out only works rationed to the one term that matters in that paragraph. - Orphan keyword. If the summary word never appears in the carried-in headline, the closed and open states stop agreeing. The headline must land on the same word.
Reference
Origin and authoritative exemplar is happiness.gf.cx's behaviour/ long-reads:
- happiness.gf.cx/behaviour/the-1am-paragraph/ — where the single-word reveal was first cut.
- happiness.gf.cx/behaviour/scorched-earth-game-plays-out/ — converted to the pattern 2026-06-22 (keywords call · yours · state · keystone · table · collateral · protective · glance).
Named by Dan on 2026-06-22. Pairs with the micro-decision article grouping — that pattern shapes the spine, this one shapes the margins.
Memorable URL · canonical redirect
"Rather than writing vault.gf.cx/software/tailscale — do vault.gf.cx/tailscale and have a redirect manage the forwarding, with the correct canonical URL. It demonstrates ease-of-use, memorable URL structures with the correct canonical."
The problem
Good taxonomy buries good URLs. A page earns its place deep in a section — vault.gf.cx/software/tailscale/ is correct: it lives under /software/ with its siblings, and that path is what crawlers and the sitemap should treat as the one true home. But nobody types or remembers a three-segment path. Ask someone to "go to the tailscale page" and they'll try vault.gf.cx/tailscale first. Flatten the structure to win the memorable URL and you lose the taxonomy; keep the taxonomy and you lose the door people actually knock on.
The shape
You don't choose. Keep the content at its canonical deep path, and add a short, memorable vanity URL that 301-redirects to it. Two artefacts, one source of truth:
- The redirect (CF Pages
_redirects) forwards the memorable door to the real room — splat included so deep links survive:
# _redirects — memorable vanity → canonical deep path
/tailscale /software/tailscale/ 301
/tailscale/* /software/tailscale/:splat 301
- The canonical tag on the page keeps declaring the deep path as the truth, so the
301and the<link rel="canonical">agree and link-equity consolidates onto one URL:
<link rel="canonical" href="https://vault.gf.cx/software/tailscale/">
Now advertise the memorable one (vault.gf.cx/tailscale) everywhere a human reads — cross-links, social, print, "go to…" — and the structured one stays the canonical record. The redirect bounces the door to the room; the canonical tag tells the crawler which is the room.
Why 301, not 302
A vanity URL is a permanent promise, so use 301 (permanent): browsers and crawlers cache it and consolidate ranking signals onto the canonical target. Reserve 302 for genuinely temporary forwards where the destination is still settling (e.g. an IA mid-migration). Mixing them up either fails to pass equity (302 where you meant permanent) or hard-caches a move you'll want to undo (301 where you meant temporary).
When it breaks
- Canonical points at the vanity URL. If the page declares the short URL as canonical but the redirect sends the short URL away, you've told the crawler the canonical is a thing that 301s elsewhere — a contradiction. The canonical must be the URL that actually serves
200(the deep path). - Vanity collides with a real path.
/tailscalemust not also be a real directory. Vanity slugs live in a flat namespace at the root; keep them distinct from section names. - Forgetting the splat.
/tailscalealone redirects the bare page but/tailscale/anything404s. Add the/*→:splatline if the target can ever grow sub-pages.
Where it applies
- Any deep-but-frequently-shared page — a tool, a single report, a vendor record — that deserves a one-word door.
- QR / print / spoken contexts where the URL has to be typeable from memory (companion to the tag-router numeric + slug peers pattern, which solves the same memorable-vs-structured tension for asset tags).
- Cross-surface links: srvc.gf.cx links the homelab write-up as
vault.gf.cx/tailscale, not the deep path — the memorable form is the one that travels.
Reference
First shipped 2026-06-24: vault.gf.cx/tailscale → 301 → vault.gf.cx/software/tailscale/ (canonical). The earlier /field-notes/<vendor> → /<vendor>/ rules on the same surface are the temporary-302 sibling of this permanent-301 pattern — same mechanism, opposite intent.