
Evidence
Built and measured directly on the real artifact. build-harness.mjs inlines artifact/design-system.src.html with the bundled fonts the way artifact/build.mjs does, grafts this stratum's CSS, HTML and JS onto the end of <main> and mounts it. Everything below is produced by probe.mjs (Playwright / Chromium, --use-gl=swiftshader --enable-unsafe-swiftshader, results in probe.json). Screenshots: shot-1440.png, shot-390.png, shot-reduced.png, and crop-plate.png (the plate at 2× nearest-neighbour, for reading the lattice).
Re-run:
node artifact/strata2/figure-played/build-harness.mjs
node artifact/strata2/figure-played/probe.mjs
node artifact/strata2/figure-played/fix-probe.mjs
Two adversarial reviews (review-doctrine.md, review-a11y-robust.md) found one blocking defect and nine should-fix defects, every one of them reproduced. All ten are fixed on disk. fix-probe.mjs re-measures exactly what they put in question and writes fix-probe.json; §10 below is that re-measurement, and the sections it supersedes (§2, §3, §6, §7, §8) carry the new numbers. Nothing is contested. fix-probe.mjs launches one browser per measurement: sharing one across twenty pages of a continuously repainting artifact under swiftshader starves the renderer until Playwright times out on actionability — a property of the harness, not of the stratum, but it made the numbers unrepeatable.
What it is
Three ephemeris quantities, all of them already printed by §09, and nothing else: the six-bit figure decides which of six pulse trains run and how, the lunar mansion sets the fundamental, and the planetary hour sets the tempo. Line i is bit i of a 0–63 step counter. A solid line plays its train whole (32 of 64 steps); a broken line drops the second pulse of every pair (16 of 64). Line 6 is the exception and it is stated, not fudged. Its train is one unbroken block of 32 inside a 64-step pattern, so it has no pair to drop; broken, it halves that one block — steps 32–47 sound, 48–63 do not. Same 16 of 64, gap in the middle, and no bit borrowed from another line. The first version of this stratum wrapped the pair-partner modulo 6, which made a broken line 6 fire at the line-1 rate (#.#.#.#) and quietly forbade line 6 broken and line 1 solid from ever coinciding. See §10.1. The sounding lines are exclusive-or'd to one bit per sample and passed through a 1200 Hz low-pass; a square an octave under the fundamental, gated by sin⁴(t·1.55) and passed through a 110 Hz low-pass, is the thump.
No name, no judgement and no reading is attached to any figure. The figure is used as six bits and nothing else.
1 · No audio node before the gesture — probe.mjs, section 2
addInitScript wraps AudioContext, webkitAudioContext and OfflineAudioContext in a Proxy before any page script runs, and records every construction with a performance.now() timestamp.
audio contexts constructed, page load -> just before click 0
performance resource entries matching blob:|worklet, before 0
handle.state() before the click none
click "listen"
audio contexts constructed 1
the one construction AudioContext @ 1524.1 ms
context state running
audio path taken AudioWorklet
button aria-pressed / label true / "silence"
printed state "playing · AudioWorklet · 44100 Hz"
click again
context state none
button aria-pressed / label false / "listen"
printed state "context closed · no audio node exists"
audio contexts constructed (still) 1
host data-fp-path attribute null
The second press ramps the master down, disconnects every node, nulls every onaudioprocess, and awaits ctx.close(). handleState: "none" is read after the close resolves.
2 · Output exists and its level — AnalyserNode, live
An AnalyserNode (fftSize 2048) sits between the master gain and the destination. 60 reads of getFloatTimeDomainData, 122 880 samples of the running graph. Re-measured after the line-6 fix (fix-probe.mjs I2 — the fix changes which lines sound together, so the level had to be taken again):
RMS -26.49 .. -26.35 dBFS
peak -19.38 .. -19.19 dBFS
ceiling required -18 dBFS PASS
clipping none
path / state AudioWorklet / running
Confirmed independently by an OfflineAudioContext render (6 s at 48 kHz) of the same generator through the same filter and gain chain, fix-probe.mjs section I:
RMS -26.60 .. -26.43 dBFS
peak -19.36 .. -19.19 dBFS
under the -18 dBFS ceiling true
clipped samples 0
Two ranges, not two numbers, because both are taken from a 6-second window of a piece whose phase is the planetary hour's own position: startStep differs between runs, so the six-second window contains a different slice of the 64-step pattern. Both endpoints are two runs of fix-probe.mjs an hour apart. The margin to the ceiling is 1.2 dB at the worst of them.
(Pre-fix: −26.88 / −19.67 live and −27.22 / −19.22 offline. The line-6 fix moved the peak by under 0.5 dB and did not approach the ceiling.)
The gain staging was measured, and the first setting failed. At the values first written (pulse 0.062, sub 0.050, arithmetic sum 0.112 = −19.0 dBFS) the render peaked at −16.04 dBFS — over the ceiling. The 2-pole low-pass rings on a square edge: measured overshoot 1.402× the sum of the voice gains. The gains were reset from the measured peak rather than from the arithmetic: 0.043 / 0.035, sum 0.078, ×1.402 = 0.1094 → −19.2 dBFS.
3 · The stream really is one bit — offline render, pre-gain buffer
The generator's own output, before any filter or gain, over 288 000 samples (fix-probe.mjs section I, after the line-6 fix):
exactly +1 or exactly -1 280 512
exactly 0 (no line sounding) 1
everything else (the edge ramp) 7 487
------------------------------------------------------------------
sum 288 000 100.00%
exactly one bit (or a rest) 97.40%
the ramp 2.60%
The +1/−1 split is not printed here because it is not stable: it was 132 620 / 147 892 in one run and 115 659 / 164 853 in another, since the window starts wherever the planetary hour stands. Their sum, the rest and the ramp are identical in both runs, which is the claim being made.
The 7 487 remaining samples are the 12 ms raised edge ramp at each step boundary, which exists because a hard gate clicks. 97.40% of the stream is ±1 or 0; the tone is carried by pulse density and made audible by the filter.
A review caught this section's own arithmetic. It previously printed 97.80% against counts that sum to 97.40% — a rounding of the four numbers that was never checked against the four numbers. The row is now printed with its sum so the check is on the page rather than left to the reader.
4 · Byte-identical bitstreams for the same date — probe.mjs, section 4
The 64 steps are serialised as 64 bytes, bit i set when line *i*+1 sounds.
Re-measured after the line-6 fix (the rule change moves every byte where line 6 is broken):
2026-09-03T09:00:00Z figure 30 000102020405060608090a0a0c0d0e0e...1e1e
2026-09-03T09:00:00Z figure 30 000102020405060608090a0a0c0d0e0e...1e1e
second, independent mount 000102020405060608090a0a0c0d0e0e...1e1e
identical true
2026-09-04T09:00:00Z figure 29 000102030405040508090a0b0c0d0c0d...1c1d
2026-03-21T00:00:00Z figure 4 000102020405040408090a0a0c0d0c0c...0404
differs across dates true
Full 64-byte strings in fix-probe.json under K_determinism.
The second mount is a separate mountStratum_figureplayed() on a cloned host, destroyed after reading. No Math.random appears in the stratum.
5 · The unequal hour, actually measured — probe.mjs, section 5
730 samples: one daylight hour (sampled at 09:00 UTC) and one night hour (21:00 UTC) for each of the 365 days of 2026, at the ephemeris's own observer. The solstice rows are sampled at 12:00 and 23:00 UTC.
observer 51.4779N, 0.0015W (the Royal Observatory)
day hour min 39.1442 max 83.1655 mean 61.3693 min
night hour min 36.8522 max 80.8945 mean 58.6307 min
all 730 min 36.8522 max 83.1655 mean 60.0000 min
step (hour / 4096) 539.83 ms .. 1218.24 ms
spread 125.67%
extreme ratio 2.257
steps per minute 111.15 .. 49.25
2026-06-21 day hour 83.165 min / night hour 36.852 min ratio 2.257
2026-12-21 day hour 39.147 min / night hour 80.894 min ratio 0.484
2026-03-20 day hour 60.704 min / night hour 59.105 min ratio 1.027
A longer hour is a longer step, so the direction is the opposite of the obvious one. The plate printed it backwards until a review recomputed it at the ephemeris's own observer:
2026-06-21 14:00Z day hour 83.1655 min step 1218.2 ms SLOWEST
2026-06-21 23:00Z night hour 36.8522 min step 539.8 ms FASTEST
ratio 2.257
faster on night
The 2.26 was right; the two words around it were swapped. The plate now reads "a longer hour is a longer step, so the piece is 2.26 times faster on a midsummer night than on a midsummer afternoon" — checked against the running DOM in fix-probe.mjs section B, not against the source file.
The brief said the ephemeris computes at 0°N 0°E; it does not. The shipped strata/ephemeris/stratum.js fixes the observer at LAT = 51.4779, LON = -0.0015 — the Royal Observatory — and states the reason in place: a planetary hour needs a sunrise, a sunrise needs a place, and the place must not be the viewer's. The copy on the plate names the real observer. It matters here rather than being a pedantic correction: at 0°N the daylight and the night are near-equal all year and the tempo would have moved by roughly two per cent; at 51.5°N it moves by a factor of 2.26, which is the difference between the unequal hour being audible and being a footnote.
6 · The strip — colour census and contrast, probe.mjs, section 6
Read back from the canvas bitmap at 1440×900, over the composited plate background rgb(10,10,11), re-measured after the line-6 fix (figure 30, 4 solid and 2 broken; the fix changes how many cells are lit):
lit pixels 6 658
neutral (the marks + the o-7 grid) 6 520
sigil (the cursor) 90
anything else 48
226,228,230 @ alpha 217 x2804 the marks, o-2 15.52 : 1
228,228,228 @ alpha 38 x 221 the o-7 grid
194, 51, 61 @ alpha 255 x 90 sigil #c2333d 3.61 : 1
remaining fringe colours max alpha 85 of 255, edges of the above
cyan pixels anywhere in the strip 0
computed #00f0ff on any property of any element in #fp-host 0
The alpha-38 row is the grid, not a fringe — a review pointed out the mislabel. It is rgba(230,230,230,.15), one device pixel per non-sounding cell, and the count is checkable against the generator, which reports 224 silent cells of 6 × 64 for today's figure 30. 221 of them survive in the bitmap; the sigil cursor column overwrites the other 3, one per row it crosses that was silent. The mark and grid counts therefore move by a few pixels from run to run as the cursor lands in a different column — 2 812 / 219 in the previous run, 2 804 / 221 in this one — while 224, the 90-pixel cursor and the zero cyan do not move at all.
The cursor is exactly 90 pixels for a 90-pixel-tall strip: one crisp, full-strength sigil column. Before the snap fix it measured 193,50,60 @ alpha 128 over two columns — a half-alpha line at 3.57:1, which is a grey by another route. Grid dots and the cursor are floored to the integer device lattice.
The 48 remaining pixels are antialias edges of the two neutral fills, produced by Chromium's unpremultiplication at low alpha. No fillStyle other than rgba(226,228,230,.85), rgba(230,230,230,.15) and #c2333d is ever set.
Text: the readout and the row numbers are rgba(signal, o-4) = 5.23:1 on void per §15; the state line and the scroll hint are --sigil-text #cd5860 = 5.00:1. No text sits below o-4.
7 · Reduced motion — one static, correct, non-blank frame
prefers-reduced-motion: reduce
lit pixels in the frame 6 658
step held at 34
step after 3 100 ms with audio running 34
held true
context state while held running
requestAnimationFrame calls in 1 200 ms 0
No requestAnimationFrame is ever scheduled when opts.reduced is true. The frame is painted once at t = 3.0 s from the step the planetary hour already stands at, so it is a correct reading and not a frozen zero: all six rows, the full 64-step pattern and the cursor are present. The audio still plays; only the drawing is held.
A review found that the two labels describing that frame stopped being true the moment the audio advanced, which is a lie about a held frame rather than an unheld frame. The behaviour is unchanged; the wording now names what it is. Measured (fix-probe.mjs section F):
before the gesture read "step 35 of 64 · hour step 1507 of 4096 · ..."
label "...the vertical rule is the current step."
playing, t+0.4 s read "held at step 35 of 64 · t = 3.0 s · hour step
1507 of 4096 · ..."
label "...Reduced motion is set, so this is a single
held frame at three seconds: the vertical rule
is the step that frame was drawn at, and it
does not follow the sound."
playing, t+3.5 s identical to t+0.4 s, step still 5
after "silence" both revert to the live wording
The prefix appears only when reduced && playing, which is the only state in which the drawing and the sound are on different clocks.
With motion allowed, the strip advances: step 21 → 23 over 2 000 ms at a measured 986.6 ms step. It repaints only when the step index changes, not every frame.
8 · Layout, targets, scroll
1440x900 canvas 1026x90 page h-scroll false strip scrolls false
390x844 canvas 464x90 page h-scroll false strip scrolls true
scroll hint shown true
"listen" button 94.2 x 45.0 px PASS (44 floor)
aria-pressed present and toggling true
console errors / page errors 0 / 0
The 0/0 was true for single presses only, and a review found the press that breaks it. See §10.7 for the fix and §10.6 for the resize case; both are now measured over the states that used to fail.
The strip has a 480px minimum: below it the marks stop being a lattice and become a rule. It scrolls inside its own container and says so in a sigil label, per §10; the hint is added only when it actually overflows and is re-evaluated on resize. The row numbers are position: sticky so they do not scroll out from under the rows they label.
9 · One generator, three consumers
fpMake is defined once. The strip calls it directly; the ScriptProcessor fallback calls it directly; the AudioWorklet module is built by fpMake.toString() concatenated with the processor class. There is no second copy of the formula, so a drawn mark and a heard pulse cannot disagree. No eval and no new Function on the main thread.
The worklet module is loaded from a URL. On the file:// harness the blob: URL is refused; the data:text/javascript;base64 fallback loads and the measured path is AudioWorklet. ScriptProcessor remains the floor and was exercised — it was the path taken before the data: fallback was added, and its measured level then was RMS −23.88 / peak −16.62 dBFS at the old gains.
10 · The two reviews, re-measured — fix-probe.mjs, fix-probe.json
Ten findings, one blocking. All ten reproduced, all ten fixed on disk, all ten re-measured below. Nothing is contested. The plate's own instant during this run: figure 30 (011110, 4 solid / 2 broken), mansion 5, f0 60.95 Hz, hour 4 of 24 (Venus), 67.35 min, step 986.6 ms.
10.1 · Line 6 now runs the rule the page prints
on() computed the broken train as ((k >> ((i+1) % 6)) & 1). For i = 5 the modulo wrapped the pair-partner back to bit 0, so a broken line 6 fired at the line-1 rate. Three printed claims were false at once — "the gap you can see is the gap you hear", "drops the second pulse of every pair", and "each line is a square wave one octave under the line below it" (a broken line 6 was five octaves out). It was visible on the plate: row 6 was spaced like row 1.
The fix states line 6's case instead of borrowing a bit. With all six lines broken (figure 000000), the trains now print:
L6 ................................################................
L5 ................################................................
L4 ........########........................########................
L3 ....####............####............####............####........
L2 ..##......##......##......##......##......##......##......##....
L1 .#...#...#...#...#...#...#...#...#...#...#...#...#...#...#...#..
matches the stated rule, all six lines true
broken density, every line 16 of 64
solid density, every line (figure 111111) 32 of 64
line 6 broken and line 1 solid coincide 8 of 64
(was 0 of 64)
The stated rule is written out a second time inside the probe, independently of the implementation, and compared step by step — so this is the printed mechanism checked against the running one, not the running one against itself. The 8 coincidences retire an undocumented mutual exclusion the XOR inherited. Nested blocks are also simply what a binary counter looks like: the plate now reads as one.
10.2 · The tempo sentence — see §5
Direction inverted, ratio correct. Recomputed at the ephemeris's own observer and checked against the running DOM. stratum.html and copy.md both amended.
10.3 · .fp-wrap takes a canon focus mark
The strip is keyboard-scrollable, so it is a tab stop, and no rule in the stratum or the base sheet covered it: it took Chromium's UA ring, measured rgb(16,16,16) auto 1px — a white 1px rectangle around the strip, wrong colour, wrong offset, reading as a container border. Two rules added:
CSS rules matching /\.fp-.*focus/ [".fp-wrap:focus", ".fp-wrap:focus-visible"]
.fp-wrap unfocused outline-style none, width 0px
.fp-wrap :focus-visible rgb(194, 51, 61) solid 1px, offset 2px
matches :focus-visible after Tab true
button (unchanged) rgb(194, 51, 61) solid 1px, offset 2px
fix-focus-wrap.png and fix-focus-btn.png. The tabindex stays: keyboard scrolling of the strip works and is the reason the stop exists.
10.4 · Both coordinates on the readout advance
E.hourStep was computed once and printed verbatim, so "hour step N of 4096" never moved — two coordinates on one line, one live and one dead, nothing to tell them apart, and wrong by up to 4095 of 4096 across a planetary hour. The readout now takes t and prints (E.hourStep + floor(t / stepSec)) % 4096. Six samples 1.1 s apart during playback:
step 7 8 9 10 11 12 delta +5
hour step 1479 1480 1481 1482 1483 1484 delta +5
deltas equal true
They advance together because they are the same clock counted twice, which is what the line claims.
10.5 · Accessible name, state, and status
The toggle carried its state twice in opposite directions: aria-pressed flipped and the label swapped listen↔silence, so a screen reader announced "silence, pressed" while sound played. A stable aria-label="listen" now fixes the accessible name; the visible text still swaps, and aria-pressed is the only carrier of state.
off visible "listen" aria-label "listen" aria-pressed "false"
on visible "silence" aria-label "listen" aria-pressed "true"
accessible name stable true
state carried only by aria-pressed true
[data-fp="state"] — the only text that reports that a context was opened, on which path and at what sample rate — is now role="status". It changes on press and on mn:ephemeris and nowhere else, so it does not chatter:
live regions in #fp-host 1 .fp-state role="status"
[data-fp="read"] is a live region false
The readout is deliberately not announced: it changes once per step (≈1 s) and would be unusable.
10.6 · Resize mid-play re-derives the bitmap now, not at the next step
onResize() returned early while playing and loop() repaints only when the step index changes, so a resize left an old-width bitmap inside the new box for up to one whole step — measured at up to 480 ms, worst case 1218 ms at the slowest planetary hour, and .px makes that stretch a visibly wrong-pitch lattice. onResize is now unconditional, painting at the same performance.now() clock loop() reads. Sampled every 120 ms while playing:
1440 -> 700 css/bitmap 556/556 556/556 556/556 556/556 556/556 556/556
700 -> 390 css/bitmap 464/464 464/464 464/464 464/464 464/464
max stretch factor over all 11 samples 1.000
ever stretched false
scroll hint agrees with actual overflow, every sample true
hint() is reached from paint(), so the sigil overflow label is now correct in the same instant rather than stale for the same window.
10.7 · The double-press race — BLOCKING, fixed
A fast double-press of listen threw an uncaught page error: cannot connect to an AudioNode belonging to a different audio context. The addModule continuation guarded with if (destroyed || !ctx) return — a null check, not an identity check. stop() nulls and closes context #1; the next press assigns context #2 to the same variable; the in-flight promise from #1 sees a non-null ctx, builds a node on #2 and connects it into a graph closed over #1. Where it did not throw, finish() read currentTime/sampleRate off the wrong context, so the printed "playing · AudioWorklet · 44100 Hz" could describe a graph that was never built.
start() now captures var myCtx = ctx and every continuation compares identity: finish(), viaScriptProcessor(), the .then and the .catch all begin if (destroyed || ctx !== myCtx) return. Every remaining bare ctx read inside chain(), wk() and viaScriptProcessor() is now myCtx, so a stale build cannot touch the live graph even by reading it.
Six on/off/on sequences at each gap, then a settle press:
gap 0 ms 4 ms 12 ms 30 ms 80 ms 200 ms
uncaught page errors 0 0 0 0 0 0
settled aria-pressed true true true true true true
settled aria-label listen listen listen listen listen listen
settled printed "playing · AudioWorklet · 44100 Hz" (all six)
settled ctx.state running running running running running running
console errors across the whole fix-probe run 0
page errors across the whole fix-probe run 0
The 0 ms gap is the one that produced 2 uncaught errors before the fix. The race window is however long addModule takes, and this stratum tries a blob: URL first and falls back to data: — two promise round-trips on the shipped origin — so the window is longer in production than in this harness. Enter and Space both drive the toggle, so it was reachable by held auto-repeat.
10.8 · Reduced motion names its held frame — see §7
Behaviour unchanged (that was already correct); the readout and the canvas aria-label no longer describe it as live.
Not established
-
That the worklet path is what runs on the shipped artifact origin. It was measured on a
file://page, whereblob:fails anddata:succeeds. An artifact served under a CSP that forbidsdata:script URLs would fall toScriptProcessor, which is a deprecated API. Neither branch was tested under a real CSP. - That it sounds good. Nothing about this was judged by ear. The numbers measure level, determinism and bit-ness; they say nothing about whether the result is worth listening to, and no listener was asked.
- Anything on real audio hardware. Headless Chromium with a null sink. The level is measured at the graph, not at a speaker, and no device gain, resampling or Bluetooth path was in the chain.
- The 12 ms edge ramp. Chosen to stop the click, never measured against a shorter or longer one, and never checked for whether it audibly softens the fastest line at the shortest step (539.8 ms).
- Whether XOR is the right combiner. It keeps the stream one bit, which was the constraint. Majority vote and a plain sum were not built or compared, and XOR's ring-modulation intervals were not analysed.
- The mansion→pitch map. One octave over 28 mansions is asserted, not derived. Two octaves, or a just-intoned 28, would be equally defensible and were not tried.
-
Latency between the cursor and the sound. The strip is driven by
performance.now()and the audio byctx.currentTime; they are started together and never resynchronised. Drift over a long session was not measured. -
Behaviour when the ephemeris date changes mid-play. The stratum stops the audio and rebuilds on
mn:ephemeris, which is deliberate — the tempo and the figure both change — but the transition was not measured, only exercised. -
Screen readers. The canvas carries an
aria-label, the state line is nowrole="status", and the accessible name is stable — but all of that is reasoned from the exposed accessibility tree. No assistive technology was run against the plate, and neither review ran one either. -
The worklet paths under a real CSP. Still untested, and the blocking race in §10.7 was longer on the shipped origin than in this harness, because the
blob:attempt must fail before thedata:attempt begins. The fix is an identity comparison and does not depend on the window's length, but the window's length in production was never measured. - Whether the stretched bitmap was ever noticed. §10.6 removes up to 1.2 s of a scaled dither. Nobody was asked whether they had seen it.
- The 44 px target as a target. 94.2 × 45.0 px is a measured box, not a measured tap.
Contested
None. All ten findings from review-doctrine.md and review-a11y-robust.md were reproduced and fixed; §10 is the re-measurement.
Adversarial review · a11y robust
Nothing below is taken from evidence.md. The harness was rebuilt (node artifact/strata2/figure-played/build-harness.mjs, 471 KB) and every number here comes from probes written for this review:
node artifact/strata2/figure-played/review-a11y-probe.mjs -> review-a11y-probe.json
node artifact/strata2/figure-played/review-a11y-probe2.mjs -> review-a11y-probe2.json
node artifact/strata2/figure-played/review-a11y-probe4.mjs -> review-a11y-probe4.json
plus review-a11y-review-over-harness.mjs — artifact/review.mjs verbatim with only its goto URL changed to harness.html (the repository script was not edited). Images: review-a11y-reduced-1.png, review-a11y-reduced-2.png, review-a11y-focus-wrap.png, review-a11y-focus-btn.png, review-a11y-plate-1440.png, review-a11y-plate-390.png, review-a11y-plate-390-scrolled.png, review-a11y-sticky-390.png.
Verdict: blocked — on one reproducible uncaught page error. It is a two-line fix. Everything else the lens asks for passes, several of them handsomely.
Blocking
B1 · A fast double-press of listen throws an uncaught page error
E_gap_0 = {"errors":2, "first":"Failed to execute 'connect' on 'AudioNode':
cannot connect to an AudioNode belonging to a different audio context."}
E_gap_4 = {"errors":0} E_gap_12 = {"errors":0} E_gap_30 = {"errors":0}
E_gap_80 = {"errors":0} E_gap_200 = {"errors":0}
Six on/off/on sequences per gap, review-a11y-probe4.mjs section 1. At a 0 ms gap it throws twice per sequence — once for the pulse chain, once for the sub chain.
stratum.js:299 guards the addModule continuation with
ctx.audioWorklet.addModule(urls[i]).then(function () {
if (destroyed || !ctx) return;
That is a null check, not an identity check. stop() sets ctx = null and closes context #1; the next press assigns context #2 to the same variable. The still-pending promise from context #1 then resolves, sees a non-null ctx, builds AudioWorkletNode(ctx /* #2 */) and chain()s it into the master, lp and gn nodes closed over context #1 — hence the throw. finish() has the same defect: it reads ctx.currentTime and ctx.sampleRate while master may belong to the older context, so even where connect does not throw the printed sample rate can describe the wrong graph.
4 ms was clean in this environment, which is not reassurance: the race window is however long audioWorklet.addModule takes, and this stratum deliberately tries a blob: URL first and falls back to a data: URL (evidence.md §9), so on the shipped origin that is two promise round-trips. A human double-click (~120 ms) or a held Enter (auto-repeat ~30 ms) lands inside it. Confirmed reachable by keyboard: K_enter_pressed_state / K_space_release_state show both keys drive the same toggle.
evidence.md §8 reports "console errors / page errors 0 / 0". That holds for single presses only.
Fix — capture the context and compare identity, in three places:
function start() {
...
ctx = new AC();
var myCtx = ctx; // <- add
...
function finish(path) {
if (destroyed || ctx !== myCtx) return; // <- was: if (destroyed) return;
master.gain.setValueAtTime(0, myCtx.currentTime);
...
setState("playing · " + path + " · " + myCtx.sampleRate + " Hz");
}
function viaWorklet() { ...
ctx.audioWorklet.addModule(urls[i]).then(function () {
if (destroyed || ctx !== myCtx) return; // <- was: if (destroyed || !ctx) return;
...
})["catch"](function () { if (!destroyed && ctx === myCtx) attempt(i + 1); });
}
}
and replace the remaining bare ctx uses inside chain, wk and viaScriptProcessor with myCtx so a stale build cannot touch the live graph.
Should fix
S1 · The scrollable strip is focusable with no canon focus indicator
.fp-wrap carries tabindex="0" and is the first Tab stop inside the plate. Tabbing to it from .plate-cap:
F_tab_from_plate_cap[0] = {tag:"div", fp:"wrap", role:"group",
focusVisible:true, outline:"auto 1px rgb(16, 16, 16) off:0px", boxShadow:"none"}
F_tab_from_plate_cap[1] = {tag:"button", fp:"listen",
focusVisible:true, outline:"solid 1px rgb(194, 51, 61) off:2px"}
F_wrap_css_rule_exists = [] // no :focus rule matches any .fp-* selector
The button is correct. The wrap gets Chromium's outline: auto, which paints a white 1px rectangle around the strip (see review-a11y-focus-wrap.png — compare review-a11y-focus-btn.png, where the sigil ring is right). §15 requires "a 1px sigil outline at 2px offset" on every interactive element, and a white rule around a plate is also a border this canon did not ask for.
Fix — in stratum.css:
.fp-wrap:focus{ outline:none; }
.fp-wrap:focus-visible{ outline:1px solid var(--sigil); outline-offset:2px; }
Keyboard scrolling works and should be kept: 12 × ArrowRight at 390 moved the wrap to scrollLeft 192, its maximum.
S2 · Resizing while playing leaves the bitmap stretched for a whole step
S_playing_resize_immediate = {cssW:464, bitmapW:1026, expected:464, stretchFactor:2.211}
S_playing_resize_after_step = {cssW:464, bitmapW:464, expected:464}
onResize() is if (!playing) paint(...), and loop() repaints only when the step index changes — so after a resize mid-play the canvas is displayed at 2.21× its bitmap for up to one step, 539.8–1218.2 ms. The canvas carries .px (image-rendering: pixelated), so this is a visibly blocky, wrong-pitch lattice, against §10 ("canvases resize by re-deriving their bitmap from the CSS box, never by stretching") and §8 ("a scaled dither is a blur"). hint() is also only re-evaluated from paint(), so the overflow label is stale for the same window.
Fix:
function onResize() {
var t = playing
? (global.performance && performance.now ? performance.now() : Date.now()) / 1000 - t0
: (reduced ? 3.0 : 0);
paint(t);
}
S3 · The toggle's accessible name contradicts its accessible state
K_enter_pressed_state = {ariaPressed:"true", label:"silence"}
K_space_release_state = {ariaPressed:"false", label:"listen"}
aria-pressed and the label both carry the state, in opposite directions. With sound playing a screen reader announces "silence, toggle button, pressed" — which reads as "silence is engaged", the exact opposite of the truth. Two carriers for one bit is one too many.
Fix — pick one. Either keep the name fixed and let aria-pressed speak:
<button ... aria-pressed="false">listen</button> <!-- label never changes -->
or drop aria-pressed and let the label speak. Given the copy leans on the words listen / silence, the cheaper fix is to keep the swap and remove aria-pressed, or add aria-label="listen" so the name stays stable while the visible text still swaps.
S4 · The one line that reports what the gesture did is never announced
K_live_regions_in_host = 0
[data-fp="state"] is the only text saying a context opened, on which path, at what sample rate, and later that it closed — the whole "nothing exists before the gesture" claim, in the UI. It is not a live region, so a non-visual reader gets nothing but the aria-pressed flip.
Fix — role="status" on .fp-state in stratum.html. It changes only on press and on mn:ephemeris, so it will not chatter. Do not put a live region on [data-fp="read"]: that changes once per step (≈1 s) and would be unusable.
Notes
N1 · Sigil text is 4.86:1, not the 5.00:1 evidence.md quotes
Measured against the true composited background, which for these three nodes is the plate (--void-2, rgb(10,10,11)), not --void:
span.label.label-sigil [data-fp=tempo] #cd5860 on rgb(10,10,11) 4.86:1
span.tw-hint [data-fp=hint] #cd5860 on rgb(10,10,11) 4.86:1
span.fp-state [data-fp=state] #cd5860 on rgb(10,10,11) 4.86:1
Still over 4.5:1, so nothing fails. But evidence.md §6 states 5.00:1, which is the ratio against #050505; on the plate the lift buys less than the canon's derivation assumes. Worth one sentence in evidence.md rather than a code change.
N2 · At 390, scrolling the strip loses the legend it depends on
review-a11y-sticky-390.png (scrollLeft 192, the maximum). The sticky column holds — rowsLeft == wrapLeft == 51 — so the digits 6…1 stay put, as designed. But the line glyph gutter, the four marks per row that say solid versus broken, is painted inside the canvas at x = 0…30 and scrolls away with it. The piece's whole thesis is "the gap you can see is the gap you hear", and at 390 the reader can no longer see which lines have the gap. The readout says "3 solid, 3 broken" but not which three. Either paint the gutter into .fp-rows (which is already sticky and already --void-2), or add the six line states to the readout string.
N3 · The reduced-motion pixel count is date-dependent, and evidence.md prints it as a constant
RM_non_blank_lit_px = 6063 (2026-09-03, step 62)
evidence.md §6/§7 = 6 073
Neither is wrong — the count is a function of the day's figure. evidence.md presents 6 073 as if it were a property of the strip.
N4 · ⁴ (U+2074) renders in Departure — now measured, was assumed
<span class="mono">sin⁴(t·1.55)</span> sets a superscript four in Departure Mono. §7 requires a glyph be verified present in both faces before it joins the vocabulary, and U+2074 is not on the canon's list. Measured with contrast-audit.mjs's own method (advance width against # at 40px):
# 25.00 present · 25.00 present — 25.00 present
⁴ 25.00 present ° 25.00 present − 25.00 present
⌗ 23.00 ABSENT ≡ 22.56 ABSENT
So it is safe. Record it; do not assume the next one.
N5 · A blob URL is leaked when both worklet attempts fail
stratum.js:294 creates an object URL; revokeObjectURL(urls[0]) runs only inside the addModule success branch (line 300). If both URLs fail and viaScriptProcessor() takes over, the blob is never revoked. One try/catch revoke at the top of viaScriptProcessor() closes it.
What passed, reproduced from scratch
Reduced motion — one static, correct, non-blank frame
RM_media_matches true
plate screenshot md5, t and t+1s c4d6ba9fe6fc3c88 / c4d6ba9fe6fc3c88 identical
canvas ImageData, t and t+1s {w:1026,h:90,lit:6063,sum:5052199,step:"62"} x2
readout text identical true
lit pixels (non-blank) 6 063
requestAnimationFrame calls in 1.2 s 0
and the harder case — the same, with the audio actually running:
RM_playing_canvas_identical true (t+0.9s vs t+3.1s)
RM_playing_plate_identical true
RM_playing_steps ["62","62"]
RM_playing_state_text "playing · AudioWorklet · 44100 Hz"
RM_errors []
Byte-identical a second apart, non-blank, and correct: the frame is at t = 3.0 from the step the planetary hour already stands at, so all six rows, the full 64 steps and the cursor are present.
Text contrast — every text node in the host, against its true background
C_text_nodes 40
C_failures [] (before the gesture)
C_failures_playing [] (with the context running)
Range 4.86:1 (sigil text on the plate) to 16.33:1 (o-1 emphasis). Nothing below o-4. The six .fp-rows digits sit at 5.22:1 and are inside an aria-hidden="true" container, which is right — the canvas carries the description — but they are still visible text and they still clear the floor.
Controls
button [data-fp=listen] 94.2 x 45.0 px >= 44 PASS
:focus-visible solid 1px rgb(194,51,61) offset 2px PASS
Enter -> aria-pressed true, "silence", "playing · AudioWorklet · 44100 Hz"
Space -> aria-pressed false, "listen", "context closed · no audio node exists"
div .fp-wrap role=group tabindex=0
accessible name "the bitstream, scrollable sideways" PASS
focus indicator see S1
canvas role=img, aria-label present PASS
Resize 1440 → 390 → 1440
1440 docScrollW 1440 clientW 1440 h-scroll false css 1026 / bitmap 1026 strip scrolls false hint hidden
390 docScrollW 390 clientW 390 h-scroll false css 464 / bitmap 464 strip scrolls true hint shown
1440 docScrollW 1440 clientW 1440 h-scroll false css 1026 / bitmap 1026 strip scrolls false hint hidden
No horizontal page scroll at any width; the bitmap tracks the CSS box; the sigil overflow hint appears and disappears with the actual overflow. (The mid-play case is S2.)
Mount / destroy × 10 — on clean, freshly-created hosts
The builder's own numbers were taken by re-mounting onto the host that already carried the harness mount, which double-counts clicks and contexts. Re-done on ten disposable hosts, with the harness mount destroyed first:
listener ledger at baseline {scroll:2, keydown:1, resize:3, mn:ephemeris:2,
pointermove:1, pointerleave:1, blur:1, visibilitychange:1}
listener ledger after 10x identical
L_listeners_leaked_vs_baseline {} <- nothing leaked
AudioContexts constructed 10
AudioContexts still open 0
handle.state() after each destroy "none"
stray [data-fp=cv] left in DOM 1 (the harness host itself)
clearRect calls on class "px fp-cv" over 3 s after the last destroy 0
The rAF rate does rise across the run (30/s → 77/s, and it does not come back down). It is not this stratum: instrumenting clearRect per canvas class for 3 s after every handle is destroyed attributes all of it to ch-cloth (46) and ch-phantom-cv (33) in the base artifact — the field degrading its own resolution and therefore getting cheaper, exactly as §9 says it will. The same rise appears in cycles that never touch the audio (G_noaudio_before 30/s → G_noaudio_after 77.5/s), which rules the stratum out.
artifact/review.mjs with the stratum present
Run verbatim against harness.html:
— faces rendering: {"Departure":619,"Geist":419}
— weights: {"400":928,"500":110}
— non-zero radii: ["U.:50","DIV.ns-controls:50","BUTTON.:50"]
— essence used as fill/border: [{"sel":"U.","as":"background","size":"9x9"},
{"sel":"I.ns-dot","as":"background","size":"10x10"}]
— sigil uses: 0 as background: 0
— CONTRAST FAILURES: 2
{"sel":"BUTTON.btn","size":12,"ratio":1.03,"need":4.5,"txt":"disabled"}
{"sel":"BUTTON.btn","size":12,"ratio":1.94,"need":4.5,"txt":"disabled"}
— landmarks: {"nav":2,"main":1,"header":1,"footer":1} lang: en
— svg icons: 0 emoji: 0 img w/o alt: 0
— focusables: 66
— H1 count: 1 headings total: 34
— pageerrors: none
2 contrast failures — both the exempt disabled buttons — and 0 page errors, as the brief requires. The two essence fills are the base artifact's living dot and the namespace dot, not this stratum: the stratum sets exactly three fill styles, rgba(226,228,230,.85), rgba(230,230,230,.15) and #c2333d, and introduces no new hex, no radius, no icon and no gradient.
Races that did not misbehave
T_race_rows (gaps 0/20/60/150/400 ms, on-off-on then settle)
every row: pressed "true", label "silence",
printed "playing · AudioWorklet · 44100 Hz", ctx "running"
T_any_lying_state false
X_after_ephemeris_midplay {pressed:"false", label:"listen",
printed:"context closed · no audio node exists",
ctx:"none", open:0}
The printed state never contradicted aria-pressed in any settled state, and a mid-play mn:ephemeris stops the audio, closes the context and returns the button coherently. The failure in B1 is the throw, not a lying readout.
What this review did not establish
- Screen readers. I measured the a11y tree and the announced properties; no NVDA/VoiceOver/Orca was run. S3 and S4 are reasoned from what the tree exposes, not from a listening session.
- The audio itself. Deliberately out of this lens, and still unheard — the builder's own self-critique stands.
-
CSP. Both worklet URL paths remain untested under a real Content-Security-Policy, as evidence.md already says. B1's race is longer on any origin where the
blob:attempt has to fail first, which is exactly the shipped configuration evidence.md describes. - Real touch. 44px was measured as a box, not as a tap.
- Whether S2's stretched frame is noticed. Measured as a fact about the bitmap; no reader was asked whether 1.2 s of blur registers.
Adversarial review · doctrine
Reviewer's own harness and probes, not the builder's numbers. Everything below was reproduced in Chromium (--use-gl=swiftshader --enable-unsafe-swiftshader) against a freshly rebuilt harness.html.
Re-run:
node artifact/strata2/figure-played/build-harness.mjs
node artifact/strata2/figure-played/review-probe.mjs → review-probe.json
node artifact/strata2/figure-played/review-probe2.mjs → review-probe2.json
node artifact/strata2/figure-played/review-probe3.mjs → review-probe3.json
Images read back and judged: shot-1440.png, shot-390.png, crop-plate.png, review-plate.png, review-focus-wrap.png, review-reduced.png.
Verdict — ships with fixes. The load-bearing claims hold: no AudioContext before the gesture, no essence anywhere in the stratum, no grey fill, no tofu, byte-identical output for a date. Six things are wrong, two of them are printed prose that states a measured relation backwards or describes a mechanism the code does not run, and one is a keyboard-focus target with no sigil ring.
What was reproduced and passes
| Claim | Reproduced | Value |
|---|---|---|
No AudioContext before the gesture | yes | Proxy over AudioContext / webkitAudioContext / OfflineAudioContext installed in addInitScript; 0 constructions at load, 1 after the click, state running, path AudioWorklet; 0 blob:/data: script resource entries before |
| Closed on second press | yes | handle state none, data-fp-path removed, printed context closed · no audio node exists |
| Level under the −18 dBFS ceiling | yes | live analyser 122 880 samples: RMS −27.06, peak −20.16 dBFS. Independent OfflineAudioContext 6 s/48 k of the same chain: RMS −26.57, peak −19.30 dBFS, 0 clipped samples |
| Same seed, same output | yes | four dates, two runs in one mount plus a second independent mountStratum_figureplayed() on a cloned host — all three byte-identical per date, four distinct streams across four dates. No Math.random in the file |
| Derivation matches the plate | yes | recomputed from MN_derive at the plate's own instant: figure 25 ✓, mansion 5 (raw 4, +1) ✓, f0 60.948 Hz ✓, step 986.6 ms ✓, and both agree with the published MN_EPHEMERIS.figure / .mansion |
| Essence budget | yes | 0 cyan-ish pixels in the strip bitmap; 0 computed #00f0ff on any property of any element in the subtree |
| No grey fill | yes | only two backgrounds in the whole subtree, both substrate tokens: .plate and .fp-rows at rgb(10,10,11) = --void-2 |
| Text floor | yes | every text node ≥ 4.5:1 on its true opaque parent. Lowest is 4.86:1 (--sigil-text on --void-2); labels/readout o-4 = 5.22:1 |
| Sigil is a line, sigil-text is text | yes | canvas cursor 194,51,61 @ alpha 255, exactly 90 px for a 90-px strip — one crisp full-strength column. All sigil text measures rgb(205,88,96) = #cd5860 |
| No glyph Departure cannot render | yes | the only non-ASCII in a Departure element is · U+B7, — U+2014, ⁴ U+2074 — all three measure exactly the # advance (25.00). The same probe correctly rejects ⌗ (23.00), ≡ (22.56), ☰ (36.00), ⋮ (40.00). ⁴ already appears once in design-system.src.html, so it sets no new precedent |
| Reduced motion is one static non-blank frame | yes | 6 073 lit pixels, step held 16 → 16 across 3 s of playback, no rAF scheduled |
| 44px target, no page h-scroll | yes | button 94.2 × 45.0; at 390 documentElement.scrollWidth 390, strip overflows internally and the sigil hint is shown |
mn:ephemeris rebuild | yes | dispatching 2026-12-21T23:30Z moved figure 25 → 43 and step 986.6 → 1185.0 ms, plate reprinted, 0 page errors |
destroy() is clean | yes | context closed, later mn:ephemeris and resize events are inert, 0 errors |
| The tempo numbers | yes | 730 hours of 2026 recomputed independently: min 36.8522, max 83.1655 min; step 539.8 – 1218.2 ms; extreme ratio 2.257 |
The copy does say plainly what the brief requires — "No name and no judgement is attached to any figure: it is a procedure with a reason, not a claim about the world." It is a calendar, not a horoscope, and it says so on the plate.
Findings
1 · The top line's broken train is not the rule the page prints — should-fix
stratum.js:54
return ((k >> ((i + 1) % 6)) & 1) ? 0 : 1; // broken: half
For lines 1–5 the modulo is inert and the rule is the stated one. For line 6 (i = 5) it wraps bit 6 to bit 0, and the top line stops being the top line. Measured (review-probe2.json → J_line6, figure 0, all six lines broken):
L6 ................................#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.
L5 ................################................................
L4 ........########........................########................
L3 ....####............####............####............####........
L2 ..##......##......##......##......##......##......##......##....
L1 .#...#...#...#...#...#...#...#...#...#...#...#...#...#...#...#..
matches_stated_rule is true for lines 1–5 and false for line 6.
Three consequences, all of them contradictions of printed copy:
- The lede — "the gap you can see in the drawn line is the gap you hear" — and the body — "a broken line drops the second pulse of every pair" — are false for one line in six. Line 6 has no pair inside a 64-step pattern; it has one block, and the code silently substitutes a rattle at 32× the line's own rate.
- The body also claims "each line is a square wave one octave under the line below it". A broken line 6 emits at the line-1 rate, which is five octaves out.
- With today's figure 25 (line 6 broken, line 1 solid) the two lines fire on opposite parities and never coincide — 0 of 64 steps — an undocumented mutual exclusion that the XOR combiner then inherits.
Fix. State the top line's case rather than wrapping it. Inside on(), after the line's own train has been confirmed:
if (i === 5) return ((k & 31) < 16) ? 1 : 0; // one block, keep its first half
Same 16/64 density, the gap lands in the middle of the block where the drawn line's gap is, and no bit is borrowed from another line. Then amend the lede, the body paragraph "Six trains from one counter", and evidence.md's "What it is" to say that the top line halves its single block because there is no pair to drop.
2 · The tempo sentence is backwards — should-fix
stratum.html:31, copy.md:38:
the piece is 2.26 times faster on a midsummer afternoon than on a midsummer night
Recomputed from MN_derive at the ephemeris's own observer:
2026-06-21 14:00Z day hour 83.165 min → step 1218.2 ms (the slowest)
2026-06-21 23:00Z night hour 36.852 min → step 539.8 ms (the fastest)
A longer planetary hour is a longer step, so midsummer afternoon is the slow end and midsummer night the fast one. The ratio 2.26 is right; the direction is inverted. In a canon whose practice is that every load-bearing number is measured, a measured relation printed backwards is worse than an unmeasured one.
Fix. Swap the two words in stratum.html:31 and copy.md:38: "the piece is 2.26 times faster on a midsummer night than on a midsummer afternoon."
3 · A keyboard focus target with no sigil ring — should-fix
.fp-wrap carries tabindex="0" and role="group", so it is in the tab order before the button (review-probe3.json → M_tab_order). It matches :focus-visible, and no rule in stratum.css or the base sheet covers it, so it takes Chromium's UA ring — measured outline: rgb(16,16,16) auto 1px, which renders as a 1px white box around the whole strip (review-focus-wrap.png). §15 requires a 1px sigil outline at 2px offset on every interactive element; a white 1px rectangle is both the wrong colour and a border on a container.
Fix. In stratum.css:
.fp-wrap:focus-visible{ outline:1px solid var(--sigil); outline-offset:2px; }
4 · "hour step N of 4096" is frozen while the piece plays — should-fix
E.hourStep is computed once in derived() and readout() prints E.hourStep + 1 verbatim. Measured over 6 s of playback (review-probe3.json → N_hourstep_advances):
step 49 → 50 → 51 → 52 → 53 → 54 (advances)
hour step 2801 → 2801 → 2801 → 2801 … (frozen)
Two coordinates on one line, one live and one dead, and nothing says which. Over a 67-minute planetary hour the printed hour coordinate is wrong by up to 4 095 of 4 096. This is the stratum's own instrumentation asserting something it is not measuring — the failure mode §16 exists to catch.
Fix. readout() already receives the step; give it t too and print (E.hourStep + Math.floor(t / stepSec)) % 4096, or drop the hour coordinate.
5 · Resizing while playing stretches the canvas — should-fix
onResize() returns early when playing, and loop() repaints only when the step index changes. Measured at 1440 → 700 during playback (review-probe2.json → I_resize_playing, sampled every 120 ms):
556/1026 556/1026 556/1026 556/1026 556/556 556/556 …
a 1026-pixel bitmap held inside a 556-pixel box for ~480 ms, worst case one full step — 1 218 ms at the slowest tempo. §10: canvases re-derive their bitmap from the CSS box, never stretch. §8: a scaled dither is a blur. Stopped, the resize is handled correctly (704/704).
Fix. Make onResize unconditional:
function onResize(){ paint(playing ? (nowSec() - t0) : (reduced ? 3.0 : 0)); }
with nowSec() the same clock loop() uses. This also fixes hint(), which is only ever called from paint().
6 · Under reduced motion the printed record and the sound diverge — should-fix
Measured: with prefers-reduced-motion: reduce the context is running and the strip is held — step 16 → 16 over 3 s, readout step 17 of 64 unchanged. The canvas aria-label reads "the vertical rule is the current step" and the readout reads "step 17 of 64"; both are false the moment the audio advances. The stratum's own headline claim is "a mark and a pulse cannot drift apart" — this is the one configuration where they do, by design and without saying so in the instrumentation.
Fix. When reduced && playing, change the wording rather than the behaviour: readout prefixed held at step 17 of 64 · t = 3.0 s, and swap the canvas aria-label for one that names a held frame. Alternatively hold the audio too, but the body copy's "the audio still plays" is a defensible choice — it is only the two live-sounding labels that lie.
7 · Token values are retyped in JS, so a namespace cannot reach them — note
stratum.js sets "#c2333d", "rgba(226,228,230,0.85)" and "rgba(230,230,230,0.15)" as canvas fill styles. They are the right values today, but §14 is explicit that a restated token is a second source of truth, and §13's contract says a [data-ns] block overriding --ns-sigil moves the whole surface. Here the cursor would stay crimson while every other line on the page moved. strata2/mark-weather already sets the precedent for the fix: it rasterises from its own computed style.
Fix. Read getComputedStyle(host) once per size change for --sigil, --witness and --signal, and compose the fill styles from them.
8 · evidence.md §3 misreports its own percentage — note
Evidence states "exactly one bit (or a rest) 97.80%" from counts it prints in the same block: 140 380 + 140 132 + 1 = 280 513 of 288 000 = 97.40%. My own render of the same chain gives 139 931 + 140 581 + 1 with the identical 7 487 remainder — 97.40%.
Fix. 97.80 → 97.40.
9 · evidence.md §6 mislabels the o-7 grid, and §3 samples a tenth of the pattern — note
-
§6's census row "
228,228,228 @ alpha 38 x 235 antialias fringe" is not a fringe. Alpha 38 is 0.15 × 255 — that row is theo-7grid, the one thing the census never names. For figure 25 the arithmetic checks: 384 cells − 144 lit = 240 grid dots expected, 235 measured after the cursor column overlaps five. -
§3 renders 6 s at the current tempo — about 6 of the 64 steps. So "exactly 0 (no line sounding) — 1" is not evidence that a rest is a zero rather than a level: the window never contains step 0, the pattern's only rest (
review-probe.json → C_rests: 1 rest step in 64, at k = 0).
Fix. Label the alpha-38 row as the grid, and drive the bit census with an explicit stepSec short enough that ≥ 64 steps land inside the render.
10 · 28 mansions, 27 pitch classes — note
f0 = 55 · 2^((m−1)/27) puts mansion 28 at exactly 110.000 Hz, an octave above mansion 1 — measured, review-probe3.json → O_octave_edge. The copy says the mansion "divides one octave from 55 Hz as it divides the ecliptic into twenty-eight", but the ecliptic division yields 28 distinct arcs and this one yields 27 distinct steps with the ends collided. evidence.md already lists the map as asserted-not-derived; it does not mention the collision.
Fix. Either /28 (mansion 28 lands at 27/28 of the octave and the wrap is a real step), or say plainly that the map is inclusive of the octave and that mansions 1 and 28 sound an octave apart.
11 · The worklet load guard checks a context, not the context — note
start()'s addModule callback tests if (destroyed || !ctx) return;. After a stop-then-start, ctx is a different, live context, so a stale promise from the previous context passes the guard and would construct AudioWorkletNode(newCtx) and connect it into the old, closed master.
I could not make it fire: four synchronous clicks, and five clicks at 8/3/12/2/20 ms, both ended in a correct state with 0 console errors and 0 page errors, and the level after the thrash measured a normal −19.49 dBFS peak. So this is a latent guard bug, not an observed defect — reported because the guard is wrong, not because it broke.
Fix. var myCtx = ctx; at the top of start(), then if (destroyed || ctx !== myCtx) return;.
Judged as a designer
At 1440 the plate is restrained and reads as this canon: one cap row, six rows of witness marks over a hairline grid, one crimson column, one secondary button, one sigil state line. No second cyan, no icon, no badge, no gradient, square throughout. It is a printed record with a cursor on it, which is exactly what the copy says it is — CANON-SEED 4 satisfied by a single constraint (a one-bit stream derived from three quantities already on the page) rather than by flourishes, and rule 9 satisfied by there being no glitch decoration at all.
At 390 the strip scrolls inside its own container, the sigil hint appears, the row numbers stay stuck under their rows, and the page does not scroll sideways.
The one thing the eye catches before the code does is row 6: its marks are spaced like row 1's, not like a top line's, which is finding 1 visible on the plate.