
Evidence
Everything below was produced by scripts in this directory, run in Playwright's Chromium with --use-gl=swiftshader --enable-unsafe-swiftshader. Headless has no GPU, so every fps figure is a lower bound: software GL is roughly an order of magnitude slower than real silicon.
harness.mjs builds harness.html — this stratum over canon.BEFORE.html (field shader live underneath)
harness-void.mjs builds harness-void.html — this stratum alone on a void page (isolates its own cost)
capture.mjs screenshots + the interaction sequence
shot2.mjs four screenshots + fps on both harnesses
verify.mjs the numbers below
lean.mjs the chromatic-lean and ghost-direction measurement
recover.mjs the isolated recovery measurement
probe.mjs canvas-vs-DOM metric check for the mark
cells.mjs hidden-layer readout across six viewports
shots/ desk · desk-reveal · desk-perturb · desk-recovered · desk-scrolled · phone · void · void-hover · reduced
Shaders
None. This stratum adds no GLSL — the field shader it sits over is the base artifact's and is untouched. verify.mjs collects console.error, which is where mountField logs shader: / program: compile and link failures. console errors across all six runs: 0. Warnings: 3 unique, none from this stratum — two GPU stall due to ReadPixels from swiftshader servicing the screenshot readback, one willReadFrequently hint raised by the test scripts' own getImageData sampling (the stratum's single readback context already sets that flag).
Frame rate
| surface | 1440×900 | 390×844 @2 |
|---|---|---|
this stratum alone (harness-void.html) | 60.3 fps | 60.6 fps |
stratum over the live field (harness.html) | 16.0 fps | 48.5 fps |
Measured over 3s of rAF. The stratum alone is vsync-capped at both sizes on software GL, so its own frame cost is under 16.7ms there; the full-page figure is dominated by the WebGL field, which the BEFORE artifact renders at a comparable rate on swiftshader with no stratum present. Particle counts: desktop 82 × 20 = 1640, phone 39 × 15 = 585.
It animates
Sampled the alpha channel of each canvas twice, ~450ms apart, at 1440×900:
animates_cloth = true
animates_phantom = true
Both differ. The cloth differs because it is still integrating; the phantom differs because the pulse is moving every dot's radius.
Reduced motion — static AND correct AND reproducible
Context opened with reducedMotion: 'reduce'. Two samples 1.5s apart:
reduced_cloth_static = true (mean |Δalpha| = 0, exactly)
reduced_phantom_static = true (mean |Δalpha| = 0, exactly)
reduced_cloth_ink_cells = 7819 non-transparent sample cells
reduced_phantom_ink_cells = 1050 non-transparent sample cells
reduced_drift = "none" (scroll drift transform disabled)
Not blank, not slowed, not a frozen live frame: the cloth runs 60 fixed 16ms steps with no pointer and draws once; the pulse is evaluated at t = 3.0 exactly as §15 specifies (sin⁴(4.65) ≈ 0.9915 — near systole, a bright frame, and a chosen one rather than t = 0).
Reloaded a second context at the same viewport and compared pixel-for-pixel against the first:
reduced_reproducible = true
Same seed, same output. There is no Math.random in the file.
The pointer perturbs; it does not drive
Measured on harness-void.html at 1440×900, sampling every 17th alpha byte of the cloth canvas, as mean absolute difference from the undisturbed frame:
rest → after a 24-step pointer sweep 4.93 – 5.71
t + 1s 6.05 (the wake is still propagating)
t + 2s 2.59
t + 4s 2.73
t + 8s 0.12 (settled back to the shape it hangs in)
The pointer is gone from the band for that whole window. The cloth returns on its own. recover.mjs, sampling more finely, reads 5.54 → 5.3 → 3.0 → 0.93 → 0.29 over the same schedule.
There is no grab: pointerdown is not listened for at all, so holding the button and dragging moves nothing. Deleting every pointer listener leaves the cloth integrating, settling and rendering — the band is authored in the no-pointer state and shots/desk.jpg is that state.
The rest-memory term (RESTORE, dt-normalised) is a deliberate departure from ClothManifesto.tsx, which has no such term. Without it one sweep leaves the cloth permanently re-shaped, which is authoring, not perturbation. The horizontal stretch limit also came down from 4.0× to 2.2× for the same reason: at this grid density 4.0× shreds columns open instead of letting them billow. Both changes are recorded in the source with their reason.
The chromatic lean
lean.mjs, averaged over 8 samples to wash out the pulse. Red/cyan energy is summed over the whole phantom canvas; direction is checked per scanline.
data-lean flag 0 (pointer outside) → 1 (pointer in the band)
red energy 183.8k → 211.3k (+15%)
cyan energy 154.5k → 173.4k (+12%)
scanlines measured 103.5
left edge is red-led 100.9 / 103.5 → 102.4 / 103.5
right edge is cyan-trailed 102.3 / 103.5 → 102.3 / 103.5
Red leads, cyan trails, on essentially every scanline in both states. The witness core does not move; the split offset goes 2px → 5px and its alpha 0.24 → 0.38. Law 5 holds by construction: the core is a separate pass.
Text contrast
Every element in the band that owns a text node, measured against its nearest opaque composited ancestor background (#050505), computed alpha-blended:
| element | size | ratio |
|---|---|---|
h1 (wordmark) | 49.6px | 20.38 |
h1 s (slash, o-4) | 49.6px | 5.21 |
h1 i (dot, essence) | 49.6px | 14.47 |
.hero-tag span | 9.5px | 8.03 |
.hero-tag b (sigil-text) | 9.5px | 5.01 |
.hero-meta span | 10.5px | 11.72 |
.hero-meta em (o-4) | 10.5px | 5.21 |
.lede.hero-lede (o-3) | 15px | 8.03 |
.ch-key (o-3) | 10px | 8.03 |
.ch-key b (sigil-text) | 10px | 5.01 |
.ch-read (o-1) | 10px | 16.33 |
.ch-read b (sigil-text) | 10px | 5.01 |
.ch-read span (o-4) | 10px | 5.21 |
Minimum 5.01:1. Nothing sits below the o-4 floor. Every one of these is a DOM text node; the cloth and the mark are canvas marks, not text, and the words they carry are stated as real prose in section 00.
Controls
<button> · 142.8 × 44 px desktop · 135.6 × 44 px phone · tabIndex 0
focus reaches it by Tab = true
:focus-visible outline = rgb(194,51,61) 1px solid, offset 2px (sigil, per §15)
Enter toggles aria-pressed → "true" = true
Space toggles aria-pressed → "false" = true
readout while lit = "· 45 periods lit every mark contains one"
readout while dormant = "· 45 periods in the weave dormant"
44px on both axes of the minimum dimension, keyboard operable with Enter and Space, and role="status" on the readout so the change is announced.
Guardrails audit (whole band, computed styles)
non-zero border radii : 0
<svg> : 0
<img> : 0
emoji : none
gradients : 1 — .hero-veil, the sanctioned one, re-tuned not added
typefaces rendering : Geist, Departure
font weights rendering : 400, 500
essence in computed CSS : only h1 > i — the wordmark's period
sigil in computed CSS : only the :focus-visible outline
Essence also appears on canvas, in exactly one place: the revealed period constellation, gated behind the pressed control and beating on the pulse. Never as a fill, a background or a border. The phantom mark's own period is not essence — it is witness at greater dot density, so the band does not introduce a second living dot alongside the field's (§09).
Hidden layer, across viewports (cells.mjs)
1440×900@1 → 45 periods 900×760@1 → 34 periods 390×844@2 → 17 periods
1280×800@1 → 43 periods 640×900@2 → 26 periods 360×640@3 → 12 periods
The count is a pure function of the text and the grid, it is what the readout prints, and it is what is lit. The earlier port of ClothManifesto's encodeCyanCells is gone: it does not round-trip, and on a short grid it silently dropped the tail of the message (measured: 18 → 14 chars at 390×844, 18 → 7 at 360×640 before it was replaced).
Mark geometry
probe.mjs compares canvas metrics against the DOM at 400px:
canvas measureText("/.") with letterSpacing -0.022em = 234.4px
DOM span, same font, same letter-spacing = 234.4px
document.fonts.check('500 400px "Geist"') = true
Exact agreement, so the phantom is the same mark the wordmark is. This only became true after adding an explicit document.fonts.load for both faces: fonts.ready alone resolves without them when no DOM node has demanded them, and the mark was being measured and drawn in a fallback face.
Other
destroy() → canvases removed = 0 left, host class removed = true
phone horizontal scroll = false
scroll drift @400px = matrix(1,0,0,1,0,-16) (linear in scroll, capped at -36px)
Not established
- No measurement on real mobile silicon; every figure here is software GL in a headless container.
- The rest-memory constant (0.022, ≈0.8s to forget) is asserted taste, not derived. It was chosen by watching recovery, not computed from anything.
- The phantom's dot-radius coefficient is 0.44, not §09's 0.62. The reason is given in the source — a glyph mask saturates where a noise field does not, and at 0.62 the marks fuse into the grey fill law 2 forbids — but the specific value 0.44 is chosen, not derived.
- Contrast is computed against the flat void ground. The band's true backdrop is a moving field under a veil; the veil is the legibility device for that and its stop values are tuned by eye, not measured against the field's brightest frame.