stratum · round one · measured

The cloth hero

The seven laws hung as a Verlet cloth over the live field. The pointer perturbs and the cloth settles on its own: recovery to 0.12 mean Δ at 8 s, byte-identical on reload.

evidence produced by scripts in artifact/strata/cloth-hero/ · plates in the review · live in the canon
The cloth hero plate

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

surface1440×900390×844 @2
this stratum alone (harness-void.html)60.3 fps60.6 fps
stratum over the live field (harness.html)16.0 fps48.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:

elementsizeratio
h1 (wordmark)49.6px20.38
h1 s (slash, o-4)49.6px5.21
h1 i (dot, essence)49.6px14.47
.hero-tag span9.5px8.03
.hero-tag b (sigil-text)9.5px5.01
.hero-meta span10.5px11.72
.hero-meta em (o-4)10.5px5.21
.lede.hero-lede (o-3)15px8.03
.ch-key (o-3)10px8.03
.ch-key b (sigil-text)10px5.01
.ch-read (o-1)10px16.33
.ch-read b (sigil-text)10px5.01
.ch-read span (o-4)10px5.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